```
You can also combine `:for` and `:if` for tags, components, and slot to act as a filter:
```heex
<.error :for={msg <- @errors} :if={msg != nil} message={msg} />
```
'''
defmacro sigil_H({:<<>>, meta, [expr]}, []) do
unless Macro.Env.has_var?(__CALLER__, {:assigns, nil}) do
raise "~H requires a variable named \"assigns\" to exist and be set to a map"
end
options = [
engine: Phoenix.LiveView.HTMLEngine,
file: __CALLER__.file,
line: __CALLER__.line + 1,
caller: __CALLER__,
indentation: meta[:indentation] || 0
]
EEx.compile_string(expr, options)
end
@doc ~S'''
Filters the assigns as a list of keywords for use in dynamic tag attributes.
Useful for transforming caller assigns into dynamic attributes while
stripping reserved keys from the result.
## Examples
Imagine the following `my_link` component which allows a caller
to pass a `new_window` assign, along with any other attributes they
would like to add to the element, such as class, data attributes, etc:
```heex
<.my_link href="/" id={@id} new_window={true} class="my-class">Home
```
We could support the dynamic attributes with the following component:
def my_link(assigns) do
target = if assigns[:new_window], do: "_blank", else: false
extra = assigns_to_attributes(assigns, [:new_window])
assigns =
assigns
|> assign(:target, target)
|> assign(:extra, extra)
~H"""
<%= render_slot(@inner_block) %>
"""
end
The above would result in the following rendered HTML:
```heex
Home
```
The second argument (optional) to `assigns_to_attributes` is a list of keys to
exclude. It typically includes reserved keys by the component itself, which either
do not belong in the markup, or are already handled explicitly by the component.
'''
def assigns_to_attributes(assigns, exclude \\ []) do
excluded_keys = @reserved_assigns ++ exclude
for {key, val} <- assigns, key not in excluded_keys, into: [], do: {key, val}
end
@doc """
Renders a LiveView within a template.
This is useful in two situations:
* When rendering a child LiveView inside a LiveView.
* When rendering a LiveView inside a regular (non-live) controller/view.
## Options
* `:session` - a map of binary keys with extra session data to be serialized and sent
to the client. All session data currently in the connection is automatically available
in LiveViews. You can use this option to provide extra data. Remember all session data is
serialized and sent to the client, so you should always keep the data in the session
to a minimum. For example, instead of storing a User struct, you should store the "user_id"
and load the User when the LiveView mounts.
* `:container` - an optional tuple for the HTML tag and DOM attributes to be used for the
LiveView container. For example: `{:li, style: "color: blue;"}`. By default it uses the module
definition container. See the "Containers" section below for more information.
* `:id` - both the DOM ID and the ID to uniquely identify a LiveView. An `:id` is
automatically generated when rendering root LiveViews but it is a required option when
rendering a child LiveView.
* `:sticky` - an optional flag to maintain the LiveView across live redirects, even if it is
nested within another LiveView. If you are rendering the sticky view within your live layout,
make sure that the sticky view itself does not use the same layout. You can do so by returning
`{:ok, socket, layout: false}` from mount.
## Examples
When rendering from a controller/view, you can call:
```heex
<%= live_render(@conn, MyApp.ThermostatLive) %>
```
Or:
```heex
<%= live_render(@conn, MyApp.ThermostatLive, session: %{"home_id" => @home.id}) %>
```
Within another LiveView, you must pass the `:id` option:
```heex
<%= live_render(@socket, MyApp.ThermostatLive, id: "thermostat") %>
```
## Containers
When a `LiveView` is rendered, its contents are wrapped in a container. By default,
the container is a `div` tag with a handful of `LiveView` specific attributes.
The container can be customized in different ways:
* You can change the default `container` on `use Phoenix.LiveView`:
use Phoenix.LiveView, container: {:tr, id: "foo-bar"}
* You can override the container tag and pass extra attributes when calling `live_render`
(as well as on your `live` call in your router):
live_render socket, MyLiveView, container: {:tr, class: "highlight"}
"""
def live_render(conn_or_socket, view, opts \\ [])
def live_render(%Plug.Conn{} = conn, view, opts) do
case Static.render(conn, view, opts) do
{:ok, content, _assigns} ->
content
{:stop, _} ->
raise RuntimeError, "cannot redirect from a child LiveView"
end
end
def live_render(%Socket{} = parent, view, opts) do
Static.nested_render(parent, view, opts)
end
@doc ~S'''
Renders a slot entry with the given optional `argument`.
```heex
<%= render_slot(@inner_block, @form) %>
```
If the slot has no entries, nil is returned.
If multiple slot entries are defined for the same slot,`render_slot/2` will automatically render
all entries, merging their contents. In case you want to use the entries' attributes, you need
to iterate over the list to access each slot individually.
For example, imagine a table component:
```heex
<.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
'''
defmacro render_slot(slot, argument \\ nil) do
quote do
unquote(__MODULE__).__render_slot__(
var!(changed, Phoenix.LiveView.Engine),
unquote(slot),
unquote(argument)
)
end
end
@doc false
def __render_slot__(_, [], _), do: nil
def __render_slot__(changed, [entry], argument) do
call_inner_block!(entry, changed, argument)
end
def __render_slot__(changed, entries, argument) when is_list(entries) do
assigns = %{entries: entries, changed: changed, argument: argument}
~H"""
<%= for entry <- @entries do %><%= call_inner_block!(entry, @changed, @argument) %><% end %>
"""
end
def __render_slot__(changed, entry, argument) when is_map(entry) do
entry.inner_block.(changed, argument)
end
defp call_inner_block!(entry, changed, argument) do
if !entry.inner_block do
message = "attempted to render slot <:#{entry.__slot__}> but the slot has no inner content"
raise RuntimeError, message
end
entry.inner_block.(changed, argument)
end
@doc """
Returns the flash message from the LiveView flash assign.
## Examples
```heex
<%= live_flash(@flash, :info) %>
<%= live_flash(@flash, :error) %>
```
"""
@doc deprecated: "Use Phoenix.Flash.get/2 in Phoenix v1.7+"
def live_flash(%_struct{} = other, _key) do
raise ArgumentError, "live_flash/2 expects a @flash assign, got: #{inspect(other)}"
end
def live_flash(%{} = flash, key), do: Map.get(flash, to_string(key))
@doc """
Returns the entry errors for an upload.
The following error may be returned:
* `:too_many_files` - The number of selected files exceeds the `:max_entries` constraint
## Examples
def error_to_string(:too_many_files), do: "You have selected too many files"
```heex
<%= for err <- upload_errors(@uploads.avatar) do %>
<%= error_to_string(err) %>
<% end %>
```
"""
def upload_errors(%Phoenix.LiveView.UploadConfig{} = conf) do
for {ref, error} <- conf.errors, ref == conf.ref, do: error
end
@doc """
Returns the entry errors for an upload.
The following errors may be returned:
* `:too_large` - The entry exceeds the `:max_file_size` constraint
* `:not_accepted` - The entry does not match the `:accept` MIME types
## Examples
def error_to_string(:too_large), do: "Too large"
def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
```heex
<%= for entry <- @uploads.avatar.entries do %>
<%= for err <- upload_errors(@uploads.avatar, entry) do %>
<%= error_to_string(err) %>
<% end %>
<% end %>
```
"""
def upload_errors(
%Phoenix.LiveView.UploadConfig{} = conf,
%Phoenix.LiveView.UploadEntry{} = entry
) do
for {ref, error} <- conf.errors, ref == entry.ref, do: error
end
@doc ~S'''
Assigns the given `key` with value from `fun` into `socket_or_assigns` if one does not yet exist.
The first argument is either a LiveView `socket` or an `assigns` map from function components.
This function is useful for lazily assigning values and referencing parent assigns.
We will cover both use cases next.
## Lazy assigns
Imagine you have a function component that accepts a color:
```heex
<.my_component color="red" />
```
The color is also optional, so you can skip it:
```heex
<.my_component />
```
In such cases, the implementation can use `assign_new` to lazily
assign a color if none is given. Let's make it so it picks a random one
when none is given:
def my_component(assigns) do
assigns = assign_new(assigns, :color, fn -> Enum.random(~w(red green blue)) end)
~H"""
Example
"""
end
## Referencing parent assigns
When a user first accesses an application using LiveView, the LiveView is first rendered in its
disconnected state, as part of a regular HTML response. In some cases, there may be data that is
shared by your Plug pipelines and your LiveView, such as the `:current_user` assign.
By using `assign_new` in the mount callback of your LiveView, you can instruct LiveView to
re-use any assigns set in your Plug pipelines as part of `Plug.Conn`, avoiding sending additional
queries to the database. Imagine you have a Plug that does:
# A plug
def authenticate(conn, _opts) do
if user_id = get_session(conn, :user_id) do
assign(conn, :current_user, Accounts.get_user!(user_id))
else
send_resp(conn, :forbidden)
end
end
You can re-use the `:current_user` assign in your LiveView during the initial render:
def mount(_params, %{"user_id" => user_id}, socket) do
{:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end
In such case `conn.assigns.current_user` will be used if present. If there is no such
`:current_user` assign or the LiveView was mounted as part of the live navigation, where no Plug
pipelines are invoked, then the anonymous function is invoked to execute the query instead.
LiveView is also able to share assigns via `assign_new` within nested LiveView. If the parent
LiveView defines a `:current_user` assign and the child LiveView also uses `assign_new/3` to
fetch the `:current_user` in its `mount/3` callback, as above, the assign will be fetched from
the parent LiveView, once again avoiding additional database queries.
Note that `fun` also provides access to the previously assigned values:
assigns =
assigns
|> assign_new(:foo, fn -> "foo" end)
|> assign_new(:bar, fn %{foo: foo} -> foo <> "bar" end)
'''
def assign_new(socket_or_assigns, key, fun)
def assign_new(%Socket{} = socket, key, fun) when is_function(fun, 1) do
validate_assign_key!(key)
case socket do
%{assigns: %{^key => _}} ->
socket
%{private: %{assign_new: {assigns, keys}}} ->
# It is important to store the keys even if they are not in assigns
# because maybe the controller doesn't have it but the view does.
socket = put_in(socket.private.assign_new, {assigns, [key | keys]})
Phoenix.LiveView.Utils.force_assign(
socket,
key,
case assigns do
%{^key => value} -> value
%{} -> fun.(socket.assigns)
end
)
%{assigns: assigns} ->
Phoenix.LiveView.Utils.force_assign(socket, key, fun.(assigns))
end
end
def assign_new(%Socket{} = socket, key, fun) when is_function(fun, 0) do
validate_assign_key!(key)
case socket do
%{assigns: %{^key => _}} ->
socket
%{private: %{assign_new: {assigns, keys}}} ->
# It is important to store the keys even if they are not in assigns
# because maybe the controller doesn't have it but the view does.
socket = put_in(socket.private.assign_new, {assigns, [key | keys]})
Phoenix.LiveView.Utils.force_assign(socket, key, Map.get_lazy(assigns, key, fun))
%{} ->
Phoenix.LiveView.Utils.force_assign(socket, key, fun.())
end
end
def assign_new(%{__changed__: changed} = assigns, key, fun) when is_function(fun, 1) do
case assigns do
%{^key => _} -> assigns
%{} -> Phoenix.LiveView.Utils.force_assign(assigns, changed, key, fun.(assigns))
end
end
def assign_new(%{__changed__: changed} = assigns, key, fun) when is_function(fun, 0) do
case assigns do
%{^key => _} -> assigns
%{} -> Phoenix.LiveView.Utils.force_assign(assigns, changed, key, fun.())
end
end
def assign_new(assigns, _key, fun) when is_function(fun, 0) or is_function(fun, 1) do
raise_bad_socket_or_assign!("assign_new/3", assigns)
end
defp raise_bad_socket_or_assign!(name, assigns) do
extra =
case assigns do
%_{} ->
""
%{} ->
"""
You passed an assigns map that does not have the relevant change tracking \
information. This typically means you are calling a function component by \
hand instead of using the HEEx template syntax. If you are using HEEx, make \
sure you are calling a component using:
<.component attribute={value} />
If you are outside of HEEx and you want to test a component, use \
Phoenix.LiveViewTest.render_component/2:
Phoenix.LiveViewTest.render_component(&component/1, attribute: "value")
"""
_ ->
""
end
raise ArgumentError,
"#{name} expects a socket from Phoenix.LiveView/Phoenix.LiveComponent " <>
" or an assigns map from Phoenix.Component as first argument, got: " <>
inspect(assigns) <> extra
end
@doc """
Adds a `key`-`value` pair to `socket_or_assigns`.
The first argument is either a LiveView `socket` or an `assigns` map from function components.
## Examples
iex> assign(socket, :name, "Elixir")
"""
def assign(socket_or_assigns, key, value)
def assign(%Socket{} = socket, key, value) do
validate_assign_key!(key)
Phoenix.LiveView.Utils.assign(socket, key, value)
end
def assign(%{__changed__: changed} = assigns, key, value) do
case assigns do
%{^key => ^value} ->
assigns
%{} ->
Phoenix.LiveView.Utils.force_assign(assigns, changed, key, value)
end
end
def assign(assigns, _key, _val) do
raise_bad_socket_or_assign!("assign/3", assigns)
end
@doc """
Adds key-value pairs to assigns.
The first argument is either a LiveView `socket` or an `assigns` map from function components.
A keyword list or a map of assigns must be given as argument to be merged into existing assigns.
## Examples
iex> assign(socket, name: "Elixir", logo: "💧")
iex> assign(socket, %{name: "Elixir"})
"""
def assign(socket_or_assigns, keyword_or_map)
when is_map(keyword_or_map) or is_list(keyword_or_map) do
Enum.reduce(keyword_or_map, socket_or_assigns, fn {key, value}, acc ->
assign(acc, key, value)
end)
end
defp validate_assign_key!(:flash) do
raise ArgumentError,
":flash is a reserved assign by LiveView and it cannot be set directly. " <>
"Use the appropriate flash functions instead."
end
defp validate_assign_key!(_key), do: :ok
@doc """
Updates an existing `key` with `fun` in the given `socket_or_assigns`.
The first argument is either a LiveView `socket` or an `assigns` map from function components.
The update function receives the current key's value and returns the updated value.
Raises if the key does not exist.
The update function may also be of arity 2, in which case it receives the current key's value
as the first argument and the current assigns as the second argument.
Raises if the key does not exist.
## Examples
iex> update(socket, :count, fn count -> count + 1 end)
iex> update(socket, :count, &(&1 + 1))
iex> update(socket, :max_users_this_session, fn current_max, %{users: users} ->
max(current_max, length(users))
end)
"""
def update(socket_or_assigns, key, fun)
def update(%Socket{assigns: assigns} = socket, key, fun) when is_function(fun, 2) do
update(socket, key, &fun.(&1, assigns))
end
def update(%Socket{assigns: assigns} = socket, key, fun) when is_function(fun, 1) do
case assigns do
%{^key => val} -> assign(socket, key, fun.(val))
%{} -> raise KeyError, key: key, term: assigns
end
end
def update(assigns, key, fun) when is_function(fun, 2) do
update(assigns, key, &fun.(&1, assigns))
end
def update(assigns, key, fun) when is_function(fun, 1) do
case assigns do
%{^key => val} -> assign(assigns, key, fun.(val))
%{} -> raise KeyError, key: key, term: assigns
end
end
def update(assigns, _key, fun) when is_function(fun, 1) or is_function(fun, 2) do
raise_bad_socket_or_assign!("update/3", assigns)
end
@doc """
Checks if the given key changed in `socket_or_assigns`.
The first argument is either a LiveView `socket` or an `assigns` map from function components.
## Examples
iex> changed?(socket, :count)
"""
def changed?(socket_or_assigns, key)
def changed?(%Socket{assigns: assigns}, key) do
Phoenix.LiveView.Utils.changed?(assigns, key)
end
def changed?(%{__changed__: _} = assigns, key) do
Phoenix.LiveView.Utils.changed?(assigns, key)
end
def changed?(assigns, _key) do
raise_bad_socket_or_assign!("changed?/2", assigns)
end
## Declarative assigns API
@doc false
defmacro __using__(opts \\ []) do
conditional =
if __CALLER__.module != Phoenix.LiveView.Helpers do
quote do: import(Phoenix.LiveView.Helpers)
end
imports =
quote bind_quoted: [opts: opts] do
import Kernel, except: [def: 2, defp: 2]
import Phoenix.Component
import Phoenix.Component.Declarative
for {prefix_match, value} <- Phoenix.Component.Declarative.__setup__(__MODULE__, opts) do
@doc false
def __global__?(unquote(prefix_match)), do: unquote(value)
end
end
[conditional, imports]
end
@doc ~S'''
Declares a function component slot.
## Arguments
* `name` - an atom defining the name of the slot. Note that slots cannot define the same name
as any other slots or attributes declared for the same component.
* `opts` - a keyword list of options. Defaults to `[]`.
* `block` - a code block containing calls to `attr/3`. Defaults to `nil`.
### Options
* `:required` - marks a slot as required. If a caller does not pass a value for a required slot,
a compilation warning is emitted. Otherwise, an omitted slot will default to `[]`.
* `:doc` - documentation for the slot. Any slot attributes declared
will have their documentation listed alongside the slot.
### Slot Attributes
A named slot may declare attributes by passing a block with calls to `attr/3`.
Unlike attributes, slot attributes cannot accept the `:default` option. Passing one
will result in a compile warning being issued.
### The Default Slot
The default slot can be declared by passing `:inner_block` as the `name` of the slot.
Note that the `:inner_block` slot declaration cannot accept a block. Passing one will
result in a compilation error.
## Compile-Time Validations
LiveView performs some validation of slots via the `:phoenix_live_view` compiler.
When slots are defined, LiveView will warn at compilation time on the caller if:
* A required slot of a component is missing.
* An unknown slot is given.
* An unknown slot attribute is given.
On the side of the function component itself, defining attributes provides the following
quality of life improvements:
* Slot documentation is generated for the component.
* Calls made to the component are tracked for reflection and validation purposes.
## Documentation Generation
Public function components that define slots will have their docs injected into the function's
documentation, depending on the value of the `@doc` module attribute:
* if `@doc` is a string, the slot docs are injected into that string. The optional placeholder
`[INSERT LVATTRDOCS]` can be used to specify where in the string the docs are injected.
Otherwise, the docs are appended to the end of the `@doc` string.
* if `@doc` is unspecified, the slot docs are used as the default `@doc` string.
* if `@doc` is `false`, the slot docs are omitted entirely.
The injected slot docs are formatted as a markdown list:
* `name` (required) - slot docs. Accepts attributes:
* `name` (`:type`) (required) - attr docs. Defaults to `:default`.
By default, all slots will have their docs injected into the function `@doc` string.
To hide a specific slot, you can set the value of `:doc` to `false`.
## Example
slot :header
slot :inner_block, required: true
slot :footer
def modal(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
As shown in the example above, `render_slot/1` returns `nil` when an optional slot is declared
and none is given. This can be used to attach default behaviour.
'''
defmacro slot(name, opts, block)
defmacro slot(name, opts, do: block) when is_atom(name) and is_list(opts) do
quote do
Phoenix.Component.Declarative.__slot__!(
__MODULE__,
unquote(name),
unquote(opts),
__ENV__.line,
__ENV__.file,
fn -> unquote(block) end
)
end
end
@doc """
Declares a slot. See `slot/3` for more information.
"""
defmacro slot(name, opts \\ []) when is_atom(name) and is_list(opts) do
{block, opts} = Keyword.pop(opts, :do, nil)
quote do
Phoenix.Component.Declarative.__slot__!(
__MODULE__,
unquote(name),
unquote(opts),
__ENV__.line,
__ENV__.file,
fn -> unquote(block) end
)
end
end
@doc ~S'''
Declares attributes for a HEEx function components.
## Arguments
* `name` - an atom defining the name of the attribute. Note that attributes cannot define the
same name as any other attributes or slots declared for the same component.
* `type` - an atom defining the type of the attribute.
* `opts` - a keyword list of options. Defaults to `[]`.
### Types
An attribute is declared by its name, type, and options. The following types are supported:
| Name | Description |
|-----------------|----------------------------------------------------------------------|
| `:any` | any term |
| `:string` | any binary string |
| `:atom` | any atom (including `true`, `false`, and `nil`) |
| `:boolean` | any boolean |
| `:integer` | any integer |
| `:float` | any float |
| `:list` | any list of any arbitrary types |
| `:global` | any common HTML attributes, plus those defined by `:global_prefixes` |
| A struct module | any module that defines a struct with `defstruct/1` |
### Options
* `:required` - marks an attribute as required. If a caller does not pass the given attribute,
a compile warning is issued.
* `:default` - the default value for the attribute if not provided. If this option is
not set and the attribute is not given, accessing the attribute will fail unless a
value is explicitly set with `assign_new/3`.
* `:examples` - a non-exhaustive list of values accepted by the attribute, used for documentation
purposes.
* `:values` - an exhaustive list of values accepted by the attributes. If a caller passes a literal
not contained in this list, a compile warning is issued.
* `:doc` - documentation for the attribute.
## Compile-Time Validations
LiveView performs some validation of attributes via the `:phoenix_live_view` compiler.
When attributes are defined, LiveView will warn at compilation time on the caller if:
* A required attribute of a component is missing.
* An unknown attribute is given.
* You specify a literal attribute (such as `value="string"` or `value`, but not `value={expr}`)
and the type does not match. The following types currently support literal validation:
`:string`, `:atom`, `:boolean`, `:integer`, `:float`, and `:list`.
* You specify a literal attribute and it is not a member of the `:values` list.
LiveView does not perform any validation at runtime. This means the type information is mostly
used for documentation and reflection purposes.
On the side of the LiveView component itself, defining attributes provides the following quality
of life improvements:
* The default value of all attributes will be added to the `assigns` map upfront. Note that
unless an attribute is marked as required or has a default defined, omitting a value for an
attribute will result in `nil` being passed as the default value to the `assigns` map,
regardless of the type defined for the attribute.
* Attribute documentation is generated for the component.
* Required struct types are annotated and emit compilation warnings. For example, if you specify
`attr :user, User, required: true` and then you write `@user.non_valid_field` in your template,
a warning will be emitted.
* Calls made to the component are tracked for reflection and validation purposes.
## Documentation Generation
Public function components that define attributes will have their attribute
types and docs injected into the function's documentation, depending on the
value of the `@doc` module attribute:
* if `@doc` is a string, the attribute docs are injected into that string. The optional
placeholder `[INSERT LVATTRDOCS]` can be used to specify where in the string the docs are
injected. Otherwise, the docs are appended to the end of the `@doc` string.
* if `@doc` is unspecified, the attribute docs are used as the default `@doc` string.
* if `@doc` is `false`, the attribute docs are omitted entirely.
The injected attribute docs are formatted as a markdown list:
* `name` (`:type`) (required) - attr docs. Defaults to `:default`.
By default, all attributes will have their types and docs injected into the function `@doc`
string. To hide a specific attribute, you can set the value of `:doc` to `false`.
## Example
attr :name, :string, required: true
attr :age, :integer, required: true
def celebrate(assigns) do
~H"""
Happy birthday <%= @name %>!
You are <%= @age %> years old.
"""
end
'''
defmacro attr(name, type, opts \\ []) when is_atom(name) and is_list(opts) do
quote bind_quoted: [name: name, type: type, opts: opts] do
Phoenix.Component.Declarative.__attr__!(
__MODULE__,
name,
type,
opts,
__ENV__.line,
__ENV__.file
)
end
end
## Components
# TODO: Insert docs for components
import Kernel, except: [def: 2, defp: 2]
import Phoenix.Component.Declarative
alias Phoenix.Component.Declarative
# We need to bootstrap by hand to avoid conflicts.
[] = Declarative.__setup__(__MODULE__, [])
attr = fn name, type, opts ->
Declarative.__attr__!(__MODULE__, name, type, opts, __ENV__.line, __ENV__.file)
end
slot = fn name, opts ->
Declarative.__slot__!(__MODULE__, name, opts, __ENV__.line, __ENV__.file, fn -> nil end)
end
@doc """
A function component for rendering `Phoenix.LiveComponent` within a parent LiveView.
While `LiveView`s can be nested, each LiveView starts its own process. A `LiveComponent` provides
similar functionality to `LiveView`, except they run in the same process as the `LiveView`,
with its own encapsulated state. That's why they are called stateful components.
## Attributes
* `id` (`:string`) (required) - A unique identifier for the LiveComponent. Note the `id` won't
necessarily be used as the DOM `id`. That is up to the component to decide.
* `module` (`:atom`) (required) - The LiveComponent module to render.
Any additional attributes provided will be passed to the LiveComponent as a map of assigns.
See `Phoenix.LiveComponent` for more information.
## Examples
```heex
<.live_component module={MyApp.WeatherComponent} id="thermostat" city="Kraków" />
```
"""
@doc type: :component
def live_component(assigns)
# TODO: add declarative attrs once we support non-global dynamic attrs
def live_component(assigns) when is_map(assigns) do
id = assigns[:id]
{module, assigns} =
assigns
|> Map.delete(:__changed__)
|> Map.pop(:module)
if module == nil or not is_atom(module) do
raise ArgumentError,
".live_component expects module={...} to be given and to be an atom, " <>
"got: #{inspect(module)}"
end
if id == nil do
raise ArgumentError, ".live_component expects id={...} to be given, got: nil"
end
case module.__live__() do
%{kind: :component} ->
%Phoenix.LiveView.Component{id: id, assigns: assigns, component: module}
%{kind: kind} ->
raise ArgumentError, "expected #{inspect(module)} to be a component, but it is a #{kind}"
end
end
def live_component(component) when is_atom(component) do
IO.warn(
"<%= live_component Component %> is deprecated, " <>
"please use <.live_component module={Component} id=\"hello\" /> inside HEEx templates instead"
)
Phoenix.LiveView.Helpers.__live_component__(component.__live__(), %{}, nil)
end
@doc """
Renders a title with automatic prefix/suffix on `@page_title` updates.
[INSERT LVATTRDOCS]
## Examples
```heex
<.live_title prefix="MyApp – ">
<%= assigns[:page_title] || "Welcome" %>
```
```heex
<.live_title suffix="- MyApp">
<%= assigns[:page_title] || "Welcome" %>
```
"""
@doc type: :component
attr.(:prefix, :string, default: nil, doc: "A prefix added before the content of `inner_block`.")
attr.(:suffix, :string, default: nil, doc: "A suffix added after the content of `inner_block`.")
slot.(:inner_block, required: true, doc: "Content rendered inside the `title` tag.")
def live_title(assigns) do
~H"""
<%= @prefix %><%= render_slot(@inner_block) %><%= @suffix %>
"""
end
@doc """
Renders a form.
[INSERT LVATTRDOCS]
This function is built on top of `Phoenix.HTML.Form.form_for/4`.
For more information about options and how to build inputs, see `Phoenix.HTML.Form`.
## Examples
### Inside LiveView
The `:for` attribute is typically an
[`Ecto.Changeset`](https://hexdocs.pm/ecto/Ecto.Changeset.html):
```heex
<.form
:let={f}
for={@changeset}
phx-change="change_name"
>
<%= text_input f, :name %>
<.form
:let={user_form}
for={@changeset}
multipart
phx-change="change_user"
phx-submit="save_user"
>
<%= text_input user_form, :name %>
<%= submit "Save" %>
```
Notice how both examples use `phx-change`. The LiveView must implement the `phx-change` event
and store the input values as they arrive on change. This is important because, if an unrelated
change happens on the page, LiveView should re-render the inputs with their updated values.
Without `phx-change`, the inputs would otherwise be cleared. Alternatively, you can use
`phx-update="ignore"` on the form to discard any updates.
The `:for` attribute can also be an atom, in case you don't have an existing data layer but you
want to use the existing form helpers. In this case, you need to pass the input values explicitly
as they change (or use `phx-update="ignore"` as per the previous paragraph):
```heex
<.form :let={user_form} for={:user} multipart phx-change="change_user" phx-submit="save_user">
<%= text_input user_form, :name, value: @user_name %>
<%= submit "Save" %>
```
In those cases, it may be more straight-forward to drop `:let` altogether and simply rely on
HTML to generate inputs:
```heex
<.form for={:form} multipart phx-change="change_user" phx-submit="save_user">
```
### Outside LiveView
The `form` component can still be used to submit forms outside of LiveView. In such cases, the
`action` attribute MUST be given. Without said attribute, the `form` method and csrf token are
discarded.
```heex
<.form :let={f} for={@changeset} action={Routes.comment_path(:create, @comment)}>
<%= text_input f, :body %>
```
"""
@doc type: :component
attr.(:for, :any, required: true, doc: "The form source data.")
attr.(:action, :string,
default: nil,
doc: """
The action to submit the form on.
This attribute must be given if you intend to submit the form to a URL without LiveView.
"""
)
attr.(:as, :atom,
default: nil,
doc: """
The server side parameter in which all params for this form
will be collected (i.e. `as: :user_params` would mean all fields for this form will be
accessed as `conn.params.user_params` server side).
Automatically inflected when a changeset is given.
"""
)
attr.(:multipart, :boolean,
default: false,
doc: """
Sets `enctype` to `multipart/form-data`.
Required when uploading files.
"""
)
attr.(:method, :string,
default: nil,
doc: """
The HTTP method.
It is only used if an `:action` is given. If the method is not `get` nor `post`,
an input tag with name `_method` is generated alongside the form tag.
"""
)
attr.(:csrf_token, :any,
doc: """
A token to authenticate the validity of requests.
One is automatically generated when an action is given and the method is not `get`.
When set to `false`, no token is generated.
"""
)
attr.(:errors, :list,
default: [],
doc: """
Use this to manually pass a keyword list of errors to the form,
e.g. `conn.assigns[:errors]`. This option is only used when a connection is used as the form
source and it will make the errors available under `f.errors`.
"""
)
attr.(:rest, :global,
include: ~w(autocomplete name rel enctype novalidate target),
doc: "Additional HTML attributes to add to the form tag."
)
slot.(:inner_block, required: true, doc: "The content rendered inside of the form tag.")
def form(assigns) do
# Extract options and then to the same call as form_for
action = assigns[:action]
form_for = assigns[:for] || raise ArgumentError, "missing :for assign to form"
form_options = assigns_to_attributes(Map.merge(assigns, assigns.rest), [:action, :for, :rest])
# Since FormData may add options, read the actual options from form
%{options: opts} =
form = %Phoenix.HTML.Form{
Phoenix.HTML.FormData.to_form(form_for, form_options)
| action: action || "#"
}
# By default, we will ignore action, method, and csrf token
# unless the action is given.
{attrs, hidden_method, csrf_token} =
if action do
{method, opts} = Keyword.pop!(opts, :method)
{method, hidden_method} = form_method(method)
{csrf_token, opts} =
Keyword.pop_lazy(opts, :csrf_token, fn ->
if method == "post" do
Plug.CSRFProtection.get_csrf_token_for(action)
end
end)
{[action: action, method: method] ++ opts, hidden_method, csrf_token}
else
{opts, nil, nil}
end
attrs =
case Keyword.pop(attrs, :multipart, false) do
{false, attrs} -> attrs
{true, attrs} -> Keyword.put(attrs, :enctype, "multipart/form-data")
end
assigns =
assign(assigns,
form: form,
csrf_token: csrf_token,
hidden_method: hidden_method,
attrs: attrs
)
~H"""
"""
end
defp form_method(nil), do: {"post", nil}
defp form_method(method) when method in ~w(get post), do: {method, nil}
defp form_method(method) when is_binary(method), do: {"post", method}
@doc """
Generates a link for live and href navigation.
[INSERT LVATTRDOCS]
## Examples
```heex
<.link href="/">Regular anchor link
```
```heex
<.link navigate={Routes.page_path(@socket, :index)} class="underline">home
```
```heex
<.link navigate={Routes.live_path(@socket, MyLive, dir: :asc)} replace={false}>
Sort By Price
```
```heex
<.link patch={Routes.page_path(@socket, :index, :details)}>view details
```
```heex
<.link href={URI.parse("https://elixir-lang.org")}>hello
```
```heex
<.link href="/the_world" method={:delete} data-confirm="Really?">delete
```
## JavaScript dependency
In order to support links where `:method` is not `:get` or use the above data attributes,
`Phoenix.HTML` relies on JavaScript. You can load `priv/static/phoenix_html.js` into your
build tool.
### Data attributes
Data attributes are added as a keyword list passed to the `data` key. The following data
attributes are supported:
* `data-confirm` - shows a confirmation prompt before generating and submitting the form when
`:method` is not `:get`.
### Overriding the default confirm behaviour
`phoenix_html.js` does trigger a custom event `phoenix.link.click` on the clicked DOM element
when a click happened. This allows you to intercept the event on it's way bubbling up
to `window` and do your own custom logic to enhance or replace how the `data-confirm`
attribute is handled. You could for example replace the browsers `confirm()` behavior with
a custom javascript implementation:
```javascript
// listen on document.body, so it's executed before the default of
// phoenix_html, which is listening on the window object
document.body.addEventListener('phoenix.link.click', function (e) {
// Prevent default implementation
e.stopPropagation();
// Introduce alternative implementation
var message = e.target.getAttribute("data-confirm");
if(!message){ return true; }
vex.dialog.confirm({
message: message,
callback: function (value) {
if (value == false) { e.preventDefault(); }
}
})
}, false);
```
Or you could attach your own custom behavior.
```javascript
window.addEventListener('phoenix.link.click', function (e) {
// Introduce custom behaviour
var message = e.target.getAttribute("data-prompt");
var answer = e.target.getAttribute("data-prompt-answer");
if(message && answer && (answer != window.prompt(message))) {
e.preventDefault();
}
}, false);
```
The latter could also be bound to any `click` event, but this way you can be sure your custom
code is only executed when the code of `phoenix_html.js` is run.
## CSRF Protection
By default, CSRF tokens are generated through `Plug.CSRFProtection`.
"""
@doc type: :component
attr.(:navigate, :string,
doc: """
Navigates from a LiveView to a new LiveView.
The browser page is kept, but a new LiveView process is mounted and its content on the page
is reloaded. It is only possible to navigate between LiveViews declared under the same router
`Phoenix.LiveView.Router.live_session/3`. Otherwise, a full browser redirect is used.
"""
)
attr.(:patch, :string,
doc: """
Patches the current LiveView.
The `handle_params` callback of the current LiveView will be invoked and the minimum content
will be sent over the wire, as any other LiveView diff.
"""
)
attr.(:href, :any,
doc: """
Uses traditional browser navigation to the new location.
This means the whole page is reloaded on the browser.
"""
)
attr.(:replace, :boolean,
default: false,
doc: """
When using `:patch` or `:navigate`,
should the browser's history be replaced with `pushState`?
"""
)
attr.(:method, :string,
default: "get",
doc: """
The HTTP method to use with the link.
In case the method is not `get`, the link is generated inside the form which sets the proper
information. In order to submit the form, JavaScript must be enabled in the browser.
"""
)
attr.(:csrf_token, :string,
default: nil,
doc: """
A custom token to use for links with an HTTP method other than `get`.
"""
)
attr.(:rest, :global,
include: ~w(download hreflang referrerpolicy rel target type),
doc: """
Additional HTML attributes added to the `a` tag.
"""
)
slot.(:inner_block,
required: true,
doc: """
The content rendered inside of the `a` tag.
"""
)
def link(%{navigate: to} = assigns) when is_binary(to) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
def link(%{patch: to} = assigns) when is_binary(to) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
def link(%{href: href} = assigns) when href != "#" and not is_nil(href) do
assigns =
case Phoenix.LiveView.Utils.valid_destination!(href, "<.link>") do
href when is_binary(href) ->
assigns
|> assign(:href, href)
|> update(:csrf_token, fn
nil -> Phoenix.HTML.Tag.csrf_token_value(href)
csrf_token -> csrf_token
end)
href ->
assign(assigns, :href, href)
end
~H"""
<%= render_slot(@inner_block) %>
"""
end
def link(%{} = assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Wraps tab focus around a container for accessibility.
This is an essential accessibility feature for interfaces such as modals, dialogs, and menus.
[INSERT LVATTRDOCS]
## Examples
Simply render your inner content within this component and focus will be wrapped around the
container as the user tabs through the containers content:
```heex
<.focus_wrap id="my-modal" class="bg-white">
Are you sure?
Cancel
OK
```
"""
@doc type: :component
attr.(:id, :string, required: true, doc: "The DOM identifier of the container tag.")
attr.(:rest, :global, doc: "Additional HTML attributes to add to the container tag.")
slot.(:inner_block, required: true, doc: "The content rendered inside of the container tag.")
def focus_wrap(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Generates a dynamically named HTML tag.
Raises an `ArgumentError` if the tag name is found to be unsafe HTML.
[INSERT LVATTRDOCS]
## Examples
```heex
<.dynamic_tag name="input" type="text"/>
```
```html
```
```heex
<.dynamic_tag name="p">content
```
```html
content
```
"""
@doc type: :component
attr.(:name, :string, required: true, doc: "The name of the tag, such as `div`.")
attr.(:rest, :global,
doc: """
Additional HTML attributes to add to the tag, ensuring proper escaping.
"""
)
slot.(:inner_block, [])
def dynamic_tag(%{name: name, rest: rest} = assigns) do
tag_name = to_string(name)
tag =
case Phoenix.HTML.html_escape(tag_name) do
{:safe, ^tag_name} ->
tag_name
{:safe, _escaped} ->
raise ArgumentError,
"expected dynamic_tag name to be safe HTML, got: #{inspect(tag_name)}"
end
assigns =
assigns
|> assign(:tag, tag)
|> assign(:escaped_attrs, Phoenix.HTML.attributes_escape(rest))
if assigns.inner_block != [] do
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?>]} %><%= render_slot(@inner_block) %><%= {:safe, [?<, ?/, @tag, ?>]} %>
"""
else
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?/, ?>]} %>
"""
end
end
@doc """
Builds a file input tag for a LiveView upload.
## Attributes
* `:upload` - The `%Phoenix.LiveView.UploadConfig{}` struct.
Arbitrary attributes may be passed to be applied to the file input tag.
## Drag and Drop
Drag and drop is supported by annotating the droppable container with a `phx-drop-target`
attribute pointing to the DOM `id` of the file input. By default, the file input `id` is the
upload `ref`, so the following markup is all that is required for drag and drop support:
```heex
<.live_file_input upload={@uploads.avatar} />
```
## Examples
```heex
<.live_file_input upload={@uploads.avatar} />
```
"""
@doc type: :component
def live_file_input(assigns)
def live_file_input(%Phoenix.LiveView.UploadConfig{} = conf) do
IO.warn(
"live_file_input(upload) is deprecatead, please use <.live_file_input upload={upload} /> instead"
)
Phoenix.LiveView.Helpers.live_file_input(conf, [])
end
# TODO: Use attr when the backwards compatibility clause above is removed.
# attr :upload, Phoenix.LiveView.UploadConfig, required: true
# attr :rest, :global
def live_file_input(%{} = assigns) do
conf =
case assigns do
%{id: _} -> raise ArgumentError, "the :id cannot be overridden on a live_file_input"
%{upload: %Phoenix.LiveView.UploadConfig{} = conf} -> conf
%{} -> raise ArgumentError, "missing required :upload attribute to <.live_file_input/>"
end
rest = assigns_to_attributes(assigns, [:upload])
rest =
if conf.max_entries > 1 do
Keyword.put(rest, :multiple, true)
else
rest
end
assigns =
assign(assigns,
conf: conf,
rest: rest,
valid?: Enum.any?(conf.entries) && Enum.empty?(conf.errors),
done_entries: for(entry <- conf.entries, entry.done?, do: entry),
preflighted_entries: for(entry <- conf.entries, entry.preflighted?, do: entry)
)
~H"""
"""
end
@doc """
Generates an image preview on the client for a selected file.
## Examples
```heex
<%= for entry <- @uploads.avatar.entries do %>
<.live_img_preview entry={entry} width="75" />
<% end %>
```
"""
@doc type: :component
def live_img_preview(assigns)
def live_img_preview(%Phoenix.LiveView.UploadEntry{} = entry) do
IO.warn("""
live_img_preview(entry) is deprecatead, please use <.live_img_preview entry={entry} /> instead
""")
live_img_preview(%{entry: entry})
end
# TODO: Use attr when the backwards compatibility clause above is removed.
# attr :entry, Phoenix.LiveView.UploadEntry, required: true
# attr :rest, :global
def live_img_preview(%{entry: %Phoenix.LiveView.UploadEntry{ref: ref} = entry} = assigns) do
rest =
assigns
|> assigns_to_attributes([:entry])
|> Keyword.put_new_lazy(:id, fn -> "phx-preview-#{ref}" end)
assigns = assign(assigns, entry: entry, ref: ref, rest: rest)
~H"""
"""
end
def live_img_preview(_assigns) do
raise ArgumentError, "missing required :entry attribute to <.live_img_preview/>"
end
end