Surface - A component-based library for Phoenix LiveView

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:

It doesn’t need to be exclusively one way or the other.

I personally no longer go the “dead view” route :laughing: for new projects with Phoenix. With that said, I still have a few classic http endpoints where it doesn’t make much sense to port them over to LiveView (think auth pages).

I’ve been keeping tabs on Surface ever since it surfaced :smiley: . It’s such a joy to work with and complements LiveView so well, that I can’t imagine myself developing LiveView apps without it at this point.

That’s what kept me away from Surface at first. I’ve wanted to stay as close as possible to LiveView, considering it hasn’t reached 1.0 yet, without adding an extra layer of complexity on top of it. I was wrong on that call. Just as I was wrong about Tailwind and it’s approach to CSS (it’s one of those things you have to try and keep at it to understand its benefits).

1 Like

Thanks for the insight. And I will definitely adopt the “dead view” term. :laughing:

This is exactly what I had in mind. We have a login screen that uses form fields, that would otherwise use the exact markup as the ones inside our live views. Do we bite the bullet and just suffer the duplication between the markup in this regular EEx template and our Surface components?

That’s what I did for the /login, /signup, /password-reset etc views. I have them hard-coded with classic Phoenix.HTML code until it will be possible to bring in Surface stateless components. They’re rather simple in nature, so the hardest thing was to keep the style in sync with the rest of the app (how an input/button/form is styled since you can’t re-use the components where these are set).

I even do auth in live_view. It is not the most efficient way but the uniformity is very pleasing.

How do you do it? I think you still need to do like a page redirect to set the cookies, so at one point the LiveView has to redirect to a controller, which performs session operations, and then goes back to LiveView, right? That’s how I do it but maybe I miss on something…

I don’t use cookie. I use LocalStorage to store the JWT token client side and send it back to LV on mount for it to verify.
If the token check out, go ahead. If not, push_redirect to a simple LV backed login form.

Ah yes, that’s doable. But you need a hook for that, right?