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>
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 %>
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}>
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>
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>
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>
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
We can call on it like this:
<ul>
<.list_item :for={book <- @books} :if={book.available} />
</ul>
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
We're on track to render our unordered list component like this:
<.unordered_list>
# ...
</unordered_list>
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
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>
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>
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
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)