Components

Rendering lists with slots

Read time: 3 minutes

Imagine you are building an application that lists boxing terms. Your initial implementation looks a lot like the code below:

Mix.install([
  {:liveview_playground, "~> 0.1.5"}
])

defmodule PageLive do
  use LiveviewPlaygroundWeb, :live_view

  def mount(_params, _session, socket) do
    boxing_terms = [
      %{term: "Jab", definition: "A quick, straight punch thrown with the lead hand."},
      %{
        term: "Hook",
        definition:
          "A punch thrown in a circular motion targeting the side of the opponent's head or body."
      },
      %{
        term: "Cross",
        definition:
          "A powerful punch thrown with the rear hand across the body, traveling straight toward the opponent."
      }
    ]

    socket = assign(socket, boxing_terms: boxing_terms)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <dl class="max-w-xs mx-auto">
      <div class="grid grid-cols-1 gap-y-2">
        <div :for={item <- @boxing_terms} class="border-b border-gray-300">
          <dt class="text-lg font-semibold"><%= item.term %></dt>
          <dd class="text-gray-600"><%= item.definition %></dd>
        </div>
      </div>
    </dl>
    """
  end
end

LiveviewPlayground.start(scripts: ["https://cdn.tailwindcss.com"])

So far nothing you haven't seen. There's an assign to define the list of terms, a loop using the special :for attribute and each item is being rendered. However, due to the previous lessons, you might notice that you could simplify this code a little more by generating a component to hide all these classes and, at the same time, have greater reusability in your <dl>.

#Mixing slots and lists

So farour slots only have rendered a single element. Whether it was a title or a subtitle per <:slot_name> usage. Let's learn how to combine lists and slots. Create and run a file called rendering_slot_list.exs:

Mix.install([
  {:liveview_playground, "~> 0.1.5"}
])

defmodule CoreComponents do
  use LiveviewPlaygroundWeb, :html

  attr :terms, :list, required: true
  slot :dt, required: true
  slot :dd, required: true

  def dl(assigns) do
    ~H"""
    <dl class="max-w-xs mx-auto">
      <div class="grid grid-cols-1 gap-y-2">
        <div :for={item <- @terms} class="border-b border-gray-300">
          <dt class="text-lg font-semibold"><%= render_slot(@dt, item) %></dt>
          <dd class="text-gray-600"><%= render_slot(@dd, item) %></dd>
        </div>
      </div>
    </dl>
    """
  end
end

defmodule PageLive do
  use LiveviewPlaygroundWeb, :live_view
  import CoreComponents

  def mount(_params, _session, socket) do
    boxing_terms = [
      %{term: "Jab", definition: "A quick, straight punch thrown with the lead hand."},
      %{
        term: "Hook",
        definition:
          "A punch thrown in a circular motion targeting the side of the opponent's head or body."
      },
      %{
        term: "Cross",
        definition:
          "A powerful punch thrown with the rear hand across the body, traveling straight toward the opponent."
      }
    ]

    socket = assign(socket, boxing_terms: boxing_terms)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <.dl terms={@boxing_terms}>
      <:dt :let={item}><%= item.term %></:dt>
      <:dd :let={item}><%= item.definition %></:dd>
    </.dl>
    """
  end
end

LiveviewPlayground.start(scripts: ["https://cdn.tailwindcss.com"])

Once again using the idea of CoreComponents we created a component called <.dl> to make it clear that this is our version of the <dl> HTML tag. We also chose the name of our slots to mimick HTML's: <:dt> (description term) and <:dd> (description detail).

The component itself isn't much different from what you've seen before. We use a loop with :for. For each element we use the render_slot/2 function. The difference is that this time we passed a second argument to that function: the current item in the loop.

When a second argument is passed to render_slot/2 we can use the special attribute :let={var} at the slot definition to store the current looped element in var. That way we managed to simplify a component that works with loops and made our LiveView render/1 extremely clean.

#Recap!

  • You can simplify loops by creating components.
  • Slots can receive loop variables by passing them in the second argument of render_slot/2 and receiving them in the slot with :let={var_name}.
  • Using slots and components makes LiveViews code cleaner.

Feedback

Got any feedback about this page? Let us know!