Surface - A component-based library for Phoenix LiveView

What exactly are you struggling with right now? I’ve recently created https://github.com/LostKobrakai/molecule for allowing better composition of markup with plain .eex templates.

1 Like

Molecule looks nice, will definitely try.
But I’m still super jelly of Surface vscode integration :grin:

any plans for non-VS Code IDE integration? Vim, perhaps?

Hi @stevensonmt!

It depends on which kind of integration you’re talking about. For features like auto-completion/suggestions which is been implemented as an ElixirSense plugin, any editor using ElixirSense directly or indirectly via Elixir-LS, will automatically have that feature.

For things like syntax highlighting, it would be really hard for me to implement it for an editor that I don’t use. Unfortunately, I have to focus on the core features and only hope that someone with the proper knowledge starts using Surface and may be willing to do that task.

2 Likes

I’m not sure if I’ll have time for working on this soon, so I’ll leave this here for any intrepid souls out here who might want to take a pass at creating a syntax highlighting plugin for vim.

Guide for creating your own syntax highlighting plugin for Vim: https://thoughtbot.com/blog/writing-vim-syntax-plugins

For the keywords and regions necessary to properly highlight Surface related code, one should probably be able to extract those from the json files inside the vscode plugin.

This should be hard to do, just time consuming.

Hi folks!

Surface v0.1.0 has been released and it’s available on hex.

Many thanks to all contributors who made this release possible. There’s no way we’d get this far without your help.

Also many thanks to all Phoenix Core Team for creating LiveView and its amazing component API, which is the foundation that made this project come true.

Happy coding!
-marlus

20 Likes

@msaraiva

I am testing out some Surface components, and I noticed they are not adopting the “live” layout that all of my other liveview templates are using.

In web.ex I have set the layout for all LiveViews with
use Phoenix.LiveView, layout: {MyWebApp.LayoutView,"live.html"}

but it seems that Surface components are not following that path. I was able to set the layout of the surface component at the router level with
live("/surface", SurfaceComponentLive, layout: {MyAppWeb.LayoutView, :live})

but it would be great if I could follow the same convention as regular liveview components where I put the layout once in web.ex and forget about it.

Is there any to apply the same layout to all of my surface components?
Thanks!

Hi @mcgingras!

I assume you did this inside the live_view/0 function which, by default, is used to define regular live views, not Surface LVs. However, you can either update those definitions replacing use Phoenix.LiveView, ... with use Surface.LiveView, ... or define a separate function, like:

  def surface_live_view do
    quote do
      use Surface.LiveView,
        layout: {MyAppWeb.LayoutView, "live.html"}

      unquote(view_helpers())
    end
  end

And then use it whenever you need to define a LV:

use MyAppWeb, :surface_live_view
2 Likes

I’ve now spent since Sunday tinkering with Surface and this is just so great.
To reiterate my rant from freenode today:

  • Easy to explain to frontend devs, it feels and acts more or less like React + Redux, but then imagine Redux lives in your server instead of in the client
  • Very declarative and very easy to use to set up reactive sidebars, navbars, footers and whatever
  • Props design is very clean and easy to use and I love the vscode and compiler enforcement and warnings, feels almost as good as how Typescript holds your hand in React land
  • Combine it with Absinthe.run() in a Service or Context module to feed data to your components and your frontend devs who really hate elixir and dont wanna learn dumb functional hard toy languages (more or less actual quote :D) can jump straight in and make awesome stuff without having touch a single piece of javascript.

10/10 experience, would recommend!

(Will update with more replies when I try to do advanced stuff and headbutt Surface/LiveView, but for now, very happy!)

11 Likes

Hi @msaraiva!

Thanks for all the help so far. I’m really enjoying the Surface library.

I’m running into an issue trying to recursively call a surface component within itself. The use case that is motivating this example is rendering a tree like select field. The Component accepts a map of data that looks like…

%{name: "name1", children: [%{name: "child", children []}]}

I’ve created a component called TreeNode that displays some information about the current node, then tries to render child TreeNode's for each child of the current node.

Not the real code but something similar to…

<div>
 {{ @name }}
 <div :if={{ length(@children) > 0 }} :for={{ childNode <- @children }}>
      <TreeNode name={{ childNode["name"] }}  children={{ childNode["children"] }} />
</div>
</div>

The problem is, Surface does not seem to allow me to call the Component within itself, recursively. I am getting the error:

you are trying to use the module MyWeb.Components.TreeNode which is currently being defined.

This sort of thing works fine if you use regular liveview components, and I would love for it to work in Surface as well. Do you have any suggestions?

Thanks again for the help, I feel like I’ve been asking quite a few questions. But I really do love the library!

Hi @mcgingras!

Currently, Surface does not allow using a component recursively. However, you can easily make it work by moving the recursion to a separate function. Something like:

defmodule Tree do
  ...
    
  @doc "The root node"
  prop node, :map
  
  def render(assigns) do
    ~H"""
    {{ render_node(@node) }}
    """
  end

  defp render_node(node) do
    ~H"""
    <div>
      {{ node.name }}
      <div :for={{ child <- node.children }}>
        {{ render_node(child) }}
      </div>
    </div>
    """
  end
end
5 Likes

Thank you very much for the tip. This works great. The only thing to note – the compiler was unhappy about using a param name of node in the call to render_node/1. It was only happy to have the param as assigns

@msaraiva, suggestions on how to best to Gettext?

Ideally we would have something like this:

<GetText bindings={{ name: name }}>Hello %{name}</GetText>

But slot based approach doesnt match well with how we do macros and the plurality will be harder to handle, so maybe something like:

<GetText 
  bindings={{ name: name }}
  count={{ some_int }}
  text={{"Hello to you, my name is %{name}"}}
  plural={{"Hello to all of you %{count}, my name is %{name}"}}
>

Or maybe a combination?

<GetText 
  bindings={{ name: name }}
  count={{ some_int }}
  plural={{"Hello to all of you %{count}, my name is %{name}"}}
>Hello to you, my name is %{name}</GetText>

… or just stick to <%= ngettext ... ? :stuck_out_tongue:

Edit: Rawdogging ngettext doesnt work actually, as it wont work inside the H sigil.

Edit2: I realize that to get this to be compile-time, it has to be
{{ gettext("Some text.") }} directly in the template.

@msaraiva

Hi Marlus.

I’m wondering if the source code for the Surface docs site is published anywhere? I’m very curious to look at the form example in the contexts section in particular, for reference.

I’ve been refactoring Surface into a LiveView based project and so far it’s been an absolute joy, everything has been so easy so far and the templates are super readable.

Thanks!

2 Likes

Hi @darraghenright!

The website’s source code is now available at https://github.com/surface-ui/surface_site.

3 Likes

@msaraiva Thanks for your work on this! One question, is it somehow possible to use Surface components layouts, e.g. in live.html.leex?

2 Likes

Excellent! Thanks a million :slight_smile:

Just reading the docs and I think I love the “typed slottables” feature. The resulting template seems so much clearer to me than the template slot=... form.

1 Like

Speaking of templates, I am using AlpineJS to do something like the following:

<div x-data="{ showModal: false}">
  
  <button @click="showModal = true">show modal</button>

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

I’m not using x-show because I want to remove the content from the DOM (I don’t want to leave things like partially completed form elements in the DOM after the modal has closed).

I am getting the following error, and I’m not sure what it means — I haven’t found anything in the docs that explains it yet:

CaseClauseError
no case clause matching: {:error, "cannot render <div> (templates are only allowed as children elements of components, but found template for default)"}

Does anyone have any thoughts? Thanks!

EDIT: Still curious about this error but thinking about what I am trying to do, and it seems like it might be a better idea to control this sort of thing in the LV (which is what I was doing initially). Still trying to find where to draw that line between LV/Surface and Alpine.

I think Alpine’s <template> is conflicting with Surface’s <template> that is used for named slots.