DEV Community

Cover image for Phoenix LiveView 0.18: New Special HTML Attributes
Sophie DeBenedetto for AppSignal

Posted on • Originally published at blog.appsignal.com

Phoenix LiveView 0.18: New Special HTML Attributes

Phoenix LiveView 0.18 just shipped, with lots of new goodies to make developing LiveView an even better experience.

In this post, I'll take you through a lesser-known new feature - LiveView's new special HTML attributes - and show you how to write cleaner HTML with :if, :for, and :let.

When we're done, you'll have an eloquent, ergonomic, and dynamic function component you can use to render a list anywhere in your LiveView app.

Let's dive in!

What are Special HTML Attributes in LiveView 0.18?

LiveView special HTML attributes are inspired by a Surface feature called "directives". Directives are built-in attributes that modify the translated code of a tag or component at compile time.

Directives are just one of a few new features, like component slots and declarative assigns, that LiveView has drawn from Surface. Surface is LiveView's independently developed, open-source component library, and it has often driven LiveView framework development forward by introducing (and battle-testing) new concepts quickly.

The :if and :for special attributes provide syntactic sugar for the <%= if ... do %> and <%= for ... do %> EEx calls you would normally expect to write in your templates.

The :let attribute works a little bit differently. It is used in component slots when you want to yield a variable from within the function component back up to the caller. You'll see this most often when you use the .form/1 function component, but you can also use it to make your own function components even more dynamic.

Let's take a look at how you can use these attributes to write eloquent, ergonomic HTML in your LiveView templates. Along the way, you'll combine the usage of all three of these attributes to build a clean and dynamic LiveView function component.

Cleaner LiveView Templates with :if and :for

You can use these two attributes in regular Phoenix templates, in components, and in slots to write cleaner HTML code. We'll take a look at an example using the :if directive first:

<div class="alert alert-danger" :if={@error_message}>
  <p><%= @error_message %></p>
</div>
Enter fullscreen mode Exit fullscreen mode

The div with the alert alert-danger class that contains the error message markup will only render if the @error_message is present and evaluates to true.

This approach replaces the more verbose one here:

<%= if @error_message do %>
<div class="alert alert-danger">
  <p><%= @error_message %></p>
</div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

The code that uses the :if attribute is easier to read--more eloquent--and easier to write--more ergonomic.

You can also use the :if attribute to conditionally render a function component, all in one simple line of code. Let's say we have a function component, .error/1, that wraps up the error message markup above. We can conditionally render it like this:

<.error message={@error_message} :if={@error_message}>
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at an example that uses the :for attribute. The :for attribute can be used to iteratively render some content for each member in a collection, like this:

<tbody id="books">
  <tr :for={book <- @books}>
    <td><%= book.title %></td>
  </tr>
</tbody>
Enter fullscreen mode Exit fullscreen mode

Thanks to the :for attribute, you don't have to establish your for loop explicitly within EEx tags, like this:

<tbody id="books">
  <%= for book <- @books %>
  <tr>
    <td><%= book.title %></td>
  </tr>
  <% end %>
</tbody>
Enter fullscreen mode Exit fullscreen mode

Once again, we're left with code that is both more eloquent and more ergonomic. You can even combine the usage of :if and :for if you need to. Let's say we have a list of books to render, but we only want to display a given book if it is available in our online store. We can achieve that like this:

<ul>
  <li :for={book <- @books} :if={book.available}><%= book.title %></li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Since both :if and :for can be used in either plain HTML or in LiveView function components, we can wrap our list item HTML markup in a function component. Given the following function component:

def list_item(assigns) do
  ~H"""
  <li>
    <%= @book.title %>
  </li>
  """
end
Enter fullscreen mode Exit fullscreen mode

We can call on it like this:

<ul>
  <.list_item :for={book <- @books} :if={book.available} />
</ul>
Enter fullscreen mode Exit fullscreen mode

This is a nice way to start compartmentalizing our markup into reusable function components made even cleaner with special HTML attributes.

We can do better with our function component, though. Right now, we've only encapsulated the markup for list items, not the entire list. On top of that, our .list_item/1 function component isn't very reusable--it expects to be called with an assigns that contains an @book attribute set to something that responds to .title.

We'll build a more dynamic function component that uses component slots and the :let attribute to encapsulate the entire unordered list into one dynamic component. When we're done, you'll have a dynamic function component that uses :for and :let to render any collection, anywhere in your LiveView app.

For this next example, we'll simplify our logic a bit by dropping the :if attribute and the requirement that a book should only render if it's available. We'll just focus on using :let and :for. Let's get started.

Dynamic Function Components with :let

First up, let's use a named component slot to create a function component that renders an unordered list and its list items. Start by defining a top-level .unordered_list/1 function component that renders a named slot, :list_item, inside the appropriate markup:

def unordered_list(assigns) do
  ~H"""
  <ul>
    <li>
      <%= render_slot(@list_item, ...) %>
    </li>
  </ul>
  """
end
Enter fullscreen mode Exit fullscreen mode

We're on track to render our unordered list component like this:

<.unordered_list>
  # ...
</unordered_list>
Enter fullscreen mode Exit fullscreen mode

When we call on the component, we'll set an assigns - items - equal to the list of books. Now, we can use :for in the function component HEEx to render a list item slot for each item in the @items assigns:

def unordered_list(assigns) do
  ~H"""
  <ul :for={item <- @items}>
    <li>
      <%= render_slot(@list_item, item) %>
    </li>
  </ul>
  """
end
Enter fullscreen mode Exit fullscreen mode

Putting it all together, we can call on our function component like this:

<.unordered_list items={@books}>
  <:list_item :let={book}>
    <%= book.title>
  </:list_item>
</.unordered_list>
Enter fullscreen mode Exit fullscreen mode

Here's where the :let special attribute comes in. In our function component, we are using :for to iterate over the list of books in the @items assignment. For each book in the list, bound to the item variable at each step in the iteration, we are calling render_slot/2 with a second argument of item.

The call to :let={book} when we call on our :list_item slot sets a variable - book - equal to the second argument passed into render_slot/2. In this way, you yield variables from your function components back to the caller with the :let attribute. So, where we call :list_item when rendering the function component, we can operate on the book variable to display its title.

By combining :for and :let, we end up with an eloquent, ergonomic, and highly dynamic function component that we can use again and again in our application to render any collection into an unordered list.

Before we wrap up, there's one limitation that I'd like to point out here. Bringing back our "only render a book title if the book is available" logic is a little tricky here. You might want to do something like this:

<.unordered_list items={@books}>
  <:list_item :let={book} :if={book.available}>
    <%= book.title>
  </:list_item>
</.unordered_list>
Enter fullscreen mode Exit fullscreen mode

This is not possible, however. The component slot call cannot combine :let with :if in that manner. We can't take advantage of the :if attribute here and instead have to write something like this:

def unordered_list(assigns) do
  ~H"""
  <ul :for={item <- @items}>
    <%= if item.available %>
      <li>
        <%= render_slot(@list_item, item) %>
      </li>
    <% end %>
  </ul>
  """
end
Enter fullscreen mode Exit fullscreen mode

The downside is that the component becomes aware of the need to call .available on the item, making it less reusable.

Wrap Up

LiveView is being adopted so quickly in the Elixir community (and beyond) partly because of its gentle learning curve and emphasis on developer happiness.

LiveView's new special HTML attributes make writing LiveView templates even easier and more fun. By borrowing ergonomic features from Surface, we end up with code that is painless to write (fewer onerous EEx tags!) and easy to understand and maintain.

Reach for these special HTML attributes when writing plain HEEx templates, LiveView HEEx templates, or function components. You'll end up with beautiful code.

Until next time, happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Top comments (0)