3561 lines
110 KiB
Elixir
3561 lines
110 KiB
Elixir
defmodule Phoenix.Component do
|
|
@moduledoc ~S'''
|
|
Define reusable function components with HEEx templates.
|
|
|
|
A function component is any function that receives an assigns
|
|
map as an argument and returns a rendered struct built with
|
|
[the `~H` sigil](`sigil_H/2`):
|
|
|
|
defmodule MyComponent do
|
|
# In Phoenix apps, the line is typically: use MyAppWeb, :html
|
|
use Phoenix.Component
|
|
|
|
def greet(assigns) do
|
|
~H"""
|
|
<p>Hello, {@name}!</p>
|
|
"""
|
|
end
|
|
end
|
|
|
|
This function uses the `~H` sigil to return a rendered template.
|
|
`~H` stands for HEEx (HTML + EEx). HEEx is a template language for
|
|
writing HTML mixed with Elixir interpolation. We can write Elixir
|
|
code inside `{...}` for HTML-aware interpolation inside tag attributes
|
|
and the body. We can also interpolate arbitrary HEEx blocks using `<%= ... %>`
|
|
We use `@name` to access the key `name` defined inside `assigns`.
|
|
|
|
When invoked within a `~H` sigil or HEEx template file:
|
|
|
|
```heex
|
|
<MyComponent.greet name="Jane" />
|
|
```
|
|
|
|
The following HTML is rendered:
|
|
|
|
```html
|
|
<p>Hello, Jane!</p>
|
|
```
|
|
|
|
If the function component is defined locally, or its module is imported,
|
|
then the caller can invoke the function directly without specifying the module:
|
|
|
|
```heex
|
|
<.greet name="Jane" />
|
|
```
|
|
|
|
For dynamic values, you can interpolate Elixir expressions into a function component:
|
|
|
|
```heex
|
|
<.greet name={@user.name} />
|
|
```
|
|
|
|
Function components can also accept blocks of HEEx content (more on this later):
|
|
|
|
```heex
|
|
<.card>
|
|
<p>This is the body of my card!</p>
|
|
</.card>
|
|
```
|
|
|
|
In this module we will learn how to build rich and composable components to
|
|
use in our applications.
|
|
|
|
## Attributes
|
|
|
|
`Phoenix.Component` provides the `attr/3` macro to declare what attributes the proceeding function
|
|
component expects to receive when invoked:
|
|
|
|
attr :name, :string, required: true
|
|
|
|
def greet(assigns) do
|
|
~H"""
|
|
<p>Hello, {@name}!</p>
|
|
"""
|
|
end
|
|
|
|
By calling `attr/3`, it is now clear that `greet/1` requires a string attribute called `name`
|
|
present in its assigns map to properly render. Failing to do so will result in a compilation
|
|
warning:
|
|
|
|
```heex
|
|
<MyComponent.greet />
|
|
<!-- warning: missing required attribute "name" for component MyAppWeb.MyComponent.greet/1
|
|
lib/app_web/my_component.ex:15 -->
|
|
```
|
|
|
|
Attributes can provide default values that are automatically merged into the assigns map:
|
|
|
|
attr :name, :string, default: "Bob"
|
|
|
|
Now you can invoke the function component without providing a value for `name`:
|
|
|
|
```heex
|
|
<.greet />
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<p>Hello, Bob!</p>
|
|
```
|
|
|
|
Accessing an attribute which is required and does not have a default value will fail.
|
|
You must explicitly declare `default: nil` or assign a value programmatically with the
|
|
`assign_new/3` function.
|
|
|
|
Multiple attributes can be declared for the same function component:
|
|
|
|
attr :name, :string, required: true
|
|
attr :age, :integer, required: true
|
|
|
|
def celebrate(assigns) do
|
|
~H"""
|
|
<p>
|
|
Happy birthday {@name}!
|
|
You are {@age} years old.
|
|
</p>
|
|
"""
|
|
end
|
|
|
|
Allowing the caller to pass multiple values:
|
|
|
|
```heex
|
|
<.celebrate name={"Genevieve"} age={34} />
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<p>
|
|
Happy birthday Genevieve!
|
|
You are 34 years old.
|
|
</p>
|
|
```
|
|
|
|
Multiple function components can be defined in the same module, with different attributes. In the
|
|
following example, `<Components.greet/>` requires a `name`, but *does not* require a `title`, and
|
|
`<Components.heading>` requires a `title`, but *does not* require a `name`.
|
|
|
|
defmodule Components do
|
|
# In Phoenix apps, the line is typically: use MyAppWeb, :html
|
|
use Phoenix.Component
|
|
|
|
attr :title, :string, required: true
|
|
|
|
def heading(assigns) do
|
|
~H"""
|
|
<h1>{@title}</h1>
|
|
"""
|
|
end
|
|
|
|
attr :name, :string, required: true
|
|
|
|
def greet(assigns) do
|
|
~H"""
|
|
<p>Hello {@name}</p>
|
|
"""
|
|
end
|
|
end
|
|
|
|
With the `attr/3` macro you have the core ingredients to create reusable function components.
|
|
But what if you need your function components to support dynamic attributes, such as common HTML
|
|
attributes to mix into a component's container?
|
|
|
|
## Global attributes
|
|
|
|
Global attributes are a set of attributes that a function component can accept when it
|
|
declares an attribute of type `:global`. By default, the set of attributes accepted are those
|
|
attributes common to all standard HTML tags.
|
|
See [Global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)
|
|
for a complete list of attributes.
|
|
|
|
Once a global attribute is declared, any number of attributes in the set can be passed by
|
|
the caller without having to modify the function component itself.
|
|
|
|
Below is an example of a function component that accepts a dynamic number of global attributes:
|
|
|
|
attr :message, :string, required: true
|
|
attr :rest, :global
|
|
|
|
def notification(assigns) do
|
|
~H"""
|
|
<span {@rest}>{@message}</span>
|
|
"""
|
|
end
|
|
|
|
The caller can pass multiple global attributes (such as `phx-*` bindings or the `class` attribute):
|
|
|
|
```heex
|
|
<.notification message="You've got mail!" class="bg-green-200" phx-click="close" />
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<span class="bg-green-200" phx-click="close">You've got mail!</span>
|
|
```
|
|
|
|
Note that the function component did not have to explicitly declare a `class` or `phx-click`
|
|
attribute in order to render.
|
|
|
|
Global attributes can define defaults which are merged with attributes provided by the caller.
|
|
For example, you may declare a default `class` if the caller does not provide one:
|
|
|
|
attr :rest, :global, default: %{class: "bg-blue-200"}
|
|
|
|
Now you can call the function component without a `class` attribute:
|
|
|
|
```heex
|
|
<.notification message="You've got mail!" phx-click="close" />
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<span class="bg-blue-200" phx-click="close">You've got mail!</span>
|
|
```
|
|
|
|
Note that the global attribute cannot be provided directly and doing so will emit
|
|
a warning. In other words, this is invalid:
|
|
|
|
```heex
|
|
<.notification message="You've got mail!" rest={%{"phx-click" => "close"}} />
|
|
```
|
|
|
|
### Included globals
|
|
|
|
You may also specify which attributes are included in addition to the known globals
|
|
with the `:include` option. For example to support the `form` attribute on a button
|
|
component:
|
|
|
|
```elixir
|
|
# <.button form="my-form"/>
|
|
attr :rest, :global, include: ~w(form)
|
|
slot :inner_block
|
|
def button(assigns) do
|
|
~H"""
|
|
<button {@rest}>{render_slot(@inner_block)}</button>
|
|
"""
|
|
end
|
|
```
|
|
|
|
The `:include` option is useful to apply global additions on a case-by-case basis,
|
|
but sometimes you want to extend existing components with new global attributes,
|
|
such as Alpine.js' `x-` prefixes, which we'll outline next.
|
|
|
|
### Custom global attribute prefixes
|
|
|
|
You can extend the set of global attributes by providing a list of attribute prefixes to
|
|
`use Phoenix.Component`. Like the default attributes common to all HTML elements,
|
|
any number of attributes that start with a global prefix will be accepted by function
|
|
components invoked by the current module. By default, the following prefixes are supported:
|
|
`phx-`, `aria-`, and `data-`. For example, to support the `x-` prefix used by
|
|
[Alpine.js](https://alpinejs.dev/), you can pass the `:global_prefixes` option to
|
|
`use Phoenix.Component`:
|
|
|
|
use Phoenix.Component, global_prefixes: ~w(x-)
|
|
|
|
In your Phoenix application, this is typically done in your
|
|
`lib/my_app_web.ex` file, inside the `def html` definition:
|
|
|
|
def html do
|
|
quote do
|
|
use Phoenix.Component, global_prefixes: ~w(x-)
|
|
# ...
|
|
end
|
|
end
|
|
|
|
Now all function components invoked by this module will accept any number of attributes
|
|
prefixed with `x-`, in addition to the default global prefixes.
|
|
|
|
You can learn more about attributes by reading the documentation for `attr/3`.
|
|
|
|
## Slots
|
|
|
|
In addition to attributes, function components can accept blocks of HEEx content, referred to
|
|
as slots. Slots enable further customization of the rendered HTML, as the caller can pass the
|
|
function component HEEx content they want the component to render. `Phoenix.Component` provides
|
|
the `slot/3` macro used to declare slots for function components:
|
|
|
|
slot :inner_block, required: true
|
|
|
|
def button(assigns) do
|
|
~H"""
|
|
<button>
|
|
{render_slot(@inner_block)}
|
|
</button>
|
|
"""
|
|
end
|
|
|
|
The expression `render_slot(@inner_block)` renders the HEEx content. You can invoke this function
|
|
component like so:
|
|
|
|
```heex
|
|
<.button>
|
|
This renders <strong>inside</strong> the button!
|
|
</.button>
|
|
```
|
|
|
|
Which renders the following HTML:
|
|
|
|
```html
|
|
<button>
|
|
This renders <strong>inside</strong> the button!
|
|
</button>
|
|
```
|
|
|
|
Like the `attr/3` macro, using the `slot/3` macro will provide compile-time validations.
|
|
For example, invoking `button/1` without a slot of HEEx content will result in a compilation
|
|
warning being emitted:
|
|
|
|
```heex
|
|
<.button />
|
|
<!-- warning: missing required slot "inner_block" for component MyAppWeb.MyComponent.button/1
|
|
lib/app_web/my_component.ex:15 -->
|
|
```
|
|
|
|
### The default slot
|
|
|
|
The example above uses the default slot, accessible as an assign named `@inner_block`, to render
|
|
HEEx content via the `render_slot/1` function.
|
|
|
|
If the values rendered in the slot need to be dynamic, you can pass a second value back to the
|
|
HEEx content by calling `render_slot/2`:
|
|
|
|
slot :inner_block, required: true
|
|
|
|
attr :entries, :list, default: []
|
|
|
|
def unordered_list(assigns) do
|
|
~H"""
|
|
<ul>
|
|
<li :for={entry <- @entries}>{render_slot(@inner_block, entry)}</li>
|
|
</ul>
|
|
"""
|
|
end
|
|
|
|
When invoking the function component, you can use the special attribute `:let` to take the value
|
|
that the function component passes back and bind it to a variable:
|
|
|
|
```heex
|
|
<.unordered_list :let={fruit} entries={~w(apples bananas cherries)}>
|
|
I like <b>{fruit}</b>!
|
|
</.unordered_list>
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<ul>
|
|
<li>I like <b>apples</b>!</li>
|
|
<li>I like <b>bananas</b>!</li>
|
|
<li>I like <b>cherries</b>!</li>
|
|
</ul>
|
|
```
|
|
|
|
Now the separation of concerns is maintained: the caller can specify multiple values in a list
|
|
attribute without having to specify the HEEx content that surrounds and separates them.
|
|
|
|
### Named slots
|
|
|
|
In addition to the default slot, function components can accept multiple, named slots of HEEx
|
|
content. For example, imagine you want to create a modal that has a header, body, and footer:
|
|
|
|
slot :header
|
|
slot :inner_block, required: true
|
|
slot :footer, required: true
|
|
|
|
def modal(assigns) do
|
|
~H"""
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
{render_slot(@header) || "Modal"}
|
|
</div>
|
|
<div class="modal-body">
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
<div class="modal-footer">
|
|
{render_slot(@footer)}
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
You can invoke this function component using the named slot HEEx syntax:
|
|
|
|
```heex
|
|
<.modal>
|
|
This is the body, everything not in a named slot is rendered in the default slot.
|
|
<:footer>
|
|
This is the bottom of the modal.
|
|
</:footer>
|
|
</.modal>
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
Modal.
|
|
</div>
|
|
<div class="modal-body">
|
|
This is the body, everything not in a named slot is rendered in the default slot.
|
|
</div>
|
|
<div class="modal-footer">
|
|
This is the bottom of the modal.
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
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.
|
|
|
|
### Slot attributes
|
|
|
|
Unlike the default slot, it is possible to pass a named slot multiple pieces of HEEx content.
|
|
Named slots can also accept attributes, defined by passing a block to the `slot/3` macro.
|
|
If multiple pieces of content are passed, `render_slot/2` will merge and render all the values.
|
|
|
|
Below is a table component illustrating multiple named slots with attributes:
|
|
|
|
slot :column, doc: "Columns with column labels" do
|
|
attr :label, :string, required: true, doc: "Column label"
|
|
end
|
|
|
|
attr :rows, :list, default: []
|
|
|
|
def table(assigns) do
|
|
~H"""
|
|
<table>
|
|
<tr>
|
|
<th :for={col <- @column}>{col.label}</th>
|
|
</tr>
|
|
<tr :for={row <- @rows}>
|
|
<td :for={col <- @column}>{render_slot(col, row)}</td>
|
|
</tr>
|
|
</table>
|
|
"""
|
|
end
|
|
|
|
You can invoke this function component like so:
|
|
|
|
```heex
|
|
<.table rows={[%{name: "Jane", age: "34"}, %{name: "Bob", age: "51"}]}>
|
|
<:column :let={user} label="Name">
|
|
{user.name}
|
|
</:column>
|
|
<:column :let={user} label="Age">
|
|
{user.age}
|
|
</:column>
|
|
</.table>
|
|
```
|
|
|
|
Rendering the following HTML:
|
|
|
|
```html
|
|
<table>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Age</th>
|
|
</tr>
|
|
<tr>
|
|
<td>Jane</td>
|
|
<td>34</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Bob</td>
|
|
<td>51</td>
|
|
</tr>
|
|
</table>
|
|
```
|
|
|
|
You can learn more about slots and the `slot/3` macro [in its documentation](`slot/3`).
|
|
|
|
## Embedding external template files
|
|
|
|
The `embed_templates/1` macro can be used to embed `.html.heex` files
|
|
as function components. The directory path is based on the current
|
|
module (`__DIR__`), and a wildcard pattern may be used to select all
|
|
files within a directory tree. For example, imagine a directory listing:
|
|
|
|
```plain
|
|
├── components.ex
|
|
├── cards
|
|
│ ├── pricing_card.html.heex
|
|
│ └── features_card.html.heex
|
|
```
|
|
|
|
Then you can embed the page templates in your `components.ex` module
|
|
and call them like any other function component:
|
|
|
|
defmodule MyAppWeb.Components do
|
|
use Phoenix.Component
|
|
|
|
embed_templates "cards/*"
|
|
|
|
def landing_hero(assigns) do
|
|
~H"""
|
|
<.pricing_card />
|
|
<.features_card />
|
|
"""
|
|
end
|
|
end
|
|
|
|
See `embed_templates/1` for more information, including declarative
|
|
assigns support for embedded templates.
|
|
|
|
## Debug information
|
|
|
|
HEEx templates support adding annotations and locations to the rendered
|
|
page, which are special HTML comments and attributes that help you identify
|
|
where markup in your HTML document is rendered within your function component
|
|
tree.
|
|
|
|
For example, imagine the following HEEx template:
|
|
|
|
```heex
|
|
<.header>
|
|
<.button>Click</.button>
|
|
</.header>
|
|
```
|
|
|
|
By turning on `debug_heex_annotations`, the HTML document would receive the
|
|
following comments when debug annotations are enabled:
|
|
|
|
```html
|
|
<!-- @caller lib/app_web/home_live.ex:20 -->
|
|
<!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
|
|
<header class="p-5">
|
|
<!-- @caller lib/app_web/home_live.ex:48 -->
|
|
<!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
|
|
<button class="px-2 bg-indigo-500 text-white">Click</button>
|
|
<!-- </AppWeb.CoreComponents.button> -->
|
|
</header>
|
|
<!-- </AppWeb.CoreComponents.header> -->
|
|
```
|
|
|
|
Similarly, you can also turn on `:debug_attributes`, which adds a
|
|
`data-phx-loc` attribute with the line of where each HTML tag is defined
|
|
(as well as `data-phx-pid` to the LiveView container):
|
|
|
|
```html
|
|
<header data-phx-loc="125" class="p-5">
|
|
<button data-phx-loc="458" class="px-2 bg-indigo-500 text-white">Click</button>
|
|
</header>
|
|
```
|
|
|
|
These features work on any `~H` or `.html.heex` template. They can be enabled
|
|
globally with the following configuration in your `config/dev.exs` file:
|
|
|
|
config :phoenix_live_view,
|
|
debug_heex_annotations: true,
|
|
debug_attributes: true
|
|
|
|
Changing this configuration will require `mix clean` and a full recompile.
|
|
|
|
## Dynamic Component Rendering
|
|
|
|
Sometimes you might need to decide at runtime which component to render.
|
|
Because function components are just regular functions, we can leverage
|
|
Elixir's `apply/3` function to dynamically call a module and/or function passed
|
|
in as an assign.
|
|
|
|
For example, using the following function component definition:
|
|
|
|
```elixir
|
|
attr :module, :atom, required: true
|
|
attr :function, :atom, required: true
|
|
# any shared attributes
|
|
attr :shared, :string, required: true
|
|
|
|
# any shared slots
|
|
slot :named_slot, required: true
|
|
slot :inner_block, required: true
|
|
|
|
def dynamic_component(assigns) do
|
|
{mod, assigns} = Map.pop(assigns, :module)
|
|
{func, assigns} = Map.pop(assigns, :function)
|
|
|
|
apply(mod, func, [assigns])
|
|
end
|
|
```
|
|
|
|
Then you can use the `dynamic_component` function like so:
|
|
|
|
```heex
|
|
<.dynamic_component
|
|
module={MyAppWeb.MyModule}
|
|
function={:my_function}
|
|
shared="Yay Elixir!"
|
|
>
|
|
<p>Howdy from the inner block!</p>
|
|
<:named_slot>
|
|
<p>Howdy from the named slot!</p>
|
|
</:named_slot>
|
|
</.dynamic_component>
|
|
```
|
|
|
|
This will call the `MyAppWeb.MyModule.my_function/1` function passing in the remaining assigns.
|
|
|
|
```elixir
|
|
defmodule MyAppWeb.MyModule do
|
|
attr :shared, :string, required: true
|
|
|
|
slot :named_slot, required: true
|
|
slot :inner_block, required: true
|
|
|
|
def my_function(assigns) do
|
|
~H"""
|
|
<p>Dynamic component with shared assigns: {@shared}</p>
|
|
{render_slot(@inner_block)}
|
|
{render_slot(@named_slot)}
|
|
"""
|
|
end
|
|
end
|
|
```
|
|
|
|
Resulting in the following HTML:
|
|
|
|
```html
|
|
<p>Dynamic component with shared assigns: Yay Elixir!</p>
|
|
<p>Howdy from the inner block!</p>
|
|
<p>Howdy from the named slot!</p>
|
|
```
|
|
|
|
Note that to get the most out of `Phoenix.Component`'s compile-time validations, it is beneficial to
|
|
define such a `dynamic_component` for a specific set of components sharing the same API, instead of
|
|
defining it for the general case.
|
|
In this example, we defined our `dynamic_component` to expect an assign called `shared`, as well as
|
|
two slots that all components we want to use with it must implement.
|
|
The called `my_function` component's attribute and slot definitions cannot be validated through the apply call.
|
|
'''
|
|
|
|
## Functions
|
|
|
|
alias Phoenix.LiveView.{Static, Socket, AsyncResult}
|
|
@reserved_assigns Phoenix.Component.Declarative.__reserved__()
|
|
# Note we allow live_action as it may be passed down to a component, so it is not listed
|
|
@non_assignables [:uploads, :streams, :socket, :myself]
|
|
|
|
@doc ~S'''
|
|
The `~H` sigil for writing HEEx templates inside source files.
|
|
|
|
`HEEx` is a HTML-aware and component-friendly extension of Elixir Embedded
|
|
language (`EEx`) that provides:
|
|
|
|
* Built-in handling of HTML attributes
|
|
|
|
* An HTML-like notation for injecting function components
|
|
|
|
* Compile-time validation of the structure of the template
|
|
|
|
* The ability to minimize the amount of data sent over the wire
|
|
|
|
* Out-of-the-box code formatting via `mix format`
|
|
|
|
## Example
|
|
|
|
~H"""
|
|
<div title="My div" class={@class}>
|
|
<p>Hello {@name}</p>
|
|
<MyApp.Weather.city name="Kraków"/>
|
|
</div>
|
|
"""
|
|
|
|
## Syntax
|
|
|
|
`HEEx` is built on top of Embedded Elixir (`EEx`). In this section, we are going to
|
|
cover the basic constructs in `HEEx` templates as well as its syntax extensions.
|
|
|
|
### Interpolation
|
|
|
|
`HEEx` allows using `{...}` for HTML-aware interpolation, inside tag attributes
|
|
as well as the body:
|
|
|
|
```heex
|
|
<p>Hello, {@name}</p>
|
|
```
|
|
|
|
If you want to interpolate an attribute, you write:
|
|
|
|
```heex
|
|
<div class={@class}>
|
|
...
|
|
</div>
|
|
```
|
|
|
|
You can put any Elixir expression between `{ ... }`. For example, if you want
|
|
to set classes, where some are static and others are dynamic, you can using
|
|
string interpolation:
|
|
|
|
```heex
|
|
<div class={"btn btn-#{@type}"}>
|
|
...
|
|
</div>
|
|
```
|
|
|
|
The following attribute values have special meaning on HTML tags:
|
|
|
|
* `true` - if a value is `true`, the attribute is rendered with no value at all.
|
|
For example, `<input required={true}>` is the same as `<input required>`;
|
|
|
|
* `false` or `nil` - if a value is `false` or `nil`, the attribute is omitted.
|
|
Note the `class` and `style` attributes will be rendered as empty strings,
|
|
instead of ommitted, which has the same effect as not rendering them, but
|
|
allows for rendering optimizations.
|
|
|
|
* `list` (only for the `class` attribute) - each element of the list is processed
|
|
as a different class. `nil` and `false` elements are discarded. Nested lists
|
|
are supported and flattened.
|
|
|
|
For multiple dynamic attributes, you can use the same notation but without
|
|
assigning the expression to any specific attribute:
|
|
|
|
```heex
|
|
<div {@dynamic_attrs}>
|
|
...
|
|
</div>
|
|
```
|
|
|
|
In this case, the expression inside `{...}` must be either a keyword list or
|
|
a map containing the key-value pairs representing the dynamic attributes.
|
|
If using a map, ensure your keys are atoms.
|
|
|
|
### Interpolating blocks
|
|
|
|
The curly braces syntax is the default mechanism for interpolating code.
|
|
However, it cannot be used in all scenarios, in particular:
|
|
|
|
* Curly braces cannot be used inside `<script>` and `<style>` tags,
|
|
as that would make writing JS and CSS quite tedious. You can also
|
|
fully disable curly braces interpolation in a given tag and
|
|
its children by adding the `phx-no-curly-interpolation` attribute
|
|
|
|
* it does not support multiline block constructs, such as `if`,
|
|
`case`, and similar
|
|
|
|
For example, if you need to interpolate a string inside a script tag,
|
|
you could do:
|
|
|
|
```heex
|
|
<script>
|
|
window.URL = "<%= @my_url %>"
|
|
</script>
|
|
```
|
|
|
|
Similarly, for block constructs in Elixir, you can write:
|
|
|
|
```heex
|
|
<%= if @show_greeting? do %>
|
|
<p>Hello, {@name}</p>
|
|
<% end %>
|
|
```
|
|
|
|
However, for conditionals and for-comprehensions, there are built-in constructs
|
|
in HEEx too, which we will explore next.
|
|
|
|
> #### Curly braces in text within tag bodies {: .tip}
|
|
>
|
|
> If you have text in your tag bodies, which includes curly braces you can use
|
|
> `{` or `<%= "{" %>` to prevent them from being considered the start of
|
|
> interpolation.
|
|
|
|
### Special attributes
|
|
|
|
Apart from normal HTML attributes, HEEx also supports some special attributes
|
|
such as `:let` and `:for`.
|
|
|
|
#### :let
|
|
|
|
This is used by components and slots that want to yield a value back to the
|
|
caller. For an example, see how `form/1` works:
|
|
|
|
```heex
|
|
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
|
|
<.input field={f[:username]} type="text" />
|
|
...
|
|
</.form>
|
|
```
|
|
|
|
Notice how the variable `f`, defined by `.form` is used by your `input` component.
|
|
The `Phoenix.Component` module has detailed documentation on how to use and
|
|
implement such functionality.
|
|
|
|
#### :if and :for
|
|
|
|
It is a syntax sugar for `<%= if .. do %>` and `<%= for .. do %>` that can be
|
|
used in regular HTML, function components, and slots.
|
|
|
|
For example in an HTML tag:
|
|
|
|
```heex
|
|
<table id="admin-table" :if={@admin?}>
|
|
<tr :for={user <- @users}>
|
|
<td>{user.name}</td>
|
|
</tr>
|
|
<table>
|
|
```
|
|
|
|
The snippet above will only render the table if `@admin?` is true,
|
|
and generate a `tr` per user as you would expect from the collection.
|
|
|
|
`:for` can be used similarly in function components:
|
|
|
|
```heex
|
|
<.error :for={msg <- @errors} message={msg}/>
|
|
```
|
|
|
|
Which is equivalent to writing:
|
|
|
|
```heex
|
|
<%= for msg <- @errors do %>
|
|
<.error message={msg} />
|
|
<% end %>
|
|
```
|
|
|
|
And `:for` in slots behaves the same way:
|
|
|
|
```heex
|
|
<.table id="my-table" rows={@users}>
|
|
<:col :for={header <- @headers} :let={user}>
|
|
<td>{user[header]}</td>
|
|
</:col>
|
|
<.table>
|
|
```
|
|
|
|
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} />
|
|
```
|
|
|
|
Note that unlike Elixir's regular `for`, HEEx' `:for` does not support multiple
|
|
generators in one expression. In such cases, you must use `EEx`'s blocks.
|
|
|
|
> #### Change tracking `:for` on slots {: .warning}
|
|
>
|
|
> Compared to regular HTML tags and components, LiveView does not
|
|
> optimize comprehensions on slots.
|
|
> This means that if `@headers` changes in the example above, all
|
|
> headers are sent over the wire again.
|
|
>
|
|
> Furthermore, `:key` (see below) is also not supported on slots
|
|
> right now.
|
|
|
|
#### `:key`ed comprehensions
|
|
|
|
When using `:for`, you can optionally provide a `:key` expression to perform
|
|
better change tracking inside the comprehension:
|
|
|
|
```heex
|
|
<ul>
|
|
<li :for={%{id: id, name: name} <- @items} :key={id}>
|
|
Count: <span>{@count}</span>,
|
|
item: {name}
|
|
</li>
|
|
</ul>
|
|
```
|
|
|
|
By default, the index is used as a key, which means that appending an entry leads to
|
|
all items being considered changed. Therefore, we recommend to use a `:key` whenever possible.
|
|
|
|
Note that the `:key` has no effect when using [streams](`Phoenix.LiveView.stream/4`).
|
|
|
|
### Function components
|
|
|
|
Function components are stateless components implemented as pure functions
|
|
with the help of the `Phoenix.Component` module. They can be either local
|
|
(same module) or remote (external module).
|
|
|
|
`HEEx` allows invoking these function components directly in the template
|
|
using an HTML-like notation. For example, a remote function:
|
|
|
|
```heex
|
|
<MyApp.Weather.city name="Kraków"/>
|
|
```
|
|
|
|
A local function can be invoked with a leading dot:
|
|
|
|
```heex
|
|
<.city name="Kraków"/>
|
|
```
|
|
|
|
where the component could be defined as follows:
|
|
|
|
defmodule MyApp.Weather do
|
|
use Phoenix.Component
|
|
|
|
def city(assigns) do
|
|
~H"""
|
|
The chosen city is: {@name}.
|
|
"""
|
|
end
|
|
|
|
def country(assigns) do
|
|
~H"""
|
|
The chosen country is: {@name}.
|
|
"""
|
|
end
|
|
end
|
|
|
|
It is typically best to group related functions into a single module, as
|
|
opposed to having many modules with a single `render/1` function. Function
|
|
components support other important features, such as slots. You can learn
|
|
more about components in `Phoenix.Component`.
|
|
|
|
## Code formatting
|
|
|
|
You can automatically format HEEx templates (.heex) and `~H` sigils
|
|
using `Phoenix.LiveView.HTMLFormatter`. Please check that module
|
|
for more information.
|
|
'''
|
|
@doc type: :macro
|
|
defmacro sigil_H({:<<>>, meta, [expr]}, modifiers)
|
|
when modifiers == [] or modifiers == ~c"noformat" do
|
|
if not 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.TagEngine,
|
|
file: __CALLER__.file,
|
|
line: __CALLER__.line + 1,
|
|
caller: __CALLER__,
|
|
indentation: meta[:indentation] || 0,
|
|
source: expr,
|
|
tag_handler: Phoenix.LiveView.HTMLEngine
|
|
]
|
|
|
|
EEx.compile_string(expr, options)
|
|
end
|
|
|
|
@doc ~S'''
|
|
Filters the assigns as a list of keywords for use in dynamic tag attributes.
|
|
|
|
One should prefer to use declarative assigns and `:global` attributes
|
|
over this function.
|
|
|
|
## 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 to="/" id={@id} new_window={true} class="my-class">Home</.my_link>
|
|
```
|
|
|
|
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, :to])
|
|
|
|
assigns =
|
|
assigns
|
|
|> assign(:target, target)
|
|
|> assign(:extra, extra)
|
|
|
|
~H"""
|
|
<a href={@to} target={@target} {@extra}>
|
|
{render_slot(@inner_block)}
|
|
</a>
|
|
"""
|
|
end
|
|
|
|
The above would result in the following rendered HTML:
|
|
|
|
```heex
|
|
<a href="/" target="_blank" id="1" class="my-class">Home</a>
|
|
```
|
|
|
|
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.
|
|
|
|
Most other cases for shared functionality, including state management and user interactions, can be
|
|
[achieved with function components or LiveComponents](welcome.html#compartmentalize-state-markup-and-events-in-liveview)
|
|
|
|
## 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. Note that this only works for LiveViews that are in the same
|
|
[live_session](`Phoenix.LiveView.Router.live_session/3`).
|
|
If you are rendering the sticky view within another LiveView, 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"}
|
|
|
|
If you don't want the container to affect layout, you can use the CSS property
|
|
`display: contents` or a class that applies it, like Tailwind's `.contents`.
|
|
|
|
Beware if you set this to `:body`, as any content injected inside the body
|
|
(such as `Phoenix.LiveReload` features) will be discarded once the LiveView
|
|
connects
|
|
|
|
## Testing
|
|
|
|
Note that `render_click/1` and other testing functions will send events to the root LiveView, and you will want to
|
|
`find_live_child/2` to interact with nested LiveViews in your live tests.
|
|
"""
|
|
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>
|
|
|
|
<: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>
|
|
<tr>
|
|
<th :for={col <- @col}>{col.label}</th>
|
|
</tr>
|
|
<tr :for={row <- @rows}>
|
|
<td :for={col <- @col}>{render_slot(col, row)}</td>
|
|
</tr>
|
|
</table>
|
|
"""
|
|
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 %>
|
|
"""noformat
|
|
end
|
|
|
|
def __render_slot__(changed, entry, argument) when is_map(entry) do
|
|
case entry.inner_block do
|
|
%Phoenix.LiveView.Rendered{} = rendered -> rendered
|
|
fun -> fun.(changed, argument)
|
|
end
|
|
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
|
|
|
|
case entry.inner_block do
|
|
%Phoenix.LiveView.Rendered{} = rendered -> rendered
|
|
fun -> fun.(changed, argument)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns the flash message from the LiveView flash assign.
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<p class="alert alert-info">{live_flash(@flash, :info)}</p>
|
|
<p class="alert alert-danger">{live_flash(@flash, :error)}</p>
|
|
```
|
|
"""
|
|
@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 errors for the upload as a whole.
|
|
|
|
For errors that apply to a specific upload entry, use `upload_errors/2`.
|
|
|
|
The output is a list. The following error may be returned:
|
|
|
|
* `:too_many_files` - The number of selected files exceeds the `:max_entries` constraint
|
|
|
|
## Examples
|
|
|
|
def upload_error_to_string(:too_many_files), do: "You have selected too many files"
|
|
|
|
```heex
|
|
<div :for={err <- upload_errors(@uploads.avatar)} class="alert alert-danger">
|
|
{upload_error_to_string(err)}
|
|
</div>
|
|
```
|
|
"""
|
|
def upload_errors(%Phoenix.LiveView.UploadConfig{} = conf) do
|
|
for {ref, error} <- conf.errors, ref == conf.ref, do: error
|
|
end
|
|
|
|
@doc """
|
|
Returns errors for the upload entry.
|
|
|
|
For errors that apply to the upload as a whole, use `upload_errors/1`.
|
|
|
|
The output is a list. 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
|
|
* `:external_client_failure` - When external upload fails
|
|
* `{:writer_failure, reason}` - When the custom writer fails with `reason`
|
|
|
|
## Examples
|
|
|
|
```elixir
|
|
defp upload_error_to_string(:too_large), do: "The file is too large"
|
|
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
|
|
defp upload_error_to_string(:external_client_failure), do: "Something went terribly wrong"
|
|
```
|
|
|
|
```heex
|
|
<%= for entry <- @uploads.avatar.entries do %>
|
|
<div :for={err <- upload_errors(@uploads.avatar, entry)} class="alert alert-danger">
|
|
{upload_error_to_string(err)}
|
|
</div>
|
|
<% 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 sharing assigns.
|
|
We will cover both use cases next.
|
|
|
|
## Lazy assigns
|
|
|
|
Imagine you have a function component that accepts a color:
|
|
|
|
```heex
|
|
<.my_component bg_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, :bg_color, fn -> Enum.random(~w(bg-red-200 bg-green-200 bg-blue-200)) end)
|
|
|
|
~H"""
|
|
<div class={@bg_color}>
|
|
Example
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
## Sharing assigns
|
|
|
|
It is possible to share assigns between the Plug pipeline and LiveView on disconnected render
|
|
and between parent-child LiveViews when connected.
|
|
|
|
### When disconnected
|
|
|
|
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. By using `assign_new` in the mount
|
|
callback of your LiveView, you can instruct LiveView to re-use any assigns already set in `conn`
|
|
during disconnected state.
|
|
|
|
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.
|
|
|
|
### When connected
|
|
|
|
LiveView is also able to share assigns via `assign_new` with children LiveViews,
|
|
as long as the child LiveView is also mounted when the parent LiveView is mounted
|
|
and the child LiveView is not rendered with `sticky: true`. Let's see an example.
|
|
|
|
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 in
|
|
the previous subsection, 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)
|
|
|
|
Assigns sharing is performed when possible but not guaranteed. Therefore, you must
|
|
ensure the result of the function given to `assign_new/3` is the same as if the value
|
|
was fetched from the parent. Otherwise consider passing values to the child LiveView
|
|
as part of its session.
|
|
'''
|
|
def assign_new(socket_or_assigns, key, fun)
|
|
|
|
def assign_new(%Socket{} = socket, key, fun) do
|
|
validate_assign_key!(key)
|
|
Phoenix.LiveView.Utils.assign_new(socket, key, fun)
|
|
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
|
|
# force assign the key if the attribute was given with matching value
|
|
%{^key => ^value, __given__: given} when not is_map_key(given, key) ->
|
|
Phoenix.LiveView.Utils.force_assign(assigns, changed, key, value)
|
|
|
|
%{^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!(assign) when assign in @non_assignables do
|
|
raise ArgumentError,
|
|
"#{inspect(assign)} is a reserved assign by LiveView and it cannot be set directly"
|
|
end
|
|
|
|
defp validate_assign_key!(key) when is_atom(key), do: :ok
|
|
|
|
defp validate_assign_key!(key) do
|
|
raise ArgumentError, "assigns in LiveView must be atoms, got: #{inspect(key)}"
|
|
end
|
|
|
|
@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} -> Phoenix.LiveView.Utils.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
|
|
|
|
@doc """
|
|
Converts a given data structure to a `Phoenix.HTML.Form`.
|
|
|
|
This is commonly used to convert a map or an Ecto changeset
|
|
into a form to be given to the `form/1` component.
|
|
|
|
## Creating a form from params
|
|
|
|
If you want to create a form based on `handle_event` parameters,
|
|
you could do:
|
|
|
|
def handle_event("submitted", params, socket) do
|
|
{:noreply, assign(socket, form: to_form(params))}
|
|
end
|
|
|
|
When you pass a map to `to_form/1`, it assumes said map contains
|
|
the form parameters, which are expected to have string keys.
|
|
|
|
You can also specify a name to nest the parameters:
|
|
|
|
def handle_event("submitted", %{"user" => user_params}, socket) do
|
|
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
|
|
end
|
|
|
|
## Creating a form from changesets
|
|
|
|
When using changesets, the underlying data, form parameters, and
|
|
errors are retrieved from it. The `:as` option is automatically
|
|
computed too. For example, if you have a user schema:
|
|
|
|
defmodule MyApp.Users.User do
|
|
use Ecto.Schema
|
|
|
|
schema "..." do
|
|
...
|
|
end
|
|
end
|
|
|
|
And then you create a changeset that you pass to `to_form`:
|
|
|
|
%MyApp.Users.User{}
|
|
|> Ecto.Changeset.change()
|
|
|> to_form()
|
|
|
|
In this case, once the form is submitted, the parameters will
|
|
be available under `%{"user" => user_params}`.
|
|
|
|
## Options
|
|
|
|
* `:as` - the `name` prefix to be used in form inputs
|
|
* `:id` - the `id` prefix to be used in form inputs
|
|
* `:errors` - keyword list of errors (used by maps exclusively)
|
|
* `:action` - The action that was taken against the form. This value can be
|
|
used to distinguish between different operations such as the user typing
|
|
into a form for validation, or submitting a form for a database insert.
|
|
For example: `to_form(changeset, action: :validate)`,
|
|
or `to_form(changeset, action: :save)`. The provided action is passed
|
|
to the underlying `Phoenix.HTML.FormData` implementation options.
|
|
|
|
The underlying data may accept additional options when
|
|
converted to forms. For example, a map accepts `:errors`
|
|
to list errors, but such option is not accepted by
|
|
changesets. `:errors` is a keyword of tuples in the shape
|
|
of `{error_message, options_list}`. Here is an example:
|
|
|
|
to_form(%{"search" => nil}, errors: [search: {"Can't be blank", []}])
|
|
|
|
If an existing `Phoenix.HTML.Form` struct is given, the
|
|
options above will override its existing values if given.
|
|
Then the remaining options are merged with the existing
|
|
form options.
|
|
|
|
Errors in a form are only displayed if the changeset's `action`
|
|
field is set (and it is not set to `:ignore`) and can be filtered
|
|
by whether the fields have been used on the client or not. Refer to
|
|
[a note on :errors for more information](#form/1-a-note-on-errors).
|
|
"""
|
|
def to_form(data_or_params, options \\ [])
|
|
|
|
def to_form(%Phoenix.HTML.Form{} = data, []) do
|
|
data
|
|
end
|
|
|
|
def to_form(%Phoenix.HTML.Form{} = data, options) do
|
|
data =
|
|
case Keyword.fetch(options, :as) do
|
|
{:ok, as} ->
|
|
name = if as == nil, do: as, else: to_string(as)
|
|
%{data | name: name, id: Keyword.get(options, :id) || name}
|
|
|
|
:error ->
|
|
case Keyword.fetch(options, :id) do
|
|
{:ok, id} -> %{data | id: id}
|
|
:error -> data
|
|
end
|
|
end
|
|
|
|
{options, data} =
|
|
Enum.reduce(options, {data.options, data}, fn
|
|
{:as, _as}, {options, data} -> {options, data}
|
|
{:action, action}, {options, data} -> {options, %{data | action: action}}
|
|
{:errors, errors}, {options, data} -> {options, %{data | errors: errors}}
|
|
{key, value}, {options, data} -> {[{key, value} | Keyword.delete(options, key)], data}
|
|
end)
|
|
|
|
%{data | options: options}
|
|
end
|
|
|
|
def to_form(data, options) do
|
|
if is_atom(data) do
|
|
IO.warn("""
|
|
Passing an atom to "for" in the form component is deprecated.
|
|
Instead of:
|
|
|
|
<.form :let={f} for={#{inspect(data)}} ...>
|
|
|
|
You might do:
|
|
|
|
<.form :let={f} for={%{}} as={#{inspect(data)}} ...>
|
|
|
|
Or, if you prefer, use to_form to create a form in your LiveView:
|
|
|
|
assign(socket, form: to_form(%{}, as: #{inspect(data)}))
|
|
|
|
and then use it in your templates (no :let required):
|
|
|
|
<.form for={@form}>
|
|
""")
|
|
end
|
|
|
|
Phoenix.HTML.FormData.to_form(data, options)
|
|
end
|
|
|
|
@doc """
|
|
Checks if the input field was used by the client.
|
|
|
|
Used inputs are only those inputs that have been focused, interacted with, or
|
|
submitted by the client. For LiveView, this is used to filter errors from the
|
|
`Phoenix.HTML.FormData` implementation to avoid showing "field can't be blank"
|
|
in scenarios where the client hasn't yet interacted with specific fields.
|
|
|
|
Used inputs are tracked internally by the client sending a sibling key
|
|
derived from each input name, which indicates the inputs that remain unused
|
|
on the client. For example, a form with email and title fields where only the
|
|
title has been modified so far on the client, would send the following payload:
|
|
|
|
%{
|
|
"title" => "new title",
|
|
"email" => "",
|
|
"_unused_email" => ""
|
|
}
|
|
|
|
The `_unused_email` key indicates that the email field has not been used by the
|
|
client, which is used to filter errors from the UI.
|
|
|
|
Nested fields are also supported. For example, a form with a nested datetime field
|
|
is considered used if any of the nested parameters are used.
|
|
|
|
%{
|
|
"bday" => %{
|
|
"year" => "",
|
|
"month" => "",
|
|
"day" => "",
|
|
"_unused_day" => ""
|
|
}
|
|
}
|
|
|
|
The `_unused_day` key indicates that the day field has not been used by the client,
|
|
but the year and month fields have been used, meaning the birthday field as a whole
|
|
was used.
|
|
|
|
## Examples
|
|
|
|
For example, imagine in your template you render a title and email input.
|
|
On initial load the end-user begins typing the title field. The client will send
|
|
the entire form payload to the server with the typed title and an empty email.
|
|
|
|
The `Phoenix.HTML.FormData` implementation will consider an empty email in
|
|
this scenario as invalid, but the user shouldn't see the error because they
|
|
haven't yet used the email input. To handle this, `used_input?/1` can be used to
|
|
filter errors from the client by referencing param metadata to distinguish between
|
|
used and unused input fields. For non-LiveViews, all inputs are considered used.
|
|
|
|
```heex
|
|
<input type="text" name={@form[:title].name} value={@form[:title].value} />
|
|
|
|
<div :if={used_input?(@form[:title])}>
|
|
<p :for={error <- @form[:title].errors}>{error}</p>
|
|
</div>
|
|
|
|
<input type="text" name={@form[:email].name} value={@form[:email].value} />
|
|
|
|
<div :if={used_input?(@form[:email])}>
|
|
<p :for={error <- @form[:email].errors}>{error}</p>
|
|
</div>
|
|
```
|
|
"""
|
|
def used_input?(%Phoenix.HTML.FormField{field: field, form: form}) do
|
|
used_param?(form.params, field)
|
|
end
|
|
|
|
defp used_param?(_params, "_unused_" <> _), do: false
|
|
|
|
defp used_param?(params, field) do
|
|
field_str = "#{field}"
|
|
unused_field_str = "_unused_#{field}"
|
|
|
|
case params do
|
|
%{^field_str => _, ^unused_field_str => _} ->
|
|
false
|
|
|
|
%{^field_str => %{} = nested} when not is_struct(nested) ->
|
|
Enum.any?(Map.keys(nested), &used_param?(nested, &1))
|
|
|
|
%{^field_str => _val} ->
|
|
true
|
|
|
|
%{} ->
|
|
false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Embeds external template files into the module as function components.
|
|
|
|
## Options
|
|
|
|
* `:root` - The root directory to embed files. Defaults to the current
|
|
module's directory (`__DIR__`)
|
|
* `:suffix` - A string value to append to embedded function names. By
|
|
default, function names will be the name of the template file excluding
|
|
the format and engine.
|
|
|
|
A wildcard pattern may be used to select all files within a directory tree.
|
|
For example, imagine a directory listing:
|
|
|
|
```plain
|
|
├── components.ex
|
|
├── pages
|
|
│ ├── about_page.html.heex
|
|
│ └── welcome_page.html.heex
|
|
```
|
|
|
|
Then to embed the page templates in your `components.ex` module:
|
|
|
|
defmodule MyAppWeb.Components do
|
|
use Phoenix.Component
|
|
|
|
embed_templates "pages/*"
|
|
end
|
|
|
|
Now, your module will have an `about_page/1` and `welcome_page/1` function
|
|
component defined. Embedded templates also support declarative assigns
|
|
via bodyless function definitions, for example:
|
|
|
|
defmodule MyAppWeb.Components do
|
|
use Phoenix.Component
|
|
|
|
embed_templates "pages/*"
|
|
|
|
attr :name, :string, required: true
|
|
def welcome_page(assigns)
|
|
|
|
slot :header
|
|
def about_page(assigns)
|
|
end
|
|
|
|
Multiple invocations of `embed_templates` is also supported, which can be
|
|
useful if you have more than one template format. For example:
|
|
|
|
defmodule MyAppWeb.Emails do
|
|
use Phoenix.Component
|
|
|
|
embed_templates "emails/*.html", suffix: "_html"
|
|
embed_templates "emails/*.text", suffix: "_text"
|
|
end
|
|
|
|
Note: this function is the same as `Phoenix.Template.embed_templates/2`.
|
|
It is also provided here for convenience and documentation purposes.
|
|
Therefore, if you want to embed templates for other formats, which are
|
|
not related to `Phoenix.Component`, prefer to
|
|
`import Phoenix.Template, only: [embed_templates: 1]` than this module.
|
|
"""
|
|
@doc type: :macro
|
|
defmacro embed_templates(pattern, opts \\ []) do
|
|
quote bind_quoted: [pattern: pattern, opts: opts] do
|
|
Phoenix.Template.compile_all(
|
|
&Phoenix.Component.__embed__(&1, opts[:suffix]),
|
|
Path.expand(opts[:root] || __DIR__, __DIR__),
|
|
pattern
|
|
)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def __embed__(path, suffix),
|
|
do:
|
|
path
|
|
|> Path.basename()
|
|
|> Path.rootname()
|
|
|> Path.rootname()
|
|
|> Kernel.<>(suffix || "")
|
|
|
|
## 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
|
|
require Phoenix.Template
|
|
|
|
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 `[]`.
|
|
|
|
* `:validate_attrs` - when set to `false`, no warning is emitted when a caller passes attributes
|
|
to a slot defined without a do block. If not set, defaults to `true`.
|
|
|
|
* `: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"""
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
{render_slot(@header) || "Modal"}
|
|
</div>
|
|
<div class="modal-body">
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
<div class="modal-footer">
|
|
{render_slot(@footer) || submit_button()}
|
|
</div>
|
|
</div>
|
|
"""
|
|
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.
|
|
'''
|
|
@doc type: :macro
|
|
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.
|
|
"""
|
|
@doc type: :macro
|
|
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 (including `nil`) |
|
|
| `: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 |
|
|
| `:map` | any map of any arbitrary types |
|
|
| `:fun` | any function |
|
|
| `{:fun, arity}` | any function of arity |
|
|
| `:global` | any common HTML attributes, plus those defined by `:global_prefixes` |
|
|
| A struct module | any module that defines a struct with `defstruct/1` |
|
|
|
|
Note only `:any` and `:atom` expect the value to be set to `nil`.
|
|
|
|
### 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`, `:map` 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.
|
|
|
|
* 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"""
|
|
<p>
|
|
Happy birthday {@name}!
|
|
You are {@age} years old.
|
|
</p>
|
|
"""
|
|
end
|
|
'''
|
|
@doc type: :macro
|
|
defmacro attr(name, type, opts \\ []) do
|
|
type = Macro.expand_literals(type, %{__CALLER__ | function: {:attr, 3}})
|
|
opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:attr, 3}})
|
|
|
|
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
|
|
|
|
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 LiveViews 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)
|
|
|
|
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
|
|
|
|
@doc """
|
|
Renders a title with automatic prefix/suffix on `@page_title` updates.
|
|
|
|
[INSERT LVATTRDOCS]
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.live_title default="Welcome" prefix="MyApp · ">
|
|
{assigns[:page_title]}
|
|
</.live_title>
|
|
```
|
|
|
|
```heex
|
|
<.live_title default="Welcome" suffix=" · MyApp">
|
|
{assigns[:page_title]}
|
|
</.live_title>
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
attr.(:prefix, :string,
|
|
default: nil,
|
|
doc: "A prefix added before the content of `inner_block`."
|
|
)
|
|
|
|
attr.(:default, :string,
|
|
default: nil,
|
|
doc:
|
|
"The default title to use if the inner block is empty on regular or connected mounts." <>
|
|
" *Note*: empty titles, such as `nil` or an empty string, fall back to the default value."
|
|
)
|
|
|
|
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"""
|
|
<title data-prefix={@prefix} data-default={@default} data-suffix={@suffix} phx-no-format>{@prefix}{render_present(render_slot(@inner_block), @default)}{@suffix}</title>
|
|
"""
|
|
end
|
|
|
|
defp render_present(rendered_block, default) do
|
|
block_str =
|
|
rendered_block
|
|
|> Phoenix.HTML.html_escape()
|
|
|> Phoenix.HTML.safe_to_string()
|
|
|
|
if String.trim(block_str) == "" do
|
|
default
|
|
else
|
|
rendered_block
|
|
end
|
|
end
|
|
|
|
@doc ~S'''
|
|
Renders a form.
|
|
|
|
This function receives a `Phoenix.HTML.Form` struct, generally created with
|
|
`to_form/2`, and generates the relevant form tags. It can be used either
|
|
inside LiveView or outside.
|
|
|
|
> To see how forms work in practice, you can run
|
|
> `mix phx.gen.live Blog Post posts title body:text` inside your Phoenix
|
|
> application, which will setup the necessary database tables and LiveViews
|
|
> to manage your data.
|
|
|
|
## Examples: inside LiveView
|
|
|
|
Inside LiveViews, this function component is typically called with
|
|
as `for={@form}`, where `@form` is the result of the `to_form/1` function.
|
|
`to_form/1` expects either a map or an [`Ecto.Changeset`](https://hexdocs.pm/ecto/Ecto.Changeset.html)
|
|
as the source of data and normalizes it into `Phoenix.HTML.Form` structure.
|
|
|
|
For example, you may use the parameters received in a
|
|
`c:Phoenix.LiveView.handle_event/3` callback to create an Ecto changeset
|
|
and then use `to_form/1` to convert it to a form. Then, in your templates,
|
|
you pass the `@form` as argument to `:for`:
|
|
|
|
```heex
|
|
<.form
|
|
for={@form}
|
|
phx-change="change_name"
|
|
>
|
|
<.input field={@form[:email]} />
|
|
</.form>
|
|
```
|
|
|
|
The `.input` component is generally defined as part of your own application
|
|
and adds all styling necessary:
|
|
|
|
```heex
|
|
def input(assigns) do
|
|
~H"""
|
|
<input type="text" name={@field.name} id={@field.id} value={@field.value} class="..." />
|
|
"""
|
|
end
|
|
```
|
|
|
|
A form accepts multiple options. For example, if you are doing file uploads
|
|
and you want to capture submissions, you might write instead:
|
|
|
|
```heex
|
|
<.form
|
|
for={@form}
|
|
multipart
|
|
phx-change="change_user"
|
|
phx-submit="save_user"
|
|
>
|
|
...
|
|
<input type="submit" value="Save" />
|
|
</.form>
|
|
```
|
|
|
|
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.
|
|
|
|
### Using the `for` attribute
|
|
|
|
The `for` attribute can also be a map or an Ecto.Changeset. In such cases,
|
|
a form will be created on the fly, and you can capture it using `:let`:
|
|
|
|
```heex
|
|
<.form
|
|
:let={form}
|
|
for={@changeset}
|
|
phx-change="change_user"
|
|
>
|
|
```
|
|
|
|
However, such approach is discouraged in LiveView for two reasons:
|
|
|
|
* LiveView can better optimize your code if you access the form fields
|
|
using `@form[:field]` rather than through the let-variable `form`
|
|
|
|
* Ecto changesets are meant to be single use. By never storing the changeset
|
|
in the assign, you will be less tempted to use it across operations
|
|
|
|
### A note on `:errors`
|
|
|
|
Even if `changeset.errors` is non-empty, errors will not be displayed in a
|
|
form if [the changeset
|
|
`:action`](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-changeset-actions)
|
|
is `nil` or `:ignore`.
|
|
|
|
This is useful for things like validation hints on form fields, e.g. an empty
|
|
changeset for a new form. That changeset isn't valid, but we don't want to
|
|
show errors until an actual user action has been performed.
|
|
|
|
For example, if the user submits and a `Repo.insert/1` is called and fails on
|
|
changeset validation, the action will be set to `:insert` to show that an
|
|
insert was attempted, and the presence of that action will cause errors to be
|
|
displayed. The same is true for Repo.update/delete.
|
|
|
|
Error visibility is handled by providing the action to `to_form/2`, which will
|
|
set the underlying changeset action. You can also set the action manually by
|
|
directly updating on the `Ecto.Changeset` struct field, or by using
|
|
`Ecto.Changeset.apply_action/2`. Since the action can be arbitrary, you can
|
|
set it to `:validate` or anything else to avoid giving the impression that a
|
|
database operation has actually been attempted.
|
|
|
|
### Displaying errors on used and unused input fields
|
|
|
|
Used inputs are only those inputs that have been focused, interacted with, or
|
|
submitted by the client. In most cases, a user shouldn't receive error feedback
|
|
for forms they haven't yet interacted with, until they submit the form. Filtering
|
|
the errors based on used input fields can be done with `used_input?/1`.
|
|
|
|
## Example: outside LiveView (regular HTTP requests)
|
|
|
|
The `form` component can still be used to submit forms outside of LiveView.
|
|
In such cases, the standard HTML `action` attribute MUST be given.
|
|
Without said attribute, the `form` method and csrf token are discarded.
|
|
|
|
```heex
|
|
<.form :let={f} for={@changeset} action={~p"/comments/#{@comment}"}>
|
|
<.input field={f[:body]} />
|
|
</.form>
|
|
```
|
|
|
|
In the example above, we passed a changeset to `for` and captured
|
|
the value using `:let={f}`. This approach is ok outside of LiveViews,
|
|
as there are no change tracking optimizations to consider.
|
|
|
|
### CSRF protection
|
|
|
|
CSRF protection is a mechanism to ensure that the user who rendered
|
|
the form is the one actually submitting it. This module generates a
|
|
CSRF token by default. Your application should check this token on
|
|
the server to avoid attackers from making requests on your server on
|
|
behalf of other users. Phoenix by default checks this token.
|
|
|
|
When posting a form with a host in its address, such as "//host.com/path"
|
|
instead of only "/path", Phoenix will include the host signature in the
|
|
token and validate the token only if the accessed host is the same as
|
|
the host in the token. This is to avoid tokens from leaking to third
|
|
party applications. If this behaviour is problematic, you can generate
|
|
a non-host specific token with `Plug.CSRFProtection.get_csrf_token/0` and
|
|
pass it to the form generator via the `:csrf_token` option.
|
|
|
|
[INSERT LVATTRDOCS]
|
|
'''
|
|
@doc type: :component
|
|
attr.(:for, :any, required: true, doc: "An existing form or the form source data.")
|
|
|
|
attr.(:action, :string,
|
|
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,
|
|
doc: """
|
|
The prefix to be used in names and IDs generated by the form.
|
|
For example, setting `as: :user_params` means the parameters
|
|
will be nested "user_params" in your `handle_event` or
|
|
`conn.params["user_params"]` for regular HTTP requests.
|
|
If you set this option, you must capture the form with `:let`.
|
|
"""
|
|
)
|
|
|
|
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,
|
|
doc: """
|
|
Use this to manually pass a keyword list of errors to the form.
|
|
This option is useful when a regular map is given as the form
|
|
source and it will make the errors available under `f.errors`.
|
|
If you set this option, you must capture the form with `:let`.
|
|
"""
|
|
)
|
|
|
|
attr.(:method, :string,
|
|
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.
|
|
If an `:action` is given with no method, the method will default to the return value
|
|
of `Phoenix.HTML.FormData.to_form/2` (usually `post`).
|
|
"""
|
|
)
|
|
|
|
attr.(:multipart, :boolean,
|
|
default: false,
|
|
doc: """
|
|
Sets `enctype` to `multipart/form-data`.
|
|
Required when uploading files.
|
|
"""
|
|
)
|
|
|
|
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
|
|
action = assigns[:action]
|
|
|
|
# We require for={...} to be given but we automatically handle nils for convenience
|
|
form_for =
|
|
case assigns[:for] do
|
|
nil -> %{}
|
|
other -> other
|
|
end
|
|
|
|
form_options =
|
|
assigns
|
|
|> Map.take([:as, :csrf_token, :errors, :method, :multipart])
|
|
|> Map.merge(assigns.rest)
|
|
|> Map.to_list()
|
|
|
|
# Since FormData may add options, read the actual options from form
|
|
%{options: opts} = form = to_form(form_for, form_options)
|
|
|
|
# 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"""
|
|
<form {@attrs}>
|
|
<input
|
|
:if={@hidden_method && @hidden_method not in ~w(get post)}
|
|
name="_method"
|
|
type="hidden"
|
|
hidden
|
|
value={@hidden_method}
|
|
/>
|
|
<input :if={@csrf_token} name="_csrf_token" type="hidden" hidden value={@csrf_token} />
|
|
{render_slot(@inner_block, @form)}
|
|
</form>
|
|
"""
|
|
end
|
|
|
|
defp form_method(nil), do: {"post", nil}
|
|
|
|
defp form_method(method) when is_binary(method) do
|
|
case String.downcase(method) do
|
|
method when method in ~w(get post) -> {method, nil}
|
|
_ -> {"post", method}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Renders nested form inputs for associations or embeds.
|
|
|
|
[INSERT LVATTRDOCS]
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.form
|
|
for={@form}
|
|
phx-change="change_name"
|
|
>
|
|
<.inputs_for :let={f_nested} field={@form[:nested]}>
|
|
<.input type="text" field={f_nested[:name]} />
|
|
</.inputs_for>
|
|
</.form>
|
|
```
|
|
|
|
## Dynamically adding and removing inputs
|
|
|
|
Dynamically adding and removing inputs is supported by rendering named buttons for
|
|
inserts and removals. Like inputs, buttons with name/value pairs are serialized with
|
|
form data on change and submit events. Libraries such as Ecto, or custom param
|
|
filtering can then inspect the parameters and handle the added or removed fields.
|
|
This can be combined with `Ecto.Changeset.cast_assoc/3`'s `:sort_param` and `:drop_param`
|
|
options. For example, imagine a parent with an `:emails` `has_many` or `embeds_many`
|
|
association. To cast the user input from a nested form, one simply needs to configure
|
|
the options:
|
|
|
|
schema "mailing_lists" do
|
|
field :title, :string
|
|
|
|
embeds_many :emails, EmailNotification, on_replace: :delete do
|
|
field :email, :string
|
|
field :name, :string
|
|
end
|
|
end
|
|
|
|
def changeset(list, attrs) do
|
|
list
|
|
|> cast(attrs, [:title])
|
|
|> cast_embed(:emails,
|
|
with: &email_changeset/2,
|
|
sort_param: :emails_sort,
|
|
drop_param: :emails_drop
|
|
)
|
|
end
|
|
|
|
Here we see the `:sort_param` and `:drop_param` options in action.
|
|
|
|
> Note: `on_replace: :delete` on the `has_many` and `embeds_many` is required
|
|
> when using these options.
|
|
|
|
When Ecto sees the specified sort or drop parameter from the form, it will sort
|
|
the children based on the order they appear in the form, add new children it hasn't
|
|
seen, or drop children if the parameter instructs it to do so.
|
|
|
|
The markup for such a schema and association would look like this:
|
|
|
|
```heex
|
|
<.inputs_for :let={ef} field={@form[:emails]}>
|
|
<input type="hidden" name="mailing_list[emails_sort][]" value={ef.index} />
|
|
<.input type="text" field={ef[:email]} placeholder="email" />
|
|
<.input type="text" field={ef[:name]} placeholder="name" />
|
|
<button
|
|
type="button"
|
|
name="mailing_list[emails_drop][]"
|
|
value={ef.index}
|
|
phx-click={JS.dispatch("change")}
|
|
>
|
|
<.icon name="hero-x-mark" class="w-6 h-6 relative top-2" />
|
|
</button>
|
|
</.inputs_for>
|
|
|
|
<input type="hidden" name="mailing_list[emails_drop][]" />
|
|
|
|
<button type="button" name="mailing_list[emails_sort][]" value="new" phx-click={JS.dispatch("change")}>
|
|
add more
|
|
</button>
|
|
```
|
|
|
|
We used `inputs_for` to render inputs for the `:emails` association, which
|
|
contains an email address and name input for each child. Within the nested inputs,
|
|
we render a hidden `mailing_list[emails_sort][]` input, which is set to the index of the
|
|
given child. This tells Ecto's cast operation how to sort existing children, or
|
|
where to insert new children. Next, we render the email and name inputs as usual.
|
|
Then we render a button containing the "delete" text with the name `mailing_list[emails_drop][]`,
|
|
containing the index of the child as its value.
|
|
|
|
Like before, this tells Ecto to delete the child at this index when the button is
|
|
clicked. We use `phx-click={JS.dispatch("change")}` on the button to tell LiveView
|
|
to treat this button click as a change event, rather than a submit event on the form,
|
|
which invokes our form's `phx-change` binding.
|
|
|
|
Outside the `inputs_for`, we render an empty `mailing_list[emails_drop][]` input,
|
|
to ensure that all children are deleted when saving a form where the user
|
|
dropped all entries. This hidden input is required whenever dropping associations.
|
|
|
|
Finally, we also render another button with the sort param name `mailing_list[emails_sort][]`
|
|
and `value="new"` name with accompanied "add more" text. Please note that this button must
|
|
have `type="button"` to prevent it from submitting the form.
|
|
Ecto will treat unknown sort params as new children and build a new child.
|
|
This button is optional and only necessary if you want to dynamically add entries.
|
|
You can optionally add a similar button before the `<.inputs_for>`, in the case you want
|
|
to prepend entries.
|
|
|
|
> ### A note on accessing a field's `value` {: .warning}
|
|
>
|
|
> You may be tempted to access `form[:field].value` or attempt to manipulate
|
|
> the form metadata in your templates. However, bear in mind that the `form[:field]`
|
|
> value reflects the most recent changes. For example, an `:integer` field may
|
|
> either contain integer values, but it may also hold a string, if the form has
|
|
> been submitted.
|
|
>
|
|
> This is particularly noticeable when using `inputs_for`. Accessing the `.value`
|
|
> of a nested field may either return a struct, a changeset, or raw parameters
|
|
> sent by the client (when using `drop_param`). This makes the `form[:field].value`
|
|
> impractical for deriving or computing other properties.
|
|
>
|
|
> The correct way to approach this problem is by computing any property either in
|
|
> your LiveViews, by traversing the relevant changesets and data structures, or by
|
|
> moving the logic to the `Ecto.Changeset` itself.
|
|
>
|
|
> As an example, imagine you are building a time tracking application where:
|
|
>
|
|
> - users enter the total work time for a day
|
|
> - individual activities are tracked as embeds
|
|
> - the sum of all activities should match the total time
|
|
> - the form should display the remaining time
|
|
>
|
|
> Instead of trying to calculate the remaining time in your template by
|
|
> doing something like `calculate_remaining(@form)` and accessing
|
|
> `form[:activities].value`, calculate the remaining time based
|
|
> on the changeset in your `handle_event` instead:
|
|
>
|
|
> ```elixir
|
|
> def handle_event("validate", %{"tracked_day" => params}, socket) do
|
|
> changeset = TrackedDay.changeset(socket.assigns.tracked_day, params)
|
|
> remaining = calculate_remaining(changeset)
|
|
> {:noreply, assign(socket, form: to_form(changeset, action: :validate), remaining: remaining)}
|
|
> end
|
|
>
|
|
> # Helper function to calculate remaining time
|
|
> defp calculate_remaining(changeset) do
|
|
> total = Ecto.Changeset.get_field(changeset, :total)
|
|
> activities = Ecto.Changeset.get_embed(changeset, :activities)
|
|
>
|
|
> Enum.reduce(activities, total, fn activity, acc ->
|
|
> duration =
|
|
> case activity do
|
|
> %{valid?: true} = changeset -> Ecto.Changeset.get_field(changeset, :duration)
|
|
> # if the activity is invalid, we don't include its duration in the calculation
|
|
> _ -> 0
|
|
> end
|
|
>
|
|
> acc - length
|
|
> end)
|
|
> end
|
|
> ```
|
|
>
|
|
> This logic might also be implemented directly in your schema module and, if you
|
|
> often need the `:remaining` value, you could also add it as a `:virtual` field to
|
|
> your schema and run the calculation when validating the changeset:
|
|
>
|
|
> ```elixir
|
|
> def changeset(tracked_day, attrs) do
|
|
> tracked_day
|
|
> |> cast(attrs, [:total_duration])
|
|
> |> cast_embed(:activities)
|
|
> |> validate_required([:total_duration])
|
|
> |> validate_number(:total_duration, greater_than: 0)
|
|
> |> validate_and_put_remaining_time()
|
|
> end
|
|
>
|
|
> defp validate_and_put_remaining_time(changeset) do
|
|
> remaining = calculate_remaining(changeset)
|
|
> put_change(changeset, :remaining, remaining)
|
|
> end
|
|
> ```
|
|
>
|
|
> By using this approach, you can safely render the remaining time in your template
|
|
> using `@form[:remaining].value`, avoiding the pitfalls of directly accessing complex field values.
|
|
"""
|
|
@doc type: :component
|
|
attr.(:field, Phoenix.HTML.FormField,
|
|
required: true,
|
|
doc: "A %Phoenix.HTML.Form{}/field name tuple, for example: {@form[:email]}."
|
|
)
|
|
|
|
attr.(:id, :string,
|
|
doc: """
|
|
The id base to be used in the form inputs. Defaults to the parent form id. The computed
|
|
id will be the concatenation of the base id with the field name, along with a book keeping
|
|
index for each input in the list.
|
|
"""
|
|
)
|
|
|
|
attr.(:as, :atom,
|
|
doc: """
|
|
The name to be used in the form, defaults to the concatenation of the given
|
|
field to the parent form name.
|
|
"""
|
|
)
|
|
|
|
attr.(:default, :any, doc: "The value to use if none is available.")
|
|
|
|
attr.(:prepend, :list,
|
|
doc: """
|
|
The values to prepend when rendering. This only applies if the field value
|
|
is a list and no parameters were sent through the form.
|
|
"""
|
|
)
|
|
|
|
attr.(:append, :list,
|
|
doc: """
|
|
The values to append when rendering. This only applies if the field value
|
|
is a list and no parameters were sent through the form.
|
|
"""
|
|
)
|
|
|
|
attr.(:skip_hidden, :boolean,
|
|
default: false,
|
|
doc: """
|
|
Skip the automatic rendering of hidden fields to allow for more tight control
|
|
over the generated markup.
|
|
"""
|
|
)
|
|
|
|
attr.(:skip_persistent_id, :boolean,
|
|
default: false,
|
|
doc: """
|
|
Skip the automatic rendering of hidden _persistent_id fields used for reordering
|
|
inputs.
|
|
"""
|
|
)
|
|
|
|
attr.(:options, :list,
|
|
default: [],
|
|
doc: """
|
|
Any additional options for the `Phoenix.HTML.FormData` protocol
|
|
implementation.
|
|
"""
|
|
)
|
|
|
|
slot.(:inner_block, required: true, doc: "The content rendered for each nested form.")
|
|
|
|
@persistent_id "_persistent_id"
|
|
def inputs_for(assigns) do
|
|
%Phoenix.HTML.FormField{field: field_name, form: parent_form} = assigns.field
|
|
options = assigns |> Map.take([:id, :as, :default, :append, :prepend]) |> Keyword.new()
|
|
|
|
options =
|
|
parent_form.options
|
|
|> Keyword.take([:multipart])
|
|
|> Keyword.merge(options)
|
|
|> Keyword.merge(assigns.options)
|
|
|
|
forms = parent_form.impl.to_form(parent_form.source, parent_form, field_name, options)
|
|
|
|
forms =
|
|
case assigns do
|
|
%{skip_persistent_id: true} ->
|
|
forms
|
|
|
|
_ ->
|
|
apply_persistent_id(
|
|
parent_form,
|
|
forms,
|
|
field_name,
|
|
options
|
|
)
|
|
end
|
|
|
|
assigns = assign(assigns, :forms, forms)
|
|
|
|
~H"""
|
|
<%= for finner <- @forms do %>
|
|
<%= if !@skip_hidden do %>
|
|
<%= for {name, value_or_values} <- finner.hidden,
|
|
name = name_for_value_or_values(finner, name, value_or_values),
|
|
value <- List.wrap(value_or_values) do %>
|
|
<input type="hidden" name={name} value={value} />
|
|
<% end %>
|
|
<% end %>
|
|
{render_slot(@inner_block, finner)}
|
|
<% end %>
|
|
"""
|
|
end
|
|
|
|
defp apply_persistent_id(parent_form, forms, field_name, options) do
|
|
seen_ids = for f <- forms, vid = f.params[@persistent_id], into: %{}, do: {vid, true}
|
|
|
|
{forms, _} =
|
|
Enum.map_reduce(forms, {seen_ids, 0}, fn
|
|
%Phoenix.HTML.Form{params: params} = form, {seen_ids, index} ->
|
|
id =
|
|
case params do
|
|
%{@persistent_id => id} -> id
|
|
%{} -> next_id(map_size(seen_ids), seen_ids)
|
|
end
|
|
|
|
form_id =
|
|
if inputs_for_id = options[:id] do
|
|
"#{inputs_for_id}_#{field_name}_#{id}"
|
|
else
|
|
"#{parent_form.id}_#{field_name}_#{id}"
|
|
end
|
|
|
|
new_params = Map.put(params, @persistent_id, id)
|
|
new_hidden = [{@persistent_id, id} | form.hidden]
|
|
|
|
new_form = %{
|
|
form
|
|
| id: form_id,
|
|
params: new_params,
|
|
hidden: new_hidden,
|
|
index: index
|
|
}
|
|
|
|
{new_form, {Map.put(seen_ids, id, true), index + 1}}
|
|
end)
|
|
|
|
forms
|
|
end
|
|
|
|
defp next_id(idx, %{} = seen_ids) do
|
|
id_str = to_string(idx)
|
|
|
|
if Map.has_key?(seen_ids, id_str) do
|
|
next_id(idx + 1, seen_ids)
|
|
else
|
|
id_str
|
|
end
|
|
end
|
|
|
|
defp name_for_value_or_values(form, field, values) when is_list(values) do
|
|
Phoenix.HTML.Form.input_name(form, field) <> "[]"
|
|
end
|
|
|
|
defp name_for_value_or_values(form, field, _value) do
|
|
Phoenix.HTML.Form.input_name(form, field)
|
|
end
|
|
|
|
@doc """
|
|
Generates a link to a given route.
|
|
|
|
It is typically used with one of the three attributes:
|
|
|
|
* `patch` - on click, it patches the current LiveView with the given path
|
|
* `navigate` - on click, it navigates to a new LiveView at the given path
|
|
* `href` - on click, it performs traditional browser navigation (as any `<a>` tag)
|
|
|
|
[INSERT LVATTRDOCS]
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.link href="/">Regular anchor link</.link>
|
|
```
|
|
|
|
```heex
|
|
<.link navigate={~p"/"} class="underline">home</.link>
|
|
```
|
|
|
|
```heex
|
|
<.link navigate={~p"/?sort=asc"} replace={false}>
|
|
Sort By Price
|
|
</.link>
|
|
```
|
|
|
|
```heex
|
|
<.link patch={~p"/details"}>view details</.link>
|
|
```
|
|
|
|
```heex
|
|
<.link href={URI.parse("https://elixir-lang.org")}>hello</.link>
|
|
```
|
|
|
|
```heex
|
|
<.link href="/the_world" method="delete" data-confirm="Really?">delete</.link>
|
|
```
|
|
|
|
## 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 its 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
|
|
// Compared to a javascript window.confirm, the custom dialog does not block
|
|
// javascript execution. Therefore to make this work as expected we store
|
|
// the successful confirmation as an attribute and re-trigger the click event.
|
|
// On the second click, the `data-confirm-resolved` attribute is set and we proceed.
|
|
const RESOLVED_ATTRIBUTE = "data-confirm-resolved";
|
|
// 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; }
|
|
|
|
// Confirm is resolved execute the click event
|
|
if (e.target?.hasAttribute(RESOLVED_ATTRIBUTE)) {
|
|
e.target.removeAttribute(RESOLVED_ATTRIBUTE);
|
|
return;
|
|
}
|
|
|
|
// Confirm is needed, preventDefault and show your modal
|
|
e.preventDefault();
|
|
e.target?.setAttribute(RESOLVED_ATTRIBUTE, "");
|
|
|
|
vex.dialog.confirm({
|
|
message: message,
|
|
callback: function (value) {
|
|
if (value == true) {
|
|
// Customer confirmed, re-trigger the click event.
|
|
e.target?.click();
|
|
} else {
|
|
// Customer canceled
|
|
e.target?.removeAttribute(RESOLVED_ATTRIBUTE);
|
|
}
|
|
}
|
|
})
|
|
}, 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 to a LiveView.
|
|
When redirecting across LiveViews, the browser page is kept, but a new LiveView process
|
|
is mounted and its contents is loaded on the page. It is only possible to navigate
|
|
between LiveViews declared under the same router
|
|
[`live_session`](`Phoenix.LiveView.Router.live_session/3`).
|
|
When used outside of a LiveView or across live sessions, it behaves like a regular
|
|
browser redirect.
|
|
"""
|
|
)
|
|
|
|
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. This is intended for usage outside of LiveView
|
|
and therefore only works with the `href={...}` attribute. It has no effect on `patch`
|
|
and `navigate` instructions.
|
|
|
|
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, :any,
|
|
default: true,
|
|
doc: """
|
|
A boolean or 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"""
|
|
<a
|
|
href={@navigate}
|
|
data-phx-link="redirect"
|
|
data-phx-link-state={if @replace, do: "replace", else: "push"}
|
|
phx-no-format
|
|
{@rest}
|
|
>{render_slot(@inner_block)}</a>
|
|
"""
|
|
end
|
|
|
|
def link(%{patch: to} = assigns) when is_binary(to) do
|
|
~H"""
|
|
<a
|
|
href={@patch}
|
|
data-phx-link="patch"
|
|
data-phx-link-state={if @replace, do: "replace", else: "push"}
|
|
phx-no-format
|
|
{@rest}
|
|
>{render_slot(@inner_block)}</a>
|
|
"""
|
|
end
|
|
|
|
def link(%{href: href} = assigns) when href != "#" and not is_nil(href) do
|
|
href = Phoenix.LiveView.Utils.valid_destination!(href, "<.link>")
|
|
assigns = assign(assigns, :href, href)
|
|
|
|
~H"""
|
|
<a
|
|
href={@href}
|
|
data-method={if @method != "get", do: @method}
|
|
data-csrf={if @method != "get", do: csrf_token(@csrf_token, @href)}
|
|
data-to={if @method != "get", do: @href}
|
|
phx-no-format
|
|
{@rest}
|
|
>{render_slot(@inner_block)}</a>
|
|
"""
|
|
end
|
|
|
|
def link(%{} = assigns) do
|
|
~H"""
|
|
<a href="#" {@rest}>{render_slot(@inner_block)}</a>
|
|
"""
|
|
end
|
|
|
|
defp csrf_token(true, href), do: Plug.CSRFProtection.get_csrf_token_for(href)
|
|
defp csrf_token(false, _href), do: nil
|
|
defp csrf_token(csrf, _href) when is_binary(csrf), do: csrf
|
|
|
|
@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">
|
|
<div id="modal-content">
|
|
Are you sure?
|
|
<button phx-click="cancel">Cancel</button>
|
|
<button phx-click="confirm">OK</button>
|
|
</div>
|
|
</.focus_wrap>
|
|
```
|
|
"""
|
|
@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"""
|
|
<div id={@id} phx-hook="Phoenix.FocusWrap" {@rest}>
|
|
<div id={"#{@id}-start"} tabindex="0" aria-hidden="true"></div>
|
|
{render_slot(@inner_block)}
|
|
<div id={"#{@id}-end"} tabindex="0" aria-hidden="true"></div>
|
|
</div>
|
|
"""
|
|
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 tag_name="input" name="my-input" type="text"/>
|
|
```
|
|
|
|
```html
|
|
<input name="my-input" type="text"/>
|
|
```
|
|
|
|
```heex
|
|
<.dynamic_tag tag_name="p">content</.dynamic_tag>
|
|
```
|
|
|
|
```html
|
|
<p>content</p>
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
attr.(:tag_name, :string, required: true, doc: "The name of the tag, such as `div`.")
|
|
|
|
attr.(:name, :string,
|
|
required: false,
|
|
doc:
|
|
"Deprecated: use tag_name instead. If tag_name is used, passed to the tag. Otherwise 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(%{rest: rest} = assigns) do
|
|
{tag_name, rest} =
|
|
case assigns do
|
|
%{tag_name: tag_name, name: name} ->
|
|
{tag_name, Map.put(rest, :name, name)}
|
|
|
|
%{tag_name: tag_name} ->
|
|
{tag_name, rest}
|
|
|
|
%{name: name} ->
|
|
IO.warn("""
|
|
Passing the tag name to `Phoenix.Component.dynamic_tag/1` using the `name` attribute is deprecated.
|
|
|
|
Instead of:
|
|
|
|
<.dynamic_tag name="p" ...>
|
|
|
|
use `tag_name` instead:
|
|
|
|
<.dynamic_tag tag_name="p" ...>
|
|
""")
|
|
|
|
{name, Map.delete(rest, :name)}
|
|
end
|
|
|
|
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.LiveView.HTMLEngine.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.
|
|
|
|
[INSERT LVATTRDOCS]
|
|
|
|
## Customizing the Label
|
|
|
|
The `id` attribute cannot be overwritten, but you can create a label with a `for` attribute
|
|
pointing to the UploadConfig `ref`:
|
|
|
|
```heex
|
|
<label for={@uploads.avatar.ref}>
|
|
<.live_file_input upload={@uploads.avatar} />
|
|
</label>
|
|
```
|
|
|
|
## Drag and Drop
|
|
|
|
Drag and drop is supported by annotating the droppable container with a `phx-drop-target`
|
|
attribute pointing to the UploadConfig `ref`, so the following markup is all that is required
|
|
for drag and drop support:
|
|
|
|
```heex
|
|
<label for={@uploads.avatar.ref} phx-drop-target={@uploads.avatar.ref}>
|
|
<.live_file_input upload={@uploads.avatar} />
|
|
</label>
|
|
```
|
|
|
|
## Examples
|
|
|
|
Rendering a file input:
|
|
|
|
```heex
|
|
<.live_file_input upload={@uploads.avatar} />
|
|
```
|
|
|
|
Rendering a file input with a label:
|
|
|
|
```heex
|
|
<label for={@uploads.avatar.ref}>Avatar</label>
|
|
<.live_file_input upload={@uploads.avatar} />
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
|
|
attr.(:upload, Phoenix.LiveView.UploadConfig,
|
|
required: true,
|
|
doc: "The `Phoenix.LiveView.UploadConfig` struct"
|
|
)
|
|
|
|
attr.(:accept, :string,
|
|
doc:
|
|
"the optional override for the accept attribute. Defaults to :accept specified by allow_upload"
|
|
)
|
|
|
|
attr.(:rest, :global, include: ~w(webkitdirectory required disabled capture form))
|
|
|
|
def live_file_input(%{upload: upload} = assigns) do
|
|
assigns = assign_new(assigns, :accept, fn -> upload.accept != :any && upload.accept end)
|
|
|
|
~H"""
|
|
<input
|
|
id={@upload.ref}
|
|
type="file"
|
|
name={@upload.name}
|
|
accept={@accept}
|
|
data-phx-hook="Phoenix.LiveFileUpload"
|
|
data-phx-update="ignore"
|
|
data-phx-upload-ref={@upload.ref}
|
|
data-phx-active-refs={join_refs(for(entry <- @upload.entries, do: entry.ref))}
|
|
data-phx-done-refs={join_refs(for(entry <- @upload.entries, entry.done?, do: entry.ref))}
|
|
data-phx-preflighted-refs={
|
|
join_refs(for(entry <- @upload.entries, entry.preflighted?, do: entry.ref))
|
|
}
|
|
data-phx-auto-upload={@upload.auto_upload?}
|
|
{if @upload.max_entries > 1, do: Map.put(@rest, :multiple, true), else: @rest}
|
|
/>
|
|
"""
|
|
end
|
|
|
|
defp join_refs(entries), do: Enum.join(entries, ",")
|
|
|
|
@doc ~S"""
|
|
Generates an image preview on the client for a selected file.
|
|
|
|
[INSERT LVATTRDOCS]
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.live_img_preview :for={entry <- @uploads.avatar.entries} entry={entry} width="75" />
|
|
```
|
|
|
|
When you need to use it multiple times, make sure that they have distinct ids
|
|
|
|
```heex
|
|
<.live_img_preview :for={entry <- @uploads.avatar.entries} entry={entry} width="75" />
|
|
|
|
<.live_img_preview :for={entry <- @uploads.avatar.entries} id={"modal-#{entry.ref}"} entry={entry} width="500" />
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
|
|
attr.(:entry, Phoenix.LiveView.UploadEntry,
|
|
required: true,
|
|
doc: "The `Phoenix.LiveView.UploadEntry` struct"
|
|
)
|
|
|
|
attr.(:id, :string,
|
|
default: nil,
|
|
doc:
|
|
"the id of the img tag. Derived by default from the entry ref, but can be overridden as needed if you need to render a preview of the same entry multiple times on the same page"
|
|
)
|
|
|
|
attr.(:rest, :global, [])
|
|
|
|
def live_img_preview(assigns) do
|
|
~H"""
|
|
<img
|
|
id={@id || "phx-preview-#{@entry.ref}"}
|
|
data-phx-upload-ref={@entry.upload_ref}
|
|
data-phx-entry-ref={@entry.ref}
|
|
data-phx-hook="Phoenix.LiveImgPreview"
|
|
data-phx-update="ignore"
|
|
phx-no-format
|
|
{@rest}
|
|
/>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Intersperses separator slot between an enumerable.
|
|
|
|
Useful when you need to add a separator between items such as when
|
|
rendering breadcrumbs for navigation. Provides each item to the
|
|
inner block.
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.intersperse :let={item} enum={["home", "profile", "settings"]}>
|
|
<:separator>
|
|
<span class="sep">|</span>
|
|
</:separator>
|
|
{item}
|
|
</.intersperse>
|
|
```
|
|
|
|
Renders the following markup:
|
|
|
|
```html
|
|
home <span class="sep">|</span> profile <span class="sep">|</span> settings
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
attr.(:enum, :any, required: true, doc: "the enumerable to intersperse with separators")
|
|
slot.(:inner_block, required: true, doc: "the inner_block to render for each item")
|
|
slot.(:separator, required: true, doc: "the slot for the separator")
|
|
|
|
def intersperse(assigns) do
|
|
~H"""
|
|
<%= for item <- Enum.intersperse(@enum, :separator) do %><%=
|
|
if item == :separator do
|
|
render_slot(@separator)
|
|
else
|
|
render_slot(@inner_block, item)
|
|
end
|
|
%><% end %>
|
|
"""noformat
|
|
end
|
|
|
|
@doc """
|
|
Renders a `Phoenix.LiveView.AsyncResult` struct (e.g. from `Phoenix.LiveView.assign_async/4`)
|
|
with slots for the different loading states.
|
|
The result state takes precedence over subsequent loading and failed
|
|
states.
|
|
|
|
> #### Note {: .info}
|
|
>
|
|
> The inner block receives the result of the async assign as a `:let`.
|
|
> The let is only accessible to the inner block and is not in scope to the
|
|
> other slots.
|
|
|
|
## Examples
|
|
|
|
```elixir
|
|
def mount(%{"slug" => slug}, _, socket) do
|
|
{:ok,
|
|
socket
|
|
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)}
|
|
end
|
|
```
|
|
|
|
```heex
|
|
<.async_result :let={org} assign={@org}>
|
|
<:loading>Loading organization...</:loading>
|
|
<:failed :let={_failure}>there was an error loading the organization</:failed>
|
|
<%= if org do %>
|
|
{org.name}
|
|
<% else %>
|
|
You don't have an organization yet.
|
|
<% end %>
|
|
</.async_result>
|
|
```
|
|
|
|
See [Async Operations](`m:Phoenix.LiveView#module-async-operations`) for more information.
|
|
|
|
To display loading and failed states again on subsequent `assign_async` calls,
|
|
reset the assign to a result-free `%AsyncResult{}`:
|
|
|
|
```elixir
|
|
{:noreply,
|
|
socket
|
|
|> assign_async(:page, :data, &reload_data/0)
|
|
|> assign(:page, AsyncResult.loading())}
|
|
```
|
|
"""
|
|
@doc type: :component
|
|
attr.(:assign, AsyncResult, required: true)
|
|
slot.(:loading, doc: "rendered while the assign is loading for the first time")
|
|
|
|
slot.(:failed,
|
|
doc:
|
|
"rendered when an error or exit is caught or assign_async returns `{:error, reason}` for the first time. Receives the error as a `:let`"
|
|
)
|
|
|
|
slot.(:inner_block,
|
|
doc:
|
|
"rendered when the assign is loaded successfully via `AsyncResult.ok/2`. Receives the result as a `:let`"
|
|
)
|
|
|
|
def async_result(%{assign: async_assign} = assigns) do
|
|
cond do
|
|
async_assign.ok? ->
|
|
~H|{render_slot(@inner_block, @assign.result)}|
|
|
|
|
async_assign.loading ->
|
|
~H|{render_slot(@loading, @assign.loading)}|
|
|
|
|
async_assign.failed ->
|
|
~H|{render_slot(@failed, @assign.failed)}|
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Renders a portal.
|
|
|
|
A portal is a component that teleports its content to another place in the DOM.
|
|
It is useful in cases where you need to render some content in another place, for
|
|
example due to overflow or [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context).
|
|
|
|
A portal consists of two parts:
|
|
|
|
1. The portal source: the component that should be teleported.
|
|
2. The portal target: the DOM element that will render the content of the portal source.
|
|
|
|
Any element can be a portal target. In most cases, the target would be rendered inside
|
|
the layout of your application. Portal sources must be defined with the `.portal` component.
|
|
|
|
## Examples
|
|
|
|
```heex
|
|
<.portal id="modal" target="body">
|
|
...
|
|
</.portal>
|
|
```
|
|
"""
|
|
|
|
attr.(:id, :string, required: true)
|
|
|
|
attr.(:target, :string,
|
|
required: true,
|
|
doc: "A CSS selector that identifies the target. The target must be unique."
|
|
)
|
|
|
|
attr.(:class, :string, default: nil, doc: "The class to apply to the portal wrapper.")
|
|
attr.(:container, :string, default: "div", doc: "The HTML tag to use as the portal wrapper.")
|
|
slot.(:inner_block, required: true)
|
|
|
|
def portal(assigns) do
|
|
~H"""
|
|
<template id={@id} data-phx-portal={@target}>
|
|
<%!--
|
|
For correct DOM patching, each portal source (template) must have a single root element,
|
|
which we enforce by wrapping the slot in a div. In the generated CSS for
|
|
new projects, we include a display: contents rule for data-phx-teleported-src,
|
|
which is set by the LiveView JS when an element is teleported.
|
|
--%>
|
|
<.dynamic_tag tag_name={@container} id={"_lv_portal_wrap_" <> @id} class={@class}>
|
|
{render_slot(@inner_block)}
|
|
</.dynamic_tag>
|
|
</template>
|
|
"""
|
|
end
|
|
end
|