Surface - A component-based library for Phoenix LiveView

Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative way to express and use components in Phoenix. Some of the main features include:

  • Components as modules - they can be stateless, stateful, data-only or compile-time
  • Declarative properties - explicitly declare the inputs (properties) of each component (like React)
  • An HTML-centric templating language with built-in directives (:for, :if, …) and syntactic sugar for attributes (inspired by Vue.js)
  • Contexts - allows parent components to share data with its children without passing them as properties
  • Compile-time checking of components and their properties
  • Integration with editor/tools for warnings, syntax highlighting, jump-to-definition, auto-completion and more

A work-in-progress live demo with more details is available at surface-demo.msaraiva.io.

Installation instructions and other useful information can be found at github.com/msaraiva/surface.

And finally, a VS Code extension that adds support for syntax highlighting is also available at
marketplace.visualstudio.com.

There’s still a lot of work to do and I hope some of you out there might be willing to help me in this journey. Bear in mind that the LiveView Component API has not reached a stable version yet and it’s currently under development, so although we try to keep track of the latest changes as much as possible, there might be temporary incompatibilities between Surface and Phoenix LiveView until a final version is released.

I’d like to thank @chrismccord and the Phoenix Core Team for buying the idea of reusable components and bringing the necessary basic concepts to the core API.

I also want to thank @josevalim, not only for the many valuable insights during development but also for his inestimable help with the main parser.

Happy coding!

-marlus

63 Likes

How does it look like?

15 Likes

Nice work Marlus! Many gems in there. It sounds silly, but the biggest immediate win for me is likely the css-twiddling - visual indicators of state changes make for lots of messy code currently.

Quick question - is it possible to have a Surface Component embedded in a “traditional” LiveView?

1 Like

Nice! I wondered how you did it when I saw your preview in Twitter!

That’s awesome! Thank you very much Marlus for this great library and great documentation.

the biggest immediate win for me is likely the css-twiddling - visual indicators of state changes make for lots of messy code currently.

Indeed. This was one of the first things I implemented. It’s quite a simple concept but it turned out to be extremely useful :slight_smile:

is it possible to have a Surface Component embedded in a “traditional” LiveView?

Definitely! After translation, which is done at compile-time, the code becomes just an ordinary phoenix template, not different from any code written using the built-in ~L. Just make sure you import Surface so you have ~H available or add it to your lib/my_app_web.ex. Like:

...

  def view do
    quote do
      ...
      import Surface
    end
  end

This way you’ll have it available in any view!

2 Likes

That’s awesome!! I think React and other JS frameworks have very nice mental models (despite the problems that surround actually using these tools in practice).

I’m glad Phoenix (& ecosystem) is a late mover in this space and that there is will to learn and borrow from these mental models. Much love Marlus :heart:

6 Likes

In case you’re giving Surface a try, please update it to the latest version on master. Updates include:

  • A new parser that provides much more accurate messages on parsing/translation errors (thanks to José Valim)
  • Handle void HTML elements like <br> and <hr> without need to self close the tag
  • Append property info to components’ documentation
  • More bug fixes

Example - showing component docs on hover with appended property info:

image

11 Likes

have you thought about using something like Liquid templates for your syntax?

Call me stupid but can someone explain the value in having such an abstraction in practice?

In the Grid example it looks like you could have put the <table> inside of a template in the render function and it would work the same and be more similar to how templates work with EEx.

What wins are there for setting things up like this instead?

1 Like

Wouldn’t it enable us to build component libraries with thought out APIs and standalone tests? Similar to what material-ui does on the React world. That would be a big win in my book :relieved:

Call me stupid but can someone explain the value in having such an abstraction in practice?

I think there’s no need to call you stupid. It’s a valid question. However, I believe that either you didn’t have the opportunity to read the “Getting started” guide at surface-demo.msaraiva.io or, most likely, I did a terrible job trying to explain some of the benefits there. So, please, let me try again.

In the Grid example it looks like you could have put the <table> inside of a template in the render function

Of course I could, but then I’d have missed the whole point of demonstrating how to use children as data. The Grid example was extracted from the section called “Children as data” of the “Getting started” guide mentioned above. That guide is not meant to be a tutorial on how you should design your application, it’s just a guide with simple examples that demonstrates the main features. The way you use those features is up to you.

and it would work the same

Well, it’s an abstraction on top of EEx, so it expected to work the same. Actually, by the end of the day, anything you can do with Surface can be done with EEx, just like anything you can do with React/JSX can also be done with pure Javascript. It’s hard to know when to stop adding abstractions on top of existing ones. There is always a tradeoff.

and be more similar to how templates work with EEx.

I have no intention whatsoever to keep Surface similar to EEx. On the contrary, I don’t want any dependency between Surface’s and EEx’s syntax. Each one of them tries to solve different problems. The main issue with EEx is that it makes no distinction between plain text and HTML (or any other structured format). Everything is treated as text. When using EEx, all you end up with is a big unstructured list of lists of chunks of text, consequently, a lot of useful information that we could use in our favour to boost productivity is lost. Here are some, IMO, clear benefits of keeping that information:

Normalized syntax for HTML elements and components

Let’s take a look at how HTML elements and Phoenix components are defined in EEx:

  <input style="padding: 1px">
  <%= live_component(@socket, Input, style: "padding: 1px") %>

The syntax is completely different. One is declarative and clean, the other is a function call inside a weird <%= ... %>.

Now let’s take a look at how HTML elements and Surface components are defined:

  <input style="padding: 1px">
  <Input style="padding: 1px">

Now both definitions are declarative, clean and use the same syntax. That makes the reading experience much more pleasant. I can also read it much faster, not only because there’s less noise but also because I don’t have to keep switching contexts (HTML <-> EEx) all the time. This is only possible because we know that <Input> is a component. We didn’t’ lose that information so we can generate the necessary code to initialize it.

Syntactic sugar for attributes/properties

There’s an example of this feature in the “Getting Started” guide. I’ll just write a shorter version here to save us some time.

Imagine you want to create a button component that sets CSS classes based on the following
rules:

  • button - always set
  • is-loading - set if @loading is truthy

so assuminng @loading is true, the following code should be generated:

<button class="button is-loading">

if it’s false:

<button class="button">

Using Surface we can achieve what we want by just:

<button class={{ "button", isLoading: @loading }}>

Do you see how clean that code looks without any conditional or ugly string concatenation?

Now you can argue that we could achieve the same result with EEx by creating a function. Again, of course you can. After all, that’s exactly how it’s implemented under the hood.

We could also extend this very same concept to boolean attributes like disabled or readonly and create another function so we can handle those too. Something like:

<button class=<%= css_class(["button", isLoading: @loading]) %> <%= boolean_attr(:disabled, @disabled) %>>

Well, that works for sure. But I must confess that it makes my eyes bleed. Could Surface do any better? Let’s see:

<button class={{ "button", isLoading: @loading }} disabled={{ @disabled }}>

Doesn’t it look better? I truly believe it does.

Static checking

  • Syntax checking - Since EEx doesn’t care about the structure of your code, any invalid HTML will only raise errors at runtime. When using Surface, most checks are done at compile-time.

  • Validation of properties and children - you can restrict what kind of properties and children a component accepts.

  • Other examples of static checking are available at https://github.com/msaraiva/surface#static-checking

Grouping and traversing children

A parent component can classify its children in different logical groups and later traverse them and make decisions based on the information retrieved. They are not just dumb unstructured chunks of text. The concept of parent and child is not lost.

Tooling

  • Syntax highlighting - Since EEx allows you to create incomplete/invalid HTML code, it might get tricky to make syntax highlighting work properly when mixing HTML with EEx/Elixir code. Code written in Surface, on the other hand, is structured, predictable and validated at compile-time. It took me just a couple of hours to create a VS Code extension for it.

  • Auto-complete - Since information about components, properties and data (state assigns) are always available for introspection, it was trivial to add this feature to ElxirSense.

  • Documentation, Go-to-Definition and … a bunch of other related stuff around tooling.

Example of auto-complete/suggestions of assigns:

Example of auto-complete/suggestions of properties and directives:

I could keep going on and on, showing more benefits of the proposed abstraction and its positive impact on productivity and maintainability. I could also try to introduce some of the planned features like scoped styles or slots, but I’m afraid that, if I haven’t convinced you yet, keep trying is not going to make any difference.

BTW, it’s totally fine if you don’t see any value in the solution. As I mentioned before, choosing the write abstraction is hard and will depend heavily on the requirements of the project in hand. I don’t’ have any expectation that Surface or even LiveView will always be a good choice for all kinds of projects. There’s still a long way to go, lots of ideas to be validated and certainly many mistakes to be made. The one thing I believe is that keeping an open mind, trying to explore new ideas to improve existing solutions will always be beneficial to any ecosystem.

Cheers.

16 Likes

Looking forward to those component examples on the demo page. I’ve got a bunch of reusable Tailwind+Liveview components in the backlog for a couple projects.

These questions I’m about to ask are for curiosity’s sake btw. Only because what you wrote sounds kind of interesting. At least to get a better understanding of how your tool works.

I didn’t read it, I only checked what was posted in this thread. But after having read that page, the “why” aspect isn’t covered. The motivation part of the docs you linked mentioned “Provide a more declarative way to express and use components in Phoenix”. But all I thought after that was “why?”.

This is coming at it from not ever using React in the past. I understand that React components exist, but I don’t have experience using and leveraging them in the real world. Personally I never found any front-end abstraction to be 100% re-usable (prior to React) and often just tackled front-end development with an approach of “ok, this is a new project, so I’m going to likely use Bootstrap and modify some bits and pieces for this specific project so that I have full 100% control over every element”.

For folks who never touched React, they don’t know the benefits of doing things this way.

I see. Personally I find that distinction to be a perk. It’s letting me know “hey, this snippet of code is going to be evaluated” and then I can trace code back to what live_component does. Where as my first instinct with <Input ...> is that it looks like maybe a typo and should be in lowercase and should probably be refactored to move the in-line style out of the HTML style attribute.

Do you have any large scale projects where you can gist one of your surface templates to see how it looks beyond a 2 line example? So we can see the skimmability of it.

Does Surface ultimately get transformed into EEx at compile time? In other words, is there zero performance implications of using it at run time?

And I’m guessing LiveComponent is dealing with EEx templates in the end? I haven’t looked into its API yet or details. But I like the idea of having compiled time syntax checks.

Is there a Vim plugin in the works? :slight_smile:

What types of projects would you use and not use Surface on?

2 Likes

Wow, this looks great. will take it for a spin! :smiley:

Can I use the components in .eex file?

Hi Zack!

My plan for the “Components” section is to list different reusable components suites written using different technologies like Bootstrap, Bulma, Tailwind, etc. Each one of them as a separate project. Since I’m still working on the core API, I didn’t have time to create any full set of components yet. In case you already have a few working examples and want to convert them to Surface, please let me know, I’ll be glad to update the page to list them. Also, feel free to contact me if need any assistance while converting the components and keep in mind that since it’s this is a work-in-progress, we might still have some changes in the API until we reach the first stable version.

4 Likes

Like everything else in this project, the documentation is still a work-in-progress. I’ll try to write more about “why?”. Thanks for the feedback.

Ok, first. I find hard to believe you would think it was a typo since the syntax highlighter uses different colours for HTML tags and components:

Secondly. Even you if don’t have syntax highlighting, you’ll get a nice warning telling you that there’s no Input component:

Cool, isn’t it?

Lastly and most importantly, this is just an example made to demonstrate the benefits of a unified syntax. In real life you don’t have to create components with the same name of existing tags.

You certainly have a problem understanding the concept of an “example”. Forget about the inline style. Again, the point was to demonstrate the syntax, not to show you the best practices of using CSS styles. Please, focus on the subject of the discussion, not on secondary pointless aspects.

No. The project is still an experimental unfinished work-in-progress. There’s no way anyone could possibly have a large scale project using it. Not even me.

Yes. Everything is translated at compile-time. There shouldn’t be any perfomance issue at runtime.

For syntax highlighting, I’m not aware of anyone working on it. I have no idea how easy it would be to convert the one I created for VS Code. The code is available at https://github.com/msaraiva/vscode-surface. Feel free to give it a try if you want. Regarding other features like auto-complete and friends, if your editor uses a plugin on top of ElixirSense, you’ll get all the benefits as soon as the maintainer updates to the new version, which by the way should be out in a couple of days.

Roughly, I think that any project that is suitable for LiveView would also be suitable for Surface. But as I said, the project is still experimental. It’s going to take some time to collect feedback to see if there will be any limitation.

4 Likes

No. EEx files will not get translated, however, you can wrap any component inside a separate function and call it normally in any EEx template. If your intention is to use Surface keeping the templates as external files, I’m working already on this feature. The plan is to allow components to load external templates with the .sface extension.

2 Likes

Not really. It would be really hard to keep the exact same syntax since we need to keep compatibility with Elixir code inside interpolation, e.g. the | operator is already taken by Elixir. Having said that, we could try to find an alternative way for the syntax to achieve the same goal. Is there any specific feature that you think it would be just awesome to have in Surface?

Just for fun, I created a demo project with some simple Surface components using Bootstrap.

Check out the demo: https://surface-bootstrap-demo.herokuapp.com/

And check out the implementation of the components here: https://github.com/joerichsen/surface_bootstrap_demo/tree/master/lib/surface_demo_web/components

It’s pretty simple for these (admittedly) simple components :slight_smile:

When I get the time, I want to tackle some more complex components like for example forms and modals.

7 Likes