Surface - A component-based library for Phoenix LiveView

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 GitHub - surface-ui/surface: A server-side rendering component library for Phoenix

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.

35 Likes