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.