defmodule Phoenix.Component do
@moduledoc ~S'''
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](`Phoenix.LiveView.Helpers.sigil_H/2`).
Here is an example:
defmodule MyComponent do
use Phoenix.Component
# Optionally also bring the HTML helpers
# use Phoenix.HTML
def greet(assigns) do
~H"""
Hello, <%= assigns.name %>
"""
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"""
"""
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](`Phoenix.LiveView.Helpers.sigil_H/2`).
## `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"""
Your name is: <%= @name %>
"""
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"""
Your name is: <%= full_name(@first_name, @last_name) %>
"""
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"""
"""
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 inside the 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"""
"""
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"""
<%= for entry <- @entries do %>
<%= render_slot(@inner_block, entry) %>
<% end %>
"""
end
And now you can invoke it as:
<.unordered_list let={entry} entries={~w(apple banana cherry)}>
I like <%= entry %>
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"""
"""
end
And now we can invoke it like this:
<.unordered_list let={%{entry: entry, gif_url: url}}>
I like <%= entry %>.
### 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.
This is the body - everything not in a
named slot goes to @inner_block.
<:footer>
The component itself could be implemented like this:
def modal(assigns) do
~H"""
<%= render_slot(@header) %>
<%= render_slot(@inner_block) %>
"""
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"""
...
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 let={user} label="Address">
<%= user.address %>
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"""
<%= for col <- @col do %>
<%= col.label %>
<% end %>
<%= for row <- @rows do %>
<%= for col <- @col do %>
<%= render_slot(col, row) %>
<% end %>
<% end %>
"""
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.
'''
@doc false
defmacro __using__(_) do
quote do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
end
end
end