Surface - A component-based library for Phoenix LiveView

Ah yes, of course. Haven’t really used named slots yet, just read about them a little. Makes sense.

In the end I went back to my original implementation, but took a lead the Dialog example to encapsulate some logic and use send_update/2 — works beautifully!

I think I’ll reduce my usage of Alpine to really simple scenarios that can be done with x-show or where simple transitions are required.

Thanks @sfusato!

Hi @benregn! I believe the only way to make that work today would be to implement render/2 in you view to avoid having Phoenix handling the template directly. Then you could you use ~H inside render/2 as in any other Surface component. In case that doesn’t work or if it’s not ideal for you, please open an issue at https://github.com/msaraiva/surface/issues/ so we can discuss possible solutions.

Hi @darraghenright! We’re currently discussing some changes in Surface’s syntax to avoid conflicts with client libs. One change that is already scheduled for the next version is to ignore <template> and <slot> and introduce their surface’s component counterparts: <Template> and <Slot>. In the meantime, if you need to make <template> hit the browser, you can use <#Raw> to bypass Surface’s compiler, for instance:

  <#Raw><template x-if="showModal"></#Raw>
    <div class="modal">
      ...
      <button @click="showModal = false">close</button>
    </div>
  <#Raw></template></#Raw>

Don’t forget to alias Surface.Components.Raw in your component.

3 Likes

Absolutely fantastic. Thanks a million @msaraiva!

I wonder if you might help scribble out how you would handle a theoretical “Card” component with optional slots.

So imagine you wanted a final template to look like:

  <Card>
    <Header class="stuff">
      header
    </Header>

    Body text

    <Footer class="footer stuff">
      my footer
    </Footer>
  </Card

So I initially thought to model this as per your example:

defmodule ExampleWeb.Component.Card do
  @moduledoc false

  use ExampleWeb, :surface_component

  slot header

  slot default

  slot footer

  def render(assigns) do
    ~H"""
    <div class="card">
      <div class="@header_class">
        <slot name="header"/>
      </div>
      <div class="body class">
        <slot />
      </div>
      <div class="@footer_class">
        <slot name="footer"/>
      </div>
    </div>
    """
  end
end

But what if we add in the requirement as here that we want to pull the header and footer classes out of the slots and also want to suppress rendering of one of the sections if it’s blank? ie if the header slot is say empty then we render nothing at all

I suppose that the first step is to stop using slots and instead make the header and footer a component so that they can render their own divs, rather than doing it in the Card component? However, is there a different solution?

I’m not quite sure of my use case yet, but I’m thinking about situations where I might have say optional icons or some other attribute and I’m wondering how I might say compute some css class that would depend on whether the icon slot had been filled or not? Can I get your thoughts?

Note, really liking Surface so far! Thanks for creating it!

1 Like

Hi @the_wildgoose!

You can use slot_assigned?/1 to check if a slot has been assigned or not.

Assuming only header and footer are optional, something like this should be enough:

  slot header

  slot default, required: true

  slot footer

  def render(assigns) do
    ~H"""
    <div class="card">
      <div :if={{ slot_assigned?(:header) }} class={{ @header_class }}>
        <slot name="header"/>
      </div>
      <div class="body class">
        <slot/>
      </div>
      <div :if={{ slot_assigned?(:footer) }} class={{ @footer_class }}>
        <slot name="footer"/>
      </div>
    </div>
    """
  end
2 Likes

You can use my implementation here as inspiration:

This is all really helpful! Thanks guys! I can get a lot of inspiration from the bulma components, some of this is not so easy to get your head around just from the docs, eg I found the button example helpful with the multiple “is_@colour: @colour” kinds of properties, as an example of what is possible! Nice!

As I have your attention, I have 2 follow on questions.

1 - I posted this also as an issue on github, but it seems to me that in the Card example, once you declare “Head” as a slot, then it’s render function becomes disabled? Looking at the code I’m not sure why though. I see that component declares a default render function in this case, but also marks it overrideable? I tried to follow through the rest of the render path, but it’s quite complex…

It seems useful to me to be able to set a render function for slots. The use would be that the upper level component understands where to place the slot in the layout, but the sub component might know how to render that slot.

Could we support this easily?

2 - I guess this is a failure to read the docs right, but could you give me a leg up on understanding how I would fish out a property from a slot component and use it back in the upper component,

eg imagine we had

<Footer class="footer_class" show="true">
  stuff
</Footer>

and we want to place this into a template, but how can we read the Footer class/show variables? There is an example in the grids section, but I’m not sure I understand how to apply what is written?

eg I want to achieve something like:

<Card>
  stuff
  <div class=@footer.class>
    <Footer class="footer_class" show="true">
      more stuff
    </Footer>
  </div>
</Card>

Thanks for your advice

OK, another, hopefully quick question:

How would I access the “flash” from a component?

So I can see in the code there are default “data flash” properties already setup. In the top level view I can access either @flash or assigns.flash, however, this has disappeared from the assigns list once I am in the render function for some component called from the view?

In most ways I expect this result. However, looking at the code I wonder if you wanted it to be otherwise? Should we pass down the flash value to sub components ? (or am I just missing the obvious way to do it?)

At present I can achieve the end result by wrapping my top level view with a <Context put={{flash: assigns.flash}} and then do a get in the sub component(s). However, I can’t create a “prop flash, :map”, which would allow passing as a simple property, because there is this already existing data definition? (obviously I can create a different name - my point was that the name looks reserved for some use?)

My use case is that my view looks something like:


~H"""
<SiteBoilerPlate>

... interesting stuff
</SiteBoilerPlate>
"""

and I would like to shift managing the display of the flash into this SiteBoilerPlate component, which generates all the outer framework of the views.

Thanks

G’day, just wondering if it’s possible to pass components as props to components?

Say I’ve got a Button component, and some Icon components. Sometimes I’d like to show an Icon component with the text on a Button… this works well:

<Button>
  <Icon.Pencil class={{"-ml-1", "mr-2", "h-5", "w-5"}} />
  Add New
</Button>

The API I’d like to provide on the Button component should hopefully allow something like this:

<Button icon={{ Icon.Pencil }} title="Add New" />

Then the button component can add those utility classes to the Icon. I see there is a :module property type, but when I set the icon prop to that, I need some way to ‘instantiate’ it as a component.

Well, this was pretty simple in the end, but I’d love to hear if it’s unsupported/frowned upon/likely to blow up in my face! My Button component ended up looking something like:

defmodule Component.Button do
  use Surface.Component

  prop title, :string, required: false, default: ""
  prop icon, :module, required: false, default: nil
  slot default, required: false

  def render(assigns) do
    ~H"""
    <button>
      {{ get_icon(assigns) }}<slot>{{ @title }}</slot>
    </button>
    """
  end

  defp get_icon(%{icon: icon}) when icon != nil do
    icon.render(%{class: ["-ml-1", "mr-2", "h-5", "w-5"]})
  end

  defp get_icon(_) do
    ""
  end
end

3 Likes

Hi!
Is there a way to access a prop in mount/1 of a Surface.LiveComponent? Essentially I am passing the currently logged in user from my live view as a prop and would then like my component to fetch extra information from the database, for which I would need to access that user id. At the time mount/1 is called, though, it does not seem like the assigns are available?
Right now I am just loading the information in the view itself, but that does not encapsulate the concerns perfectly.

Cheers

You could probably do that through update/2.

Remember that overriding update function requires you to socket = assign(socket, assigns) (because this is default behaviour).

This way you can also change the loaded data if the prop should change as well. It’s your codes responsibility to not load excessively, so check if socket.assigns.your_data_prop holds the data before you load and assign to socket again.

Edit:

In general the send_update combo with update/2 is a very powerful way to model self contained components.
In SurfaceBootstrap I have a public api as such:

# External API

  def show(id) do
    send_update(__MODULE__, id: id, action: :show)
  end

  def hide(id) do
    send_update(__MODULE__, id: id, action: :hide)
  end

With my update/2 looking like this:

def update(assigns, socket) do
    socket = assign(socket, assigns)

    socket =
      case assigns[:action] do
        nil ->
          socket

        :show ->
          push_event(socket, "bsn-show-modal-#{assigns.id}", %{})

        :hide ->
          push_event(socket, "bsn-hide-modal-#{assigns.id}", %{})
      end
      |> assign(:action, nil)

    {:ok, socket}
  end
3 Likes

Hi folks!

Last week, Surface v0.3.0 has been released and it’s now available on hex.

This version improves the Surface Catalogue API which was previously released in v0.2.0 and also introduces a new surface compiler that allows autoloading colocated JS hooks.

Source code and installation instructions for the Surface Catalogue can be found at GitHub - surface-ui/surface_catalogue. If you want to see it in action, there’s a short video available at https://twitter.com/MarlusSaraiva/status/1360254701808324615. The full changelog can also be found at surface/CHANGELOG.md at master · msaraiva/surface · GitHub.

We are happy to see that Surface is becoming more and more popular. There’s big a chance Surface is already part of many developers stacks, so it becomes essential to support these developers to have a stable version 1.0 of Surface.

Let’s not be afraid of words, our ambition is to make Surface the best possible platform for creating LV projects! To move forward on this path, we’re pleased to announce that @Malian and @miguel-s have joined our core team and will be helping us to improve Surface.

If you would like to shape the future with us, report issues or simply need help, feel free to join us on the slack channel or on github!

Happy coding!

27 Likes

I was trying to use the built in flash, but couldn’t figure out a way, so I just renamed it and wrapped around my existing utility class:

  # in surface component
  prop message, :map

  def render(assigns) do
    ~H"""
    {{ Helpers.render_flash(@message) }}
    """
  end

# used in parent component
<Flash message={{ @flash }} />
1 Like

I am trying to rewrite live.html.leex into a Surface.LiveComponent. Therefore, I need to access @flash from within the component. However, if I have:

data flash, :map 

then it complains:

cannot use name "flash". There's already a built-in data assign with the same name.

If I remove that line then it compiles, but from within the component the @flash seems to be an empty map?

As a general question, if I need to share some data assign between multiple Surface LiveView/LiveComponent, can I do that?

After I read some discussion above, I think I can pass the flash into a prop. However, how do I clear the flash from within the component then?

1 Like

To answer my own question, I think components cannot share data assigns. So to factor out the flash display out in a component, I have to use a stateless component and pass the @flash in as a prop. Inside the component I can do:

phx-value-key="error" phx-click="lv:clear-flash"

as in plain LiveView. and it will clear the flash assign. The whole thing is reactive, so the passed in prop is changed as well.

1 Like

Hi @derek-zhou!

This has been fixed. If you try out the version on master, you should be able to define a prop named flash in any component (live or not). Thanks for reporting this issue.

2 Likes

@msaraiva, I found myself using :attrs and :props more and more, instead of specifying attrs and props individually. As I understand it, :attrs are for html elements only, and :props are for components only. My question is why do we need both constructs? It is not like there can be any confusion?

If for example we only have :props for both html elements and components it is just one less thing to remember and the components will feel more like super elements as it is intended to be?

Attrs can only go on raw elements, and that is an important distinction to help hygiene imo.

1 Like

This week I’ve been pondering whether to introduce Surface to our project or not. Our front-end devs seem to be happy with how close this is to what they’re used to from Vue etc., so that’s great.

I’ve read up everything I could (including this thread) and it seems there is a lot of interest in rendering Surface components without LiveView. I understand that this is not in the cards at the moment, so I want to ask the opposite question.

How terrible would it be if I decide to build our project exclusively with LiveView just so we can use Surface? It is a new project so there would be no rewriting needed. It’s mostly a question of introducing additional complexity. Before looking into Surface my estimate was that we would need LV to build out some parts, maybe even the majority, but not everything.

Please talk me (to jump) off this ledge. :laughing: