Surface - A component-based library for Phoenix LiveView

Just started really playing around with Surface, and gotta say nice work y’all! It’s really nice to have typed templates.

I’ve created a few widgets to help with standard IoT/Dashboard things I build a lot (for work and personal), see SurfaceBulmaWidgets. Warning, they probably break a few best practices. :wink:

What I really wanted to share for others was a “bind” concept and macro implementation. See SurfaceBulmaWidgets.Bindings. It draws on my previous experience writing bulma_widgets (which was pre-live-components IIRC).

SurfaceBulmaWidgets.Bindings takes/passes the name of the variable along with it’s value to the child widget (currently Surface.LiveComponent’s). The widget then sends the parent a standardized event {:update_bind, name, value}.

It’s somewhat like contexts but I prefer having a standard event and not needing to pass entire contexts around. It’s basically Vue’s bind mechanism.

defmodule SurfaceBulmaWidgetsPlaygroundWeb.HomePageLive do
  use Surface.LiveView
  require Logger
  use SurfaceBulmaWidgets

  alias Surface.Components.LiveRedirect
  alias SurfaceBulmaWidgets.Layouts.{Columns, Column}
  alias SurfaceBulmaWidgets.UI.{RangedSlider, NumberDisplay|

  data count1, :integer, default: 10
  data count2, :integer, default: 80

  def render(assigns) do
    ~H"""
      <Columns multiline>
        <Column width=4>
          <div class="box" >
            <p>Example Reactive Widgets</p>

            <RangedSlider id="rl1" var={{bind(@count1)}}/>
            <RangedSlider id="rl2" var={{bind(@count2)}}/>
            <NumberDisplay id="ndl2" name="Count 2" value={{bind(@count2)}} digits=1/>
            <p><LiveRedirect label="Learn more" to="/uicomponents" /></p>
          </div>
        </Column>
      </Columns>
    """
  end

  # Override: `handle_info({:update_bind, name, value} = msg, socket)` for custom behavior
end
1 Like

For anyone on the upgrade path from Surface version 0.3.* or less to 0.4.*, I created this mix task which converts the deprecated “atom-string shorthand” syntax.

This:

<SomeComponent atom_prop="warning">

is turned into this:

<SomeComponent atom_prop={{ :warning }}>

In 0.4.0 there is a deprecation warning, but in the upcoming 0.5.0 it will fail compilation.

3 Likes

I just read this (Proposal for syntax changes in Surface v0.5 · GitHub). So the plan is to move from the mustache style syntax {{...}} to svelte style syntax {...}. I want to know if the old style will be supported indefinitely, or do we have to migrate all code at some point?

Surface v0.5 has already been released!

An overview of the new syntax can be found at Template Syntax.

We advise all users to upgrade to the new version as the old syntax will no longer be supported.

To make the migration process as smooth as possible, Surface v0.5 ships with a mix surface.convert task that converts the old syntax into the new one.

For detailed instructions on using the converter, see the Migration Guide. Please make sure you read the instructions before running it.

Full CHANGELOG can found at surface/CHANGELOG.md at master · surface-ui/surface · GitHub.

3 Likes

Thank you for this upgrade. I recently upgraded a project to version 0.5 and it took less than an hour (small project with ~15 components). I had to mind the quoted="{@values}" though. Really love the new syntax.

the converter works like a charm, thanks!

1 Like

@code-shoily did you use mix surface.convert or did you replace everything manually? The converter should have handled those interpolations too.

2 Likes

I tried mix surface.convert at first, but I recall it did not convert some of the {{ }} so I tried the manual process instead. I had put that in my todo list to circle back to it and inform if needed. In fact, I have a version 0.4.0 branch and I have some time to play around, I will just run mix surface.convert on it and come back with diffs :slight_smile:

I could totally be wrong about it too.

Update: I was!

So I just did it again, looks like it was my fault, I had made a mistake on the command I ran. First I tried mix surface.convert which didn’t convert the sface file but then when I added the path argument I made an error (missed the **) there which was causing the files to not convert. I tried the command right now and it works.

(I tried mix surface.convert "lib/**/*.{ex,exs,sface}" "test/**/*.{ex,exs}" pasted from the docs). Well, I don’t regret manually changing though, gave my muscle memory some cool adjustments but I totally recommend and mix surface.convert. Thank you again.

1 Like

@msaraiva, it appears to me that the new surface syntax is moving to be very close to that of svelte. I really appreciate the trend as the resemblance can increase productivity and lower barrier to entry. However, I see there is still some discrepancies. In Surface, we have:

{#if @a > 0}
...
{#elseif @a < 0}
...
{#else}
...
{/if}

but in svelte we have:

{#if a > 0}
...
{:else if a < 0}
...
{:else}
...
{/if}

the difference is subtle but still there. It could throw people off. Since we are making the new syntax anyway, why don’t we try to follow existing convention closer?

We can’t have the same Svelte syntax because it would be ambiguous as there’s no way to distinct, for instance, whether {:else} is a sub-block for {#if} or an ordinary expression containing an atom :else. Having a different embedded language (Elixir vs JS) imposes different choices when defining the syntax.

Besides, I don’t want to have our templating solution fully anchored to any specific language as this would limit our choices as well as bring false expectations about its use and evolution.

We certainly bring features inspired by different languages but we always need to make sure they are feasible in our context :wink:

6 Likes

That can be worked around by requiring a leading atom to have a space after the curly. eg: {:else} vs { :else}. I understand the dilemma though.

One more question, with the new {#XXX} syntax, are we going to deprecate all colon attributes, such as :for, :props and :if? There is not many left, and for things like :on-click we can go back to phx-click.

:for, :attrs and :props will be deprecated. The others will be kept.

Keep in mind directives manipulates the code at compile-time, usually introducing additional behaviour to improve DX. They are similar to Elixir macros in that sense.

So just as we don’t create Elixir macros to map an existing function without adding some extra behaviour, we don’t map existing attributes to do the same thing. We prefer to create a new abstraction leaving the semantics of existing attributes untouched.

For instance, the :on-click directive, by default, automatically generates both, phx-click and phx-target based on the type of the host component (stateful or stateless). That’s why we don’t use the :phx- prefix. We want to make it explicit that they are not the same thing and if you want to use the original phx- attributes, you can still do it and they should work with the same behaviour as they were originally designed.

What’s the difference between :if and {#if}, if :if is not going to be deprecated?

One is basically an attribute on a single element. The other can encapsulate many elements.

<button :if={@show_button}>
  Button and everything inside hidden if falsy.
</button>

<div>
  Never hidden
</div>

Versus

{#if @show_button}
  <button>
    Hidden if falsy
  </button>

  <div>
    Also hidden if falsy
  </div>
{/if}
1 Like

I know this part. What I wanted to ask was is there anything :if can do but {#if} cannot?

1 Like

To answer my own question in part, it seems like :if={...} is more efficient. I converted a bunch of :if={...} to {#if...} and observed that the DOM diff down the socket is bigger. I cannot pin point the problem but it feel like the change tracking is not ideal in the cases of if...elseif or cascading if

My unsolicited 5¢. IMO YMMV :slight_smile:

I find that “control attributes” (such as :if, :for and similar) are a net negative on DX (not a large negative, but a negative nonetheless):

  1. My own terminology explainer. Control attributes vs shorthand attributes

    Control attributes, well, control the behaviour of an element. So, :if controls whether an element is inserted depending on some condition.

    Shorthand attributes are, well, shorthands for often-used things. Like :on-click that generates phx-click and phx-target in one go.

  2. Props are passed down to the element, control attributes “work outside of the element”

    With possibly with the exception of class and style, attributes/props are what is passed down to the element, and that let the element control what to do with them. This is especially true if we don’t draw a distinction between regular HTML elements and our custom elements (React-style).

    Control attributes kinda break the abstraction. “Look, we pass all this onto the element except this one, this one will be handled by something else”.

  3. They require extra work to compose/refactor

    The moment you need an else on an :if, or you need to control more than one element, you have to remove the attribute and use something else. This is less of an issue for an established codebase, but will definitely be a minor pain when prototyping or redesigning.

    This also reminds me of how awkward if/else is, for example, in Angular: Angular

  4. Optimised for a special case

    Bullet point 2 stems from the fact that control attributes are optimised for a very special case: a single element that we can control.

    In my opinion, this is not a good way to optimise. There are way more special cases that we can come up with syntax for, and each special case will be sufficiently different from others that you will need a cheatsheet to remember them all :slight_smile:

    Even now, what is the actual difference between these three? (the question is a bit toungue-in-cheek :stuck_out_tongue: ):

      <div :if={ @show_div }>
      <div :show={ @show_div }>
      <div class="{{ "show": @show }}">
    
  5. An extension of 1 and 3: they break assumptions because they look like shorthand props

    By way of example: :on-click is a shorthand for phx-click phx-target. But :if is not a shorthand. But :show is kinda a shorthand, but it’s a shorthand for something that’s library-defined (is it display: none?)?

In the end, all these may be quite minor gripes, and not matter to most people. But those were my 5¢.

3 Likes

Hi @dmitriid! Thanks for the valuable 5¢.

I agree. That’s why we’re deprecating :for and discussing if we should do the same with :show.

As for :if, I’m not convinced yet. It seems it’s an exception that brings value as I haven’t found yet a Surface user that doesn’t like it (if you’re user, you’re the first one :stuck_out_tongue_winking_eye: that came out). The reason is probably because it’s very handy and looks quite familiar as most of other solutions also provide it. So it’s a tough call and I’ll consider users’ feedback a lot for this one before considering removing it.

For now, there’s no plan to deprecate it based on that feedback. You can consider :if as just a shorthand for {#if}...{/if} when applied to a single node instead of a real flow-control construct. If you need latter, just go with {#if}/{#elseif}/{#else}

3 Likes

Hi @msaraiva, is it possible to use Surface without LiveView? I will move parts of my app to LiveView later, but the component system looks great and I’d love to use it with regular views if possible. I think I saw some discussion about this somewhere, but cannot find it now.