Handle LiveView Streams empty state with Tailwind CSS
Currently, in September 2023, LiveView does not offer an easy way to check if the @streams
does not contain elements. The information is essential if we want a different UI for our empty state. After some research, I saw a post from “codeanpeace” on the Elixir Forum proposing using some Tailwind helpers. I decided to give it a try, and it works pretty great.
The final solution will be like this:
Now, let’s dive into the code. Our list is very straightforward. We added a boards
key with the list of “boards” to our stream and will use this later in the html.
defmodule ReflectiveLoopWeb.BoardLive.Index do
use ReflectiveLoopWeb, :live_view
alias ReflectiveLoop.Retro
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :boards, Retro.list_boards())}
end
# ...
end
I use a custom component to display a list that applies my desired style. I’ve omitted the non-essential parts. The critical factor is the render_slot(@new_item_action)
. Notice that it’s inside the div
that will have the items from the @streams
. The stacked_list component doesn’t know about the style of the slot, so we have the freedom to have lists with or without a custom empty state.
def stacked_list(assigns) do
~H"""
<div id={@id}>
<div phx-update="stream" id="board-list">
<%= render_slot(@new_item_action) %>
<div :for={{dom_id, line} <- @lines} id={dom_id} role="list">
<div>
<p>
<a href="#">
<%= render_slot(@title, line) %>
</a>
</p>
</div>
</div>
</div>
</div>
"""
end
Now, for the html part, we will use some pseudo-classes to change the style of the new item button, considering if the list is empty or not. I opted to use a single link with some extra css applied when it is the only element in the list.
Let’s check how the component works. Again, I’ve omitted the non-essential parts. As you can see, it’s straightforward. What makes the “magic” is the only
pseudo-class that I’ve applied in the link.
<.stacked_list
id="boards"
lines={@streams.boards}
>
<:title :let={board} label="name">
<%= board.name %>
</:title>
<:new_item_action>
<.link
id="new-action-button"
patch={~p"/boards/new"}
class="only:block only:relative only:w-full only:rounded-lg only:bg-white only:border-2 only:border-dashed only:border-gray-300 only:p-12 only:text-center"
>
<.icon name="hero-inbox-stack-solid" class="text-gray-900" />
<span class="pl-1 text-sm font-semibold text-gray-900">
Create a new board
</span>
</.link>
</:new_item_action>
</.stacked_list>
The structure needs to have a specific format. We have a div with two children, and we use Tailwind to display extra css when the link is the only child.
<div phx-update="stream">
<a></a>
<div></div>
</div>
But there is an a
and a div
, so we always have two children, no? Actually, no. The tag is not rendered when no items are in the @streams
. In the empty state, the HTML will be a single child tag.
<div>
<a></a>
</div>
I have never considered using CSS to handle states, but this works well with streams.
If you don’t use Tailwind, style your div with :only-child
, and you have the same result.