Phoenix.Component (Phoenix LiveView v0.17.0) View Source

API for function components.

A function component is any function that receives an assigns map as argument and returns a rendered struct built with the ~H sigil.

Here is an example:

defmodule MyComponent do
  use Phoenix.Component

  # Optionally also bring the HTML helpers
  # use Phoenix.HTML

  def greet(assigns) do
    ~H"""
    <p>Hello, <%= assigns.name %></p>
    """
  end
end

The component can be invoked as a regular function:

MyComponent.greet(%{name: "Jane"})

But it is typically invoked using the function component syntax from the ~H sigil:

~H"""
<MyComponent.greet name="Jane" />
"""

If the MyComponent module is imported or if the function is defined locally, you can skip the module name:

~H"""
<.greet name="Jane" />
"""

Similar to any HTML tag inside the ~H sigil, you can interpolate attributes values too:

~H"""
<.greet name={@user.name} />
"""

You can learn more about the ~H sigil in its documentation.

use Phoenix.Component

Modules that define function components should call use Phoenix.Component at the top. Doing so will import the functions from both Phoenix.LiveView and Phoenix.LiveView.Helpers modules. Phoenix.LiveView and Phoenix.LiveComponent automatically invoke use Phoenix.Component for you.

You must avoid defining a module for each component. Instead, we should use modules to group side-by-side related function components.

Assigns

While inside a function component, you must use Phoenix.LiveView.assign/3 and Phoenix.LiveView.assign_new/3 to manipulate assigns, so that LiveView can track changes to the assigns values. For example, let's imagine a component that receives the first name and last name and must compute the name assign. One option would be:

def show_name(assigns) do
  assigns = assign(assigns, :name, assigns.first_name <> assigns.last_name)

  ~H"""
  <p>Your name is: <%= @name %></p>
  """
end

However, when possible, it may be cleaner to break the logic over function calls instead of precomputed assigns:

def show_name(assigns) do
  ~H"""
  <p>Your name is: <%= full_name(@first_name, @last_name) %></p>
  """
end

defp full_name(first_name, last_name), do: first_name <> last_name

Another example is making an assign optional by providing a default value:

def field_label(assigns) do
  assigns = assign_new(assigns, :help, fn -> nil end)

  ~H"""
  <label>
    <%= @text %>

    <%= if @help do %>
      <span class="help"><%= @help %></span>
    <% end %>
  </label>
  """
end

Slots

Slots is a mechanism to give HTML blocks to function components as in regular HTML tags.

Default slots

Any content you pass inside a component is assigned to a default slot called @inner_block. For example, imagine you want to create a button component like this:

<.button>
  This renders <strong>inside</strong> the button!
</.button>

It is quite simple to do so. Simply define your component and call render_slot(@inner_block) where you want to inject the content:

def button(assigns) do
  ~H"""
  <button class="btn">
    <%= render_slot(@inner_block) %>
  </button>
  """
end

In a nutshell, the contents given to the component is assigned to the @inner_block assign and then we use Phoenix.LiveView.Helpers.render_slot/2 to render it.

You can even have the component give a value back to the caller, by using let. Imagine this component:

def unordered_list(assigns) do
  ~H"""
  <ul>
    <%= for entry <- @entries do %>
      <li><%= render_block(@inner_block, entry) %></li>
    <% end %>
  </ul>
  """
end

And now you can invoke it as:

<.unordered_list let={entry} entries={~w(apple banana cherry)}>
  I like <%= entry %>
</.unordered_list>

You can also pattern match the arguments provided to the render block. Let's make our unordered_list component fancier:

def unordered_list(assigns) do
  ~H"\""
  <ul>
    <%= for entry <- @entries do %>
      <li><%= render_block(@inner_block, %{entry: entry, gif_url: random_gif()} %></li>
    <% end %>
  </ul>
  "\""
end

And now we can invoke it like this:

<.unordered_list let={%{entry: entry, gif_url: url}}>
  I like <%= entry %>. <img src={url} />
</.unordered_list>

Named slots

Besides @inner_block, it is also possible to pass named slots to the component. For example, imagine that you want to create a modal component. The modal component has a header, a footer, and the body of the modal, which we would use like this:

<.modal>
  <:header>
    This is the top of the modal.
  </:header>

  This is the body - everything not in a
  named slot goes to @inner_block.

  <:footer>
    <button>Save</button>
  </:footer>
</.modal>

The component itself could be implemented like this:

def modal(assigns) do
  ~H"""
  <div class="modal">
    <div class="modal-header">
      <%= render_slot(@header) %>
    </div>

    <div class="modal-body">
      <%= render_slot(@inner_block) %>
    </div>

    <div class="modal-footer">
      <%= render_slot(@footer) %>
    </div>
  </div>
  """
end

If you want to make the @header and @footer optional, you can assign them a default of an empty list at the top:

def modal(assigns) do
  assigns =
    assigns
    |> assign_new(:header, fn -> [] end)
    |> assign_new(:footer, fn -> [] end)

  ~H"""
  <div class="modal">
    ...
end

Named slots with attributes

It is also possible to pass the same named slot multiple times and also give attributes to each of them.

If multiple slot entries are defined for the same slot, render_slot/2 will automatically render all entries, merging their contents. But sometimes we want more fine grained control over each individual slot, including access to their attributes. Let's see an example. Imagine we want to implement a table component

For example, imagine a table component:

<.table rows={@users}>
  <:col let={user} label="Name">
    <%= user.name %>
  </:col>

  <:col let={user} label="Address">
    <%= user.address %>
  </:col>
</.table>

At the top level, we pass the rows as an assign and we define a :col slot for each column we want in the table. Each column also has a label, which we are going to use in the table header.

Inside the component, you can render the table with headers, rows, and columns:

def table(assigns) do
  ~H"""
  <table>
    <th>
      <%= for col <- @col do %>
        <td><%= col.label %></td>
      <% end >
    </th>
    <%= for row <- @rows do %>
      <tr>
        <%= for col <- @col do %>
          <td><%= render_slot(col, row) %></td>
        <% end %>
      </tr>
    <% end %>
  </table>
  """
end

Each named slot (including the @inner_block) is a list of maps, where the map contains all slot attributes, allowing us to access the label as col.label. This gives us complete control over how we render them.