Phoenix.Component
Исходный кодОпределение повторно используемых функциональных компонентов с шаблонами HEEx.
Функциональный компонент — это любая функция, которая получает в качестве аргумента карту assigns и возвращает отрисованную структуру, созданную с помощью сигила ~H:
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
Эта функция использует сигил ~H для возврата отрисованного шаблона. ~H означает HEEx (HTML + EEx). HEEx — это язык шаблонов для написания HTML с интерполяцией Elixir. Внутри {...} можно писать код Elixir для интерполяции с учетом HTML в атрибутах тегов и теле документа. Также можно интерполировать произвольные блоки HEEx с помощью <%= ... %>. Мы используем @name для доступа к ключу name, определенному внутри assigns.
При вызове внутри сигила ~H или файла шаблона HEEx:
<MyComponent.greet name="Jane" />
Будет отрисован следующий HTML:
<p>Hello, Jane!</p>
Если функциональный компонент определен локально или его модуль импортирован, вызывающий код может вызвать функцию напрямую, не указывая модуль:
<.greet name="Jane" />
Для динамических значений в функциональный компонент можно интерполировать выражения Elixir:
<.greet name={@user.name} />
Функциональные компоненты также могут принимать блоки содержимого HEEx (об этом подробнее далее):
<.card> <p>This is the body of my card!</p> </.card>
В этом модуле мы научимся создавать насыщенные и компонуемые компоненты для использования в наших приложениях.
Атрибуты
Phoenix.Component предоставляет макрос attr/3 для объявления атрибутов, которые ожидает получить вызываемый функциональный компонент:
attr :name, :string, required: true
def greet(assigns) do
~H"""
<p>Hello, {@name}!</p>
"""
end
При вызове attr/3 теперь явно указано, что для корректной отрисовки greet/1 требуется строковый атрибут name в карте assigns. Если его не указать, появится предупреждение компилятора:
<MyComponent.greet />
<!-- warning: missing required attribute "name" for component MyAppWeb.MyComponent.greet/1
lib/app_web/my_component.ex:15 -->
Для атрибутов можно задавать значения по умолчанию, которые автоматически объединяются с картой assigns:
attr :name, :string, default: "Bob"
Теперь функциональный компонент можно вызвать, не указывая значение для name:
<.greet />
В результате будет отрисован следующий HTML:
<p>Hello, Bob!</p>
Обращение к обязательному атрибуту без значения по умолчанию приведет к ошибке. Необходимо явно объявить default: nil или программно присвоить значение с помощью функции assign_new/3.
Для одного и того же функционального компонента можно объявить несколько атрибутов:
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
Это позволит вызывающему коду передавать несколько значений:
<.celebrate name={"Genevieve"} age={34} />
В результате будет отрисован следующий HTML:
<p> Happy birthday Genevieve! You are 34 years old. </p>
В одном модуле можно определить несколько функциональных компонентов с разными атрибутами. В следующем примере для <Components.greet/> требуется name, но title не является обязательным, а для <Components.heading> требуется title, но 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
Макрос attr/3 предоставляет основные средства для создания повторно используемых функциональных компонентов. Но что делать, если функциональным компонентам нужно поддерживать динамические атрибуты, например стандартные атрибуты HTML, которые добавляются к контейнеру компонента?
Глобальные атрибуты
Глобальные атрибуты — это набор атрибутов, которые может принимать функциональный компонент, если он объявляет атрибут типа :global. По умолчанию принимаются атрибуты, общие для всех стандартных тегов HTML. Полный список см. в разделе Глобальные атрибуты.
После объявления глобального атрибута вызывающий код может передавать любое количество атрибутов из набора, не изменяя сам функциональный компонент.
Ниже приведен пример функционального компонента, принимающего динамическое количество глобальных атрибутов:
attr :message, :string, required: true
attr :rest, :global
def notification(assigns) do
~H"""
<span {@rest}>{@message}</span>
"""
end
Вызывающий код может передавать несколько глобальных атрибутов (например, привязки phx-* или атрибут class):
<.notification message="You've got mail!" class="bg-green-200" phx-click="close" />
В результате будет отрисован следующий HTML:
<span class="bg-green-200" phx-click="close">You've got mail!</span>
Обратите внимание, что функциональному компоненту не пришлось явно объявлять атрибут class или phx-click для корректной отрисовки.
Для глобальных атрибутов можно задавать значения по умолчанию, которые объединяются с атрибутами, предоставленными вызывающим кодом. Например, можно объявить значение class по умолчанию, если вызывающий код его не предоставил:
attr :rest, :global, default: %{class: "bg-blue-200"}
Теперь функциональный компонент можно вызвать без атрибута class:
<.notification message="You've got mail!" phx-click="close" />
В результате будет отрисован следующий HTML:
<span class="bg-blue-200" phx-click="close">You've got mail!</span>
Обратите внимание: глобальный атрибут нельзя передать напрямую — это приведет к предупреждению. Иными словами, следующий вариант недопустим:
<.notification message="You've got mail!" rest={%{"phx-click" => "close"}} />
Дополнительные глобальные атрибуты
С помощью параметра :include можно указать дополнительные атрибуты помимо известных глобальных. Например, чтобы поддержать атрибут form в компоненте кнопки:
# <.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
Параметр :include удобен для добавления глобальных атрибутов в отдельных случаях, но иногда требуется расширить существующие компоненты новыми глобальными атрибутами, например префиксами x- из Alpine.js, о которых пойдет речь далее.
Пользовательские префиксы глобальных атрибутов
Набор глобальных атрибутов можно расширить, передав список префиксов атрибутов в use Phoenix.Component. Как и атрибуты по умолчанию, общие для всех элементов HTML, любое количество атрибутов, начинающихся с глобального префикса, будет приниматься функциональными компонентами, вызываемыми текущим модулем. По умолчанию поддерживаются следующие префиксы: phx-, aria- и data-. Например, чтобы поддержать префикс x-, используемый Alpine.js, можно передать параметр :global_prefixes в use Phoenix.Component:
use Phoenix.Component, global_prefixes: ~w(x-)
В приложении Phoenix это обычно настраивается в файле lib/my_app_web.ex внутри определения def html:
def html do
quote do
use Phoenix.Component, global_prefixes: ~w(x-)
# ...
end
end
Теперь все функциональные компоненты, вызываемые этим модулем, будут принимать любое количество атрибутов с префиксом x-, а также стандартными глобальными префиксами.
Подробнее об атрибутах можно узнать из документации к attr/3.
Слоты
Помимо атрибутов, функциональные компоненты могут принимать блоки содержимого HEEx, называемые слотами. Слоты позволяют дополнительно настраивать отрисованный HTML: вызывающий код может передать функциональному компоненту содержимое HEEx, которое тот должен отрисовать. Phoenix.Component предоставляет макрос slot/3 для объявления слотов функциональных компонентов:
slot :inner_block, required: true
def button(assigns) do
~H"""
<button>
{render_slot(@inner_block)}
</button>
"""
end
Выражение render_slot(@inner_block) отрисовывает содержимое HEEx. Вызвать этот функциональный компонент можно так:
<.button> This renders <strong>inside</strong> the button! </.button>
В результате будет отрисован следующий HTML:
<button> This renders <strong>inside</strong> the button! </button>
Как и макрос attr/3, макрос slot/3 обеспечивает проверки во время компиляции. Например, вызов button/1 без слота с содержимым HEEx приведет к предупреждению компилятора:
<.button />
<!-- warning: missing required slot "inner_block" for component MyAppWeb.MyComponent.button/1
lib/app_web/my_component.ex:15 -->
Слот по умолчанию
В приведенном выше примере используется слот по умолчанию, доступный как assign с именем @inner_block, для отрисовки содержимого HEEx с помощью функции render_slot/1.
Если значения, отрисовываемые в слоте, должны быть динамическими, можно передать содержимому HEEx второе значение, вызвав 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
При вызове функционального компонента можно использовать специальный атрибут :let, чтобы получить значение, которое передает функциональный компонент, и связать его с переменной:
<.unordered_list :let={fruit} entries={~w(apples bananas cherries)}>
I like <b>{fruit}</b>!
</.unordered_list>
В результате будет отрисован следующий HTML:
<ul> <li>I like <b>apples</b>!</li> <li>I like <b>bananas</b>!</li> <li>I like <b>cherries</b>!</li> </ul>
Так сохраняется разделение ответственности: вызывающий код может указать несколько значений в атрибуте-списке, не задавая окружающее их содержимое HEEx и разделители.
Именованные слоты
Помимо слота по умолчанию, функциональные компоненты могут принимать несколько именованных слотов с содержимым HEEx. Например, представьте, что вы хотите создать модальное окно с заголовком, основным содержимым и нижним колонтитулом:
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
Вызвать этот функциональный компонент можно с помощью синтаксиса 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>
В результате будет отрисован следующий 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>
Как показано в примере выше, render_slot/1 возвращает nil, если слот объявлен как необязательный, но не передан. Это можно использовать для задания поведения по умолчанию.
Атрибуты слотов
В отличие от слота по умолчанию, именованному слоту можно передать несколько фрагментов содержимого HEEx. Именованные слоты также могут принимать атрибуты, определенные путем передачи блока в макрос slot/3. Если передано несколько фрагментов содержимого, функция render_slot/2 объединит и отрисует все значения.
Ниже приведен компонент таблицы, демонстрирующий несколько именованных слотов с атрибутами:
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
Вызвать этот функциональный компонент можно так:
<.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>
В результате будет отрисован следующий 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>
Подробнее о слотах и макросе slot/3 можно узнать в документации к нему.
Встраивание внешних файлов шаблонов
Макрос embed_templates/1 можно использовать для встраивания файлов .html.heex в качестве функциональных компонентов. Путь к каталогу задается относительно текущего модуля (__DIR__); для выбора всех файлов в дереве каталогов можно использовать шаблон с подстановочными знаками. Например, представим каталог со следующей структурой:
├── components.ex ├── cards │ ├── pricing_card.html.heex │ └── features_card.html.heex
Затем можно встроить шаблоны страниц в модуль components.ex и вызывать их, как любой другой функциональный компонент:
defmodule MyAppWeb.Components do
use Phoenix.Component
embed_templates "cards/*"
def landing_hero(assigns) do
~H"""
<.pricing_card />
<.features_card />
"""
end
end
Дополнительные сведения, включая поддержку декларативных assigns для встроенных шаблонов, см. в разделе embed_templates/1.
Отладочная информация
Шаблоны HEEx поддерживают добавление аннотаций и расположения в отрисованную страницу. Это специальные комментарии и атрибуты HTML, помогающие определить, где в дереве функциональных компонентов отрисована разметка HTML-документа.
Например, представим следующий шаблон HEEx:
<.header> <.button>Click</.button> </.header>
При включении debug_heex_annotations 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> -->
Аналогичным образом можно включить :debug_attributes, добавляющий атрибут data-phx-loc с номером строки, в которой определен каждый тег HTML (а также data-phx-pid для контейнера LiveView):
<header data-phx-loc="125" class="p-5"> <button data-phx-loc="458" class="px-2 bg-indigo-500 text-white">Click</button> </header>
Эти возможности работают с любыми шаблонами ~H или .html.heex. Их можно включить глобально с помощью следующей конфигурации в файле config/dev.exs:
config :phoenix_live_view, debug_heex_annotations: true, debug_attributes: true
Изменение этой конфигурации потребует запуска mix clean и полной повторной компиляции.
Также можно настроить отладочные аннотации для конкретного модуля, задав параметры в качестве атрибута модуля:
@debug_heex_annotations false @debug_attributes false
Это полезно главным образом для отключения аннотаций, например если нужно показывать пользователям исходный HTML.
Динамическая отрисовка компонентов
Иногда требуется определить во время выполнения, какой компонент нужно отрисовать. Поскольку функциональные компоненты — это обычные функции, можно использовать функцию apply/3 из Elixir для динамического вызова модуля и/или функции, переданных в качестве assign.
Например, используя следующее определение функционального компонента:
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
Затем можно использовать функцию dynamic_component следующим образом:
<.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>
Будет вызвана функция MyAppWeb.MyModule.my_function/1 с остальными assigns.
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
В результате будет получен следующий HTML:
<p>Dynamic component with shared assigns: Yay Elixir!</p> <p>Howdy from the inner block!</p> <p>Howdy from the named slot!</p>
Обратите внимание: чтобы максимально использовать проверки во время компиляции в Phoenix.Component, рекомендуется определять такой dynamic_component для определенного набора компонентов с одинаковым API, а не для общего случая. В этом примере мы определили dynamic_component, ожидающий assign с именем shared, а также два слота, которые должны быть реализованы всеми компонентами, используемыми с ним. Определения атрибутов и слотов вызываемого компонента my_function нельзя проверить через вызов apply.
Краткое описание
- async_result(assigns)
Отрисовывает структуру
Phoenix.LiveView.AsyncResult(например, полученную изPhoenix.LiveView.assign_async/4) со слотами для разных состояний загрузки. Состояние результата имеет приоритет над последующими состояниями загрузки и ошибки.- dynamic_tag(assigns)
Создает динамически именуемый тег HTML.
- focus_wrap(assigns)
Ограничивает переход табуляцией контейнером для обеспечения доступности.
- form(assigns)
Отрисовывает форму.
- inputs_for(assigns)
Отрисовывает вложенные поля формы для ассоциаций или встраиваемых структур.
- intersperse(assigns)
Вставляет слот-разделитель между элементами перечисляемой коллекции.
- link(assigns)
Создает ссылку на указанный маршрут.
- live_component(assigns)
Функциональный компонент для отрисовки
Phoenix.LiveComponentвнутри родительского LiveView.- live_file_input(assigns)
Создает тег поля выбора файла для загрузки в LiveView.
- live_img_preview(assigns)
Создает на клиенте предварительный просмотр изображения для выбранного файла.
- live_title(assigns)
Отрисовывает заголовок с автоматическим префиксом или суффиксом при обновлениях
@page_title.
Компоненты
- attr(name, type, opts \\ [])
Объявляет атрибуты функционального компонента HEEx.
- embed_templates(pattern, opts \\ [])
Встраивает внешние файлы шаблонов в модуль в качестве функциональных компонентов.
- sigil_H(arg, modifiers)
Сигил
~Hдля написания шаблонов HEEx в исходных файлах.- slot(name, opts \\ [])
Объявляет слот. Дополнительные сведения см. в разделе
slot/3.- slot(name, opts, block)
Объявляет слот функционального компонента.
Макросы
- assign(socket_or_assigns, keyword_or_map)
Добавляет пары ключ-значение в assigns.
- assign(socket_or_assigns, key, value)
Добавляет пару
key-valueвsocket_or_assigns.- assign_new(socket_or_assigns, key, fun)
Добавляет заданный
keyсо значением изfunвsocket_or_assigns, если такого значения еще нет.- assigns_to_attributes(assigns, exclude \\ [])
Фильтрует assigns в виде списка ключевых слов для использования в атрибутах динамических тегов.
- changed?(socket_or_assigns, key)
Проверяет, изменился ли заданный ключ в
socket_or_assigns.- live_flash(other, key) устарела
Возвращает флеш-сообщение из assign flash в LiveView.
- live_render(conn_or_socket, view, opts \\ [])
Отрисовывает LiveView внутри шаблона.
- portal(assigns)
Отрисовывает портал.
- render_slot(slot, argument \\ nil)
Отрисовывает элемент слота с заданным необязательным
argument.- to_form(data_or_params, options \\ [])
Преобразует заданную структуру данных в
Phoenix.HTML.Form.- update(socket_or_assigns, key, fun)
Обновляет существующий
keyс помощьюfunв заданномsocket_or_assigns.- upload_errors(conf)
Возвращает ошибки загрузки в целом.
- upload_errors(conf, entry)
Возвращает ошибки для элемента загрузки.
- used_input?(form_field)
Проверяет, использовал ли клиент поле ввода.
Функции
Компоненты
async_result(assigns)Источник
Отображает структуру Phoenix.LiveView.AsyncResult (например, полученную из Phoenix.LiveView.assign_async/4) со слотами для различных состояний загрузки. Состояние результата имеет приоритет над последующими состояниями загрузки и ошибки.
Примечание
Внутренний блок получает результат асинхронного assign в качестве :let. Переменная let доступна только внутреннему блоку и не входит в область видимости других слотов.
Примеры
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)}
end
<.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>
Дополнительные сведения см. в разделе Асинхронные операции.
Чтобы при последующих вызовах assign_async снова отображались состояния загрузки и ошибки, сбросьте assign, чтобы в %AsyncResult{} не было результата:
{:noreply,
socket
|> assign_async(:page, &reload_data/0)
|> assign(:page, AsyncResult.loading())}
Атрибуты
-
assign(Phoenix.LiveView.AsyncResult) (обязательный)
Слоты
-
loading— отображается при первой загрузке assign. -
failed— отображается, если перехвачена ошибка или выход либо если assign_async впервые возвращает{:error, reason}. Получает ошибку в качестве:let. -
inner_block— отображается, если assign успешно загружен с помощьюAsyncResult.ok/2. Получает результат в качестве:let.
dynamic_tag(assigns)Источник
Создаёт HTML-тег с динамически заданным именем.
Вызывает исключение ArgumentError, если имя тега признано небезопасным HTML.
Атрибуты
-
tag_name(:string) (обязательный) — имя тега, напримерdiv. -
name(:string) — устарел: используйте вместо него tag_name. Если используется tag_name, передаётся тегу. В противном случае — имя тега, напримерdiv. - Поддерживаются глобальные атрибуты. Дополнительные HTML-атрибуты, добавляемые к тегу с надлежащим экранированием.
Слоты
inner_block
Примеры
<.dynamic_tag tag_name="input" name="my-input" type="text"/>
<input name="my-input" type="text"/>
<.dynamic_tag tag_name="p">content</.dynamic_tag>
<p>content</p>
focus_wrap(assigns)Источник
Обеспечивает циклическое перемещение фокуса клавишей Tab внутри контейнера для повышения доступности.
Это важная функция доступности для таких элементов интерфейса, как модальные окна, диалоговые окна и меню.
Атрибуты
-
id(:string) (обязательный) — идентификатор DOM-тега контейнера. - Поддерживаются глобальные атрибуты. Дополнительные HTML-атрибуты, добавляемые к тегу контейнера.
Слоты
-
inner_block(обязательный) — содержимое, отображаемое внутри тега контейнера.
Примеры
Просто поместите внутреннее содержимое в этот компонент: при перемещении пользователя по содержимому контейнера с помощью Tab фокус будет оставаться внутри него:
<.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> form(assigns)Источник
Отображает форму.
Эта функция принимает структуру Phoenix.HTML.Form, обычно созданную с помощью to_form/2, и генерирует соответствующие теги формы. Её можно использовать как внутри LiveView, так и вне его.
Чтобы увидеть, как формы работают на практике, можно запустить
mix phx.gen.live Blog Post posts title body:textв приложении Phoenix. Эта команда настроит необходимые таблицы базы данных и LiveView для управления данными.
Примеры: внутри LiveView
Внутри LiveView этот функциональный компонент обычно вызывается с for={@form}, где @form — результат функции to_form/1. Функция to_form/1 принимает в качестве источника данных либо map, либо Ecto.Changeset и нормализует его в структуру Phoenix.HTML.Form.
Например, можно использовать параметры, полученные в обратном вызове Phoenix.LiveView.handle_event/3, чтобы создать changeset Ecto, а затем воспользоваться to_form/1, чтобы преобразовать его в форму. Затем в шаблонах передайте @form в качестве аргумента компоненту :for:
<.form
for={@form}
id="my-form"
phx-change="change_name"
>
<.input field={@form[:email]} />
</.form>
Компонент .input обычно определяется в самом приложении и добавляет все необходимые стили:
def input(assigns) do
~H"""
<input type="text" name={@field.name} id={@field.id} value={@field.value} class="..." />
"""
end
Форма принимает несколько параметров. Например, при загрузке файлов, если нужно обрабатывать отправку формы, можно написать так:
<.form
for={@form}
id="my-form"
phx-change="change_user"
phx-submit="save_user"
multipart
>
...
<input type="submit" value="Save" />
</.form>
Обратите внимание, что в обоих примерах используется phx-change. LiveView должен реализовать событие phx-change и сохранять значения полей по мере их изменения. Это важно, поскольку при изменении несвязанной части страницы LiveView должен повторно отобразить поля с обновлёнными значениями. Без phx-change значения полей будут очищены. В качестве альтернативы можно использовать phx-update="ignore" формы, чтобы отбросить все обновления.
Использование атрибута for
Атрибут for также может принимать map или Ecto.Changeset. В этом случае форма будет создана автоматически, а получить её можно с помощью :let:
<.form
:let={form}
for={@changeset}
id="my-form"
phx-change="change_user"
>
Однако в LiveView такой подход не рекомендуется по двум причинам:
LiveView может эффективнее оптимизировать код, если обращаться к полям формы через
@form[:field], а не через переменную letformChangeset Ecto предназначены для однократного использования. Если не сохранять changeset в assign, у вас будет меньше соблазна использовать его в нескольких операциях
Примечание об ошибках :errors
Даже если changeset.errors не пуст, ошибки не будут отображаться в форме, если действие changeset :action равно nil или :ignore.
Это полезно, например, для подсказок проверки полей формы, таких как пустой changeset для новой формы. Такой changeset недействителен, но мы не хотим показывать ошибки, пока пользователь не выполнит какое-либо действие.
Например, если пользователь отправляет форму, вызывается Repo.insert/1, и проверка changeset завершается неудачей, для действия задаётся значение :insert, указывающее на попытку вставки. Наличие этого действия приводит к отображению ошибок. То же самое справедливо для Repo.update/delete.
Видимость ошибок регулируется передачей действия в to_form/2, которая задаёт действие underlying changeset. Действие также можно задать вручную, напрямую изменив поле структуры Ecto.Changeset или воспользовавшись Ecto.Changeset.apply_action/2. Поскольку действие может быть произвольным, можно задать ему значение :validate или любое другое, чтобы не создавать впечатление, будто операция с базой данных действительно выполнялась.
Отображение ошибок в использованных и неиспользованных полях ввода
Использованными считаются только поля ввода, на которые клиент устанавливал фокус, с которыми взаимодействовал или которые отправлял. В большинстве случаев пользователь не должен получать сообщения об ошибках в формах, с которыми он ещё не взаимодействовал, пока не отправит форму. Фильтровать ошибки по использованным полям ввода можно с помощью used_input?/1.
Пример: вне LiveView (обычные HTTP-запросы)
Компонент form по-прежнему можно использовать для отправки форм вне LiveView. В таких случаях необходимо указать стандартный HTML-атрибут action. Без него метод form и токен CSRF будут отброшены.
<.form :let={f} for={@changeset} action={~p"/comments/#{@comment}"}>
<.input field={f[:body]} />
</.form>
В приведённом выше примере мы передали changeset в for и получили значение с помощью :let={f}. Такой подход допустим вне LiveView, поскольку там не нужно учитывать оптимизации отслеживания изменений.
Защита от CSRF
Защита от CSRF — это механизм, который позволяет убедиться, что форму отправляет тот же пользователь, который её отобразил. Этот модуль по умолчанию генерирует токен CSRF. Приложение должно проверять этот токен на сервере, чтобы злоумышленники не могли отправлять запросы от имени других пользователей. По умолчанию Phoenix проверяет этот токен.
Если адрес отправки формы содержит хост, например "//host.com/path" вместо просто "/path", Phoenix включит подпись хоста в токен и проверит его, только если хост запроса совпадает с хостом в токене. Это предотвращает утечку токенов сторонним приложениям. Если такое поведение вызывает проблемы, можно создать токен, не привязанный к хосту, с помощью Plug.CSRFProtection.get_csrf_token/0 и передать его генератору формы через параметр :csrf_token.
Атрибуты
for(:any) (обязательный) — существующая форма или исходные данные формы.action(:string) — адрес, на который отправляется форма. Этот атрибут необходимо указать, если вы хотите отправлять форму на URL вне LiveView.as(:atom) — префикс для имён и идентификаторов, создаваемых формой. Например, если задатьas: :user_params, параметры будут вложены в "user_params" в вашемhandle_eventилиconn.params["user_params"]при обычных HTTP-запросах. Если задан этот параметр, необходимо получить форму с помощью:let.csrf_token(:any) — токен для проверки подлинности запросов. Он генерируется автоматически, если указан адрес отправки, а метод не равенget. Если задано значениеfalse, токен не генерируется.errors(:list) — позволяет вручную передать форме список ошибок в формате keyword list. Этот параметр полезен, когда в качестве источника формы передан обычный map: ошибки будут доступны вf.errors. Если задан этот параметр, необходимо получить форму с помощью:let.method(:string) — HTTP-метод. Используется только при наличии:action. Если метод не равенgetилиpost, рядом с тегом формы создаётся тег input с именем_method. Если указан:action, но метод не задан, по умолчанию используется значение, возвращаемоеPhoenix.HTML.FormData.to_form/2(обычноpost).-
multipart(:boolean) — устанавливаетenctypeв значениеmultipart/form-data. Обязателен при загрузке файлов.По умолчанию —
false. Поддерживаются глобальные атрибуты. Дополнительные HTML-атрибуты, добавляемые к тегу формы. Поддерживаются все глобальные атрибуты, а также:
["autocomplete", "name", "rel", "enctype", "novalidate", "target"].
Слоты
-
inner_block(обязательный) — содержимое, отображаемое внутри тега формы.
inputs_for(assigns)Источник
Отображает вложенные поля формы для ассоциаций или встраиваемых структур.
Атрибуты
field(Phoenix.HTML.FormField) (обязательный) — кортеж %Phoenix.HTML.Form{}/имя поля, например: {@form[:email]}.id(:string) — базовый идентификатор для полей формы. По умолчанию используется идентификатор родительской формы. Вычисленный идентификатор будет состоять из базового идентификатора и имени поля, а также индекса для каждого поля в списке.as(:atom) — имя формы; по умолчанию это имя родительской формы, объединённое с заданным полем.default(:any) — значение, используемое, если другое значение отсутствует.prepend(:list) — значения, добавляемые в начало при отображении. Применяется, только если значение поля — список и через форму не были переданы параметры.append(:list) — значения, добавляемые в конец при отображении. Применяется, только если значение поля — список и через форму не были переданы параметры.-
skip_hidden(:boolean) — пропускает автоматическое отображение скрытых полей, позволяя точнее контролировать создаваемую разметку.По умолчанию —
false. -
skip_persistent_id(:boolean) — пропускает автоматическое отображение скрытых полей _persistent_id, используемых для изменения порядка полей ввода.По умолчанию —
false. -
options(:list) — любые дополнительные параметры для реализации протоколаPhoenix.HTML.FormData.По умолчанию —
[].
Слоты
-
inner_block(обязательный) — содержимое, отображаемое для каждой вложенной формы.
Примеры
<.form
for={@form}
id="my-form"
phx-change="change_name"
>
<.inputs_for :let={f_nested} field={@form[:nested]}>
<.input type="text" field={f_nested[:name]} />
</.inputs_for>
</.form>
Динамическое добавление и удаление полей ввода
Динамическое добавление и удаление полей ввода поддерживается с помощью именованных кнопок добавления и удаления. Как и поля ввода, кнопки с парами имя/значение сериализуются вместе с данными формы при событиях изменения и отправки. Библиотеки, такие как Ecto, или пользовательская фильтрация параметров могут анализировать параметры и обрабатывать добавленные или удалённые поля. Это можно сочетать с параметрами :sort_param и :drop_param функции Ecto.Changeset.cast_assoc/3. Например, представьте родительскую структуру с ассоциацией :emails has_many или embeds_many. Чтобы преобразовать пользовательский ввод из вложенной формы, достаточно настроить параметры:
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
Здесь показано, как работают параметры :sort_param и :drop_param.
Примечание
При использовании этих параметров требуется on_replace: :delete для has_many и embeds_many.
Когда Ecto получает из формы указанный параметр сортировки или удаления, он сортирует дочерние элементы в порядке их появления в форме, добавляет новые дочерние элементы, которых раньше не было, или удаляет элементы, если этого требует параметр.
Разметка для такой схемы и ассоциации будет выглядеть так:
<.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>
Мы использовали inputs_for для отображения полей ввода ассоциации :emails, которая содержит поля для адреса электронной почты и имени каждого дочернего элемента. Во вложенных полях ввода мы отображаем скрытое поле mailing_list[emails_sort][], которому присваивается индекс соответствующего дочернего элемента. Это сообщает операции cast в Ecto, как сортировать существующие дочерние элементы или куда вставлять новые. Затем мы отображаем поля для электронной почты и имени обычным способом. После этого отображаем кнопку с текстом "delete" и именем mailing_list[emails_drop][], значением которого является индекс дочернего элемента.
Как и прежде, это указывает Ecto удалить дочерний элемент с данным индексом при нажатии кнопки. Мы используем phx-click={JS.dispatch("change")} для кнопки, чтобы сообщить LiveView, что нажатие следует считать событием изменения, а не отправкой формы, которая вызывает привязку phx-change нашей формы.
За пределами inputs_for мы отображаем пустое поле ввода mailing_list[emails_drop][], чтобы при сохранении формы удалялись все дочерние элементы, если пользователь удалил все записи. Это скрытое поле обязательно при удалении ассоциаций.
Наконец, мы также отображаем другую кнопку с именем параметра сортировки mailing_list[emails_sort][] и именем value="new", снабжённую текстом "add more". Обратите внимание: у этой кнопки должен быть атрибут type="button", чтобы она не отправляла форму. Ecto считает неизвестные параметры сортировки новыми дочерними элементами и создаёт новый дочерний элемент. Эта кнопка необязательна и нужна только для динамического добавления записей. При желании можно добавить похожую кнопку перед <.inputs_for>, если нужно добавлять записи в начало списка.
Примечание о доступе к value поля
Может возникнуть желание обратиться к form[:field].value или попытаться изменить метаданные формы в шаблонах. Однако помните, что значение form[:field] отражает самые последние изменения. Например, поле :integer может содержать целочисленные значения, но также может содержать строку, если форма была отправлена.
Это особенно заметно при использовании inputs_for. Обращение к .value вложенного поля может вернуть структуру, changeset или исходные параметры, отправленные клиентом (при использовании drop_param). Поэтому form[:field].value непрактично для получения или вычисления других свойств.
Правильный подход к решению этой проблемы — вычислять нужные свойства либо в LiveView, обходя соответствующие changeset и структуры данных, либо переносить эту логику непосредственно в Ecto.Changeset.
Например, представьте приложение для учёта времени, в котором:
- пользователи указывают общее рабочее время за день
- отдельные действия хранятся как встраиваемые структуры
- сумма времени всех действий должна совпадать с общим временем
- в форме должно отображаться оставшееся время
Вместо того чтобы вычислять оставшееся время в шаблоне, например с помощью calculate_remaining(@form) и обращения к form[:activities].value, вычислите оставшееся время на основе changeset в handle_event:
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
Эту логику также можно реализовать непосредственно в модуле схемы. Если значение :remaining требуется часто, его можно добавить в схему как поле :virtual и выполнять вычисление при проверке changeset:
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
Используя этот подход, можно безопасно отображать оставшееся время в шаблоне с помощью @form[:remaining].value, избегая проблем, связанных с прямым доступом к сложным значениям полей.
intersperse(assigns)Источник
Вставляет слот-разделитель между элементами перечисления.
Полезно, когда нужно добавить разделитель между элементами, например при отображении навигационной цепочки. Передаёт каждый элемент внутреннему блоку.
Примеры
<.intersperse :let={item} enum={["home", "profile", "settings"]}>
<:separator>
<span class="sep">|</span>
</:separator>
{item}
</.intersperse>
Отображает следующую разметку:
home <span class="sep">|</span> profile <span class="sep">|</span> settings
Атрибуты
-
enum(:any) (обязательный) — перечисление, между элементами которого вставляются разделители.
Слоты
-
inner_block(обязательный) — inner_block, отображаемый для каждого элемента. -
separator(обязательный) — слот для разделителя.
link(assigns)Источник
Создаёт ссылку на указанный маршрут.
Обычно используется с одним из трёх атрибутов:
-
patch— при нажатии изменяет текущий LiveView на указанный путь -
navigate— при нажатии выполняет переход к новому LiveView по указанному пути -
href— при нажатии выполняет обычную навигацию браузера (как любой тег<a>)
Атрибуты
navigate(:string) — выполняет переход к LiveView. При переходе между LiveView страница браузера сохраняется, но запускается новый процесс LiveView, и его содержимое загружается на страницу. Переход возможен только между LiveView, объявленными в рамках одного маршрутизатораlive_session. При использовании вне LiveView или между live-сессиями ведёт себя как обычное перенаправление браузера.patch(:string) — обновляет текущий LiveView. Вызывается обратный вызовhandle_paramsтекущего LiveView, а по сети передаётся только минимально необходимое содержимое, как и при любом другом обновлении LiveView.href(:any) — выполняет обычную навигацию браузера к новому адресу. Это означает, что страница в браузере будет загружена заново.-
replace(:boolean) — следует ли при использовании:patchили:navigateзаменить запись в истории браузера с помощьюreplaceState?По умолчанию —
false. -
method(:string) — HTTP-метод ссылки. Предназначен для использования вне LiveView и работает только с атрибутомhref={...}. Не влияет на инструкцииpatchиnavigate.Если метод не равен
get, ссылка создаётся с атрибутомdata-method, который перехватывается библиотекой JavaScriptphoenix_htmlдля отправки формы. Для отправки формы в браузере должен быть включён JavaScript.По умолчанию —
"get". csrf_token(:any) — логическое значение или пользовательский токен для ссылок с HTTP-методом, отличным отget. По умолчанию —true.Поддерживаются глобальные атрибуты. Дополнительные HTML-атрибуты, добавляемые к тегу
a. Поддерживаются все глобальные атрибуты, а также:["download", "hreflang", "referrerpolicy", "rel", "target", "type"].
Слоты
-
inner_block(обязательный) — содержимое, отображаемое внутри тегаa.
Примеры
<.link href="/">Regular anchor link</.link>
<.link navigate={~p"/"} class="underline">home</.link>
<.link navigate={~p"/?sort=asc"} replace={false}>
Sort By Price
</.link>
<.link patch={~p"/details"}>view details</.link>
<.link href={URI.parse("https://elixir-lang.org")}>hello</.link>
<.link href="/the_world" method="delete" data-confirm="Really?">delete</.link>
Зависимость от JavaScript
Для поддержки ссылок, у которых :method не равно "get", или использования перечисленных выше атрибутов данных, Phoenix.HTML использует JavaScript. Можно добавить priv/static/phoenix_html.js в инструмент сборки.
Атрибуты данных
Атрибуты данных добавляются в виде списка ключевых слов, передаваемого ключу data. Поддерживаются следующие атрибуты данных:
-
data-confirm— отображает запрос подтверждения перед созданием и отправкой формы, если:methodне равно"get".
Изменение стандартного поведения подтверждения
phoenix_html.js действительно запускает пользовательское событие phoenix.link.click для нажатого элемента DOM при щелчке. Это позволяет перехватить событие при его всплытии к window и выполнить собственную логику, чтобы дополнить или изменить обработку атрибута data-confirm. Например, можно заменить поведение браузера confirm() собственной реализацией на 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);
Либо можно добавить собственное поведение.
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);
Последний вариант также можно привязать к любому событию click, но так вы будете уверены, что пользовательский код выполнится только при запуске кода phoenix_html.js.
Защита от CSRF
По умолчанию токены CSRF создаются с помощью Plug.CSRFProtection.
live_component(assigns)Источник
Функциональный компонент для отображения Phoenix.LiveComponent внутри родительского LiveView.
LiveView можно вкладывать друг в друга, но каждый LiveView запускает собственный процесс. LiveComponent предоставляет похожие возможности, но работает в том же процессе, что и LiveView, и имеет собственное инкапсулированное состояние. Поэтому такие компоненты называют компонентами с состоянием.
Атрибуты
id(:string) (обязательный) — уникальный идентификатор LiveComponent. Обратите внимание, чтоidне обязательно будет использоваться в качестве DOMid. Это определяется самим компонентом.module(:atom) (обязательный) — модуль LiveComponent для отображения.
Все дополнительные атрибуты передаются LiveComponent в виде map со значениями assign. Дополнительные сведения см. в разделе Phoenix.LiveComponent.
Примеры
<.live_component module={MyApp.WeatherComponent} id="thermostat" city="Kraków" /> live_file_input(assigns)Источник
Создаёт тег поля ввода файла для загрузки LiveView.
Атрибуты
-
upload(Phoenix.LiveView.UploadConfig) (обязательный) — структураPhoenix.LiveView.UploadConfig. -
accept(:string) — необязательное переопределение атрибута accept. По умолчанию используется значение :accept, указанное в allow_upload. - Допускаются глобальные атрибуты. Поддерживаются все глобальные атрибуты, а также:
["webkitdirectory", "required", "disabled", "capture", "form"].
Настройка метки
Атрибут id нельзя переопределить, но можно создать метку с атрибутом for, указывающим на ref UploadConfig:
<label for={@uploads.avatar.ref}>
<.live_file_input upload={@uploads.avatar} />
</label>
Перетаскивание
Перетаскивание поддерживается, если добавить контейнеру, в который можно перетаскивать файлы, атрибут phx-drop-target, указывающий на ref UploadConfig. Таким образом, для поддержки перетаскивания достаточно следующей разметки:
<label for={@uploads.avatar.ref} phx-drop-target={@uploads.avatar.ref}>
<.live_file_input upload={@uploads.avatar} />
</label>
Когда цель активна, она получает класс phx-drop-target-active. Дополнительные сведения см. в руководстве по загрузке файлов.
Примеры
Отрисовка поля ввода файла:
<.live_file_input upload={@uploads.avatar} />
Отрисовка поля ввода файла с меткой:
<label for={@uploads.avatar.ref}>Avatar</label>
<.live_file_input upload={@uploads.avatar} /> live_img_preview(assigns)Источник
Создаёт на клиенте предварительный просмотр выбранного файла в виде изображения.
Атрибуты
-
entry(Phoenix.LiveView.UploadEntry) (обязательный) — структураPhoenix.LiveView.UploadEntry. -
id(:string) — идентификатор тега img. По умолчанию он формируется на основе ref записи, но его можно переопределить, если требуется несколько раз отобразить предварительный просмотр одной и той же записи на одной странице. По умолчанию —nil. - Допускаются глобальные атрибуты.
Примеры
<.live_img_preview :for={entry <- @uploads.avatar.entries} entry={entry} width="75" />
Если компонент нужно использовать несколько раз, убедитесь, что у каждого экземпляра свой идентификатор
<.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" /> live_title(assigns)Источник
Отображает заголовок с автоматическим префиксом и суффиксом при обновлениях @page_title.
Атрибуты
-
prefix(:string) — префикс, добавляемый перед содержимымinner_block. По умолчанию —nil. -
default(:string) — заголовок по умолчанию, используемый, если внутренний блок пуст при обычном или подключённом монтировании. Примечание: пустые заголовки, напримерnilили пустая строка, заменяются значением по умолчанию. По умолчанию —nil. -
suffix(:string) — суффикс, добавляемый после содержимогоinner_block. По умолчанию —nil.
Слоты
-
inner_block(обязательный) — содержимое, отображаемое внутри тегаtitle.
Примеры
<.live_title default="Welcome" prefix="MyApp · ">
{assigns[:page_title]}
</.live_title>
<.live_title default="Welcome" suffix=" · MyApp">
{assigns[:page_title]}
</.live_title> Макросы
attr(name, type, opts \\ [])Источник
Объявляет атрибуты для функционального компонента HEEx.
Аргументы
name— атом, задающий имя атрибута. Обратите внимание: имя атрибута не должно совпадать с именем других атрибутов или слотов, объявленных для того же компонента.type— атом, задающий тип атрибута.opts— список ключевых слов с параметрами. По умолчанию —[].
Типы
Атрибут объявляется с указанием имени, типа и параметров. Поддерживаются следующие типы:
| Имя | Описание |
|---|---|
:any |
любой термин (включая nil) |
:string |
любая двоичная строка |
:atom |
любой атом (включая true, false и nil) |
:boolean |
любое логическое значение |
:integer |
любое целое число |
:float |
любое число с плавающей запятой |
:list |
любой список элементов произвольных типов |
:map |
любая карта с элементами произвольных типов |
:fun |
любая функция |
{:fun, arity} |
любая функция заданной арности |
:global |
любые распространённые атрибуты HTML, а также атрибуты, определённые в :global_prefixes
|
| Модуль структуры | любой модуль, определяющий структуру с помощью defstruct/1
|
Обратите внимание: только для :any и :atom ожидается, что значение будет равно nil.
Параметры
:required— помечает атрибут как обязательный. Если вызывающий код не передаст этот атрибут, будет выдано предупреждение при компиляции.:default— значение атрибута по умолчанию, если оно не задано. Если этот параметр не указан и атрибут не передан, обращение к атрибуту завершится ошибкой, если только значение явно не задано с помощьюassign_new/3.:examples— неполный список значений, допустимых для атрибута; используется для документации.:values— полный список значений, допустимых для атрибутов. Если вызывающий код передаст литерал, отсутствующий в этом списке, будет выдано предупреждение при компиляции.:doc— документация атрибута.
Проверки во время компиляции
LiveView проверяет некоторые атрибуты с помощью компилятора :phoenix_live_view. При определении атрибутов LiveView во время компиляции выдаёт вызывающему коду предупреждение в следующих случаях:
Не указан обязательный атрибут компонента.
Указан неизвестный атрибут.
Указан атрибут-литерал (например,
value="string"илиvalue, но неvalue={expr}), тип которого не соответствует ожидаемому. Сейчас проверка литералов поддерживается для следующих типов::string,:atom,:boolean,:integer,:float,:mapи:list.Указан атрибут-литерал, отсутствующий в списке
:values.
LiveView не выполняет проверки во время выполнения. Это означает, что сведения о типах используются главным образом для документации и рефлексии.
В самом компоненте LiveView определение атрибутов даёт следующие удобства:
Значения по умолчанию для всех атрибутов заранее добавляются в карту
assigns.Для компонента создаётся документация атрибутов.
Для обязательных типов структур добавляются аннотации и выдаются предупреждения при компиляции. Например, если указать
attr :user, User, required: true, а затем написать@user.non_valid_fieldв шаблоне, будет выдано предупреждение.Вызовы компонента отслеживаются для целей рефлексии и проверки.
Генерация документации
Типы и документация атрибутов, определённых в общедоступных функциональных компонентах, добавляются в документацию функции в зависимости от значения модульного атрибута @doc:
если
@doc— строка, документация атрибутов добавляется в эту строку. Необязательный заполнитель[INSERT LVATTRDOCS]позволяет указать место вставки документации. В противном случае документация добавляется в конец строки@doc.если
@docне задан, документация атрибутов используется как строка@docпо умолчанию.если
@docравенfalse, документация атрибутов полностью опускается.
Вставляемая документация атрибутов форматируется в виде списка Markdown:
-
name(:type) (обязательный) — документация атрибута. По умолчанию —:default.
По умолчанию типы и документация всех атрибутов добавляются в строку @doc функции. Чтобы скрыть конкретный атрибут, задайте для :doc значение false.
Пример
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 embed_templates(pattern, opts \\ [])Источник
Встраивает внешние файлы шаблонов в модуль в виде функциональных компонентов.
Параметры
-
:root— корневой каталог для встраивания файлов. По умолчанию используется каталог текущего модуля (__DIR__) -
:suffix— строка, добавляемая к именам встроенных функций. По умолчанию именами функций становятся имена файлов шаблонов без формата и движка.
Для выбора всех файлов в дереве каталогов можно использовать шаблон с подстановочными знаками. Например, представим, что каталог содержит следующие файлы:
├── components.ex ├── pages │ ├── about_page.html.heex │ └── welcome_page.html.heex
Затем встроим шаблоны страниц в модуль components.ex:
defmodule MyAppWeb.Components do use Phoenix.Component embed_templates "pages/*" end
Теперь в модуле будут определены функциональные компоненты about_page/1 и welcome_page/1. Встроенные шаблоны также поддерживают декларативные assigns, заданные с помощью объявлений функций без тела, например:
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
Также поддерживается многократный вызов embed_templates, что может быть полезно, если используется более одного формата шаблонов. Например:
defmodule MyAppWeb.Emails do use Phoenix.Component embed_templates "emails/*.html", suffix: "_html" embed_templates "emails/*.text", suffix: "_text" end
Примечание: эта функция совпадает с Phoenix.Template.embed_templates/2. Она также доступна здесь для удобства и в целях документирования. Поэтому, если вы хотите встраивать шаблоны других форматов, не связанных с Phoenix.Component, предпочтительнее использовать import Phoenix.Template, only: [embed_templates: 1], а не этот модуль.
sigil_H(arg, modifiers)Источник
Сигил ~H для написания шаблонов HEEx в исходных файлах.
HEEx — расширение Embedded Elixir (EEx), учитывающее HTML и удобное для компонентов. Оно предоставляет следующие возможности:
Встроенная обработка атрибутов HTML
Синтаксис, похожий на HTML, для вставки функциональных компонентов
Проверка структуры шаблона во время компиляции
Возможность уменьшить объём данных, передаваемых по сети
Форматирование кода из коробки с помощью
mix format
Пример
~H"""
<div title="My div" class={@class}>
<p>Hello {@name}</p>
<MyApp.Weather.city name="Kraków"/>
</div>
"""
Синтаксис
HEEx построен на основе Embedded Elixir (EEx). В этом разделе рассмотрены основные конструкции шаблонов HEEx, а также расширения их синтаксиса.
Интерполяция
HEEx позволяет использовать {...} для интерполяции с учётом HTML как в атрибутах тегов, так и в их содержимом:
<p>Hello, {@name}</p>
Чтобы интерполировать атрибут, напишите:
<div class={@class}>
...
</div>
Между { ... } можно поместить любое выражение Elixir. Например, чтобы задать классы, часть которых статическая, а часть динамическая, можно использовать интерполяцию строк:
<div class={"btn btn-#{@type}"}>
...
</div>
Следующие значения атрибутов имеют особое значение для тегов HTML:
true— если значение равноtrue, атрибут выводится без значения. Например,<input required={true}>эквивалентно<input required>;falseилиnil— если значение равноfalseилиnil, атрибут не выводится. Обратите внимание: атрибутыclassиstyleвыводятся как пустые строки, а не пропускаются. Это даёт тот же эффект, что и их отсутствие, но позволяет оптимизировать отрисовку.list(только для атрибутаclass) — каждый элемент списка обрабатывается как отдельный класс. Элементыnilиfalseотбрасываются. Вложенные списки поддерживаются и разворачиваются.
Для нескольких динамических атрибутов можно использовать ту же запись, не присваивая выражение какому-либо конкретному атрибуту:
<div {@dynamic_attrs}>
...
</div>
В этом случае выражение внутри {...} должно быть списком ключевых слов или картой, содержащей пары ключ-значение динамических атрибутов. При использовании карты убедитесь, что ключи являются атомами.
Интерполяция блоков
Синтаксис с фигурными скобками используется по умолчанию для интерполяции кода. Однако он подходит не для всех случаев, в частности:
Фигурные скобки нельзя использовать внутри тегов
<script>и<style>, иначе писать JS и CSS будет довольно утомительно. Также можно полностью отключить интерполяцию фигурных скобок в заданном теге и его дочерних элементах, добавив атрибутphx-no-curly-interpolationне поддерживаются многострочные блочные конструкции, такие как
if,caseи подобные
Например, чтобы интерполировать строку внутри тега script, можно написать:
<script> window.URL = "<%= @my_url %>" </script>
Аналогично, для блочных конструкций Elixir можно написать:
<%= if @show_greeting? do %>
<p>Hello, {@name}</p>
<% end %>
Однако для условных выражений и генераторов списков в HEEx есть встроенные конструкции, которые рассмотрим далее.
Фигурные скобки в тексте внутри тела тега
Если в тексте внутри тела тега есть фигурные скобки, можно использовать { или <%= "{" %>, чтобы они не воспринимались как начало интерполяции.
Специальные атрибуты
Помимо обычных атрибутов HTML, HEEx поддерживает специальные атрибуты, например :let и :for.
:let
Этот атрибут используется компонентами и слотами, которые передают значение вызывающему коду. Пример его работы см. в описании form/1:
<.form :let={f} for={@form} id="my-form" phx-change="validate" phx-submit="save">
<.input field={f[:username]} type="text" />
...
</.form>
Обратите внимание, как переменная f, определённая с помощью .form, используется компонентом input. В модуле Phoenix.Component подробно описано, как использовать и реализовать эту функциональность.
:if и :for
Это синтаксический сахар для <%= if .. do %> и <%= for .. do %>, который можно использовать в обычном HTML, функциональных компонентах и слотах.
Например, в теге HTML:
<table id="admin-table" :if={@admin?}>
<tr :for={user <- @users}>
<td>{user.name}</td>
</tr>
<table>
Приведённый выше фрагмент отобразит таблицу, только если @admin? истинно, и создаст tr для каждого пользователя, как и ожидается при обходе коллекции.
:for аналогичным образом можно использовать в функциональных компонентах:
<.error :for={msg <- @errors} message={msg}/>
Это эквивалентно следующей записи:
<%= for msg <- @errors do %>
<.error message={msg} />
<% end %>
В слотах :for работает так же:
<.table id="my-table" rows={@users}>
<:col :for={header <- @headers} :let={user}>
<td>{user[header]}</td>
</:col>
<.table>
Также можно объединить :for и :if для тегов, компонентов и слотов, чтобы использовать их как фильтр:
<.error :for={msg <- @errors} :if={msg != nil} message={msg} />
Обратите внимание: в отличие от обычного for в Elixir, :for в HEEx не поддерживает несколько генераторов в одном выражении. В таких случаях необходимо использовать блоки из EEx.
Отслеживание изменений :for в слотах
В отличие от обычных тегов HTML и компонентов, LiveView не оптимизирует генераторы списков в слотах. Это означает, что при изменении @headers в приведённом выше примере все заголовки повторно передаются по сети.
Кроме того, сейчас в слотах также не поддерживается :key (см. ниже).
:key-генераторы списков
При использовании :for можно дополнительно указать выражение :key для более эффективного отслеживания изменений внутри генератора списка:
<ul>
<li :for={%{id: id, name: name} <- @items} :key={id}>
Count: <span>{@count}</span>,
item: {name}
</li>
</ul>
По умолчанию в качестве ключа используется индекс, поэтому при добавлении записи изменившимися считаются все элементы. Поэтому, по возможности, рекомендуем использовать :key.
Обратите внимание: :key не действует при использовании потоков.
Функциональные компоненты
Функциональные компоненты — это не имеющие состояния компоненты, реализованные как чистые функции с помощью модуля Phoenix.Component. Они могут быть локальными (в том же модуле) или удалёнными (во внешнем модуле).
HEEx позволяет вызывать такие функциональные компоненты непосредственно в шаблоне с помощью записи, похожей на HTML. Например, удалённая функция:
<MyApp.Weather.city name="Kraków"/>
Локальную функцию можно вызвать, добавив точку в начале имени:
<.city name="Kraków"/>
а компонент можно определить следующим образом:
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
Обычно лучше объединять связанные функции в одном модуле, а не создавать множество модулей с единственной функцией render/1. Функциональные компоненты поддерживают и другие важные возможности, например слоты. Подробнее о компонентах см. в Phoenix.Component.
Форматирование кода
Шаблоны HEEx (.heex) и сигилы ~H можно форматировать автоматически с помощью Phoenix.LiveView.HTMLFormatter. Дополнительные сведения см. в документации этого модуля.
slot(name, opts \\ [])Источник
Объявляет слот. Дополнительные сведения см. в описании slot/3.
slot(name, opts, block)Источник
Объявляет слот функционального компонента.
Аргументы
name— атом, задающий имя слота. Обратите внимание: имя слота не должно совпадать с именем других слотов или атрибутов, объявленных для того же компонента.opts— список ключевых слов с параметрами. По умолчанию —[].block— блок кода с вызовамиattr/3. По умолчанию —nil.
Параметры
:required— помечает слот как обязательный. Если вызывающий код не передаст значение для обязательного слота, будет выдано предупреждение при компиляции. В противном случае пропущенный слот по умолчанию будет равен[].:validate_attrs— если задано значениеfalse, предупреждение не выдаётся, когда вызывающий код передаёт атрибуты слоту, объявленному без блока do. Если параметр не задан, по умолчанию используетсяtrue.:doc— документация слота. Документация объявленных атрибутов слота приводится вместе с документацией слота.
Атрибуты слота
Для именованного слота можно объявить атрибуты, передав блок с вызовами attr/3.
В отличие от атрибутов, для атрибутов слота нельзя задать параметр :default. Его передача приведёт к ошибке компиляции.
Слот по умолчанию
Слот по умолчанию можно объявить, передав :inner_block в качестве name слота.
Обратите внимание: объявление слота :inner_block не может принимать блок. Его передача приведёт к ошибке компиляции.
Проверки во время компиляции
LiveView проверяет некоторые слоты с помощью компилятора :phoenix_live_view. При определении слотов LiveView во время компиляции выдаёт вызывающему коду предупреждение в следующих случаях:
Не указан обязательный слот компонента.
Указан неизвестный слот.
Указан неизвестный атрибут слота.
В самом функциональном компоненте определение атрибутов даёт следующие удобства:
Для компонента создаётся документация слотов.
Вызовы компонента отслеживаются для целей рефлексии и проверки.
Генерация документации
Документация слотов, определённых в общедоступных функциональных компонентах, добавляется в документацию функции в зависимости от значения модульного атрибута @doc:
если
@doc— строка, документация слотов добавляется в эту строку. Необязательный заполнитель[INSERT LVATTRDOCS]позволяет указать место вставки документации. В противном случае документация добавляется в конец строки@doc.если
@docне задан, документация слотов используется как строка@docпо умолчанию.если
@docравенfalse, документация слотов полностью опускается.
Вставляемая документация слотов форматируется в виде списка Markdown:
-
name(обязательный) — документация слота. Поддерживает атрибуты:-
name(:type) (обязательный) — документация атрибута. По умолчанию —:default.
-
По умолчанию документация всех слотов добавляется в строку @doc функции. Чтобы скрыть конкретный слот, задайте для :doc значение false.
Пример
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
Как показано в примере выше, render_slot/1 возвращает nil, если объявлен необязательный слот, но значение для него не передано. Это можно использовать для задания поведения по умолчанию.
Функции
assign(socket_or_assigns, keyword_or_map)Источник
Добавляет пары ключ-значение в assigns.
Первый аргумент — это либо socket LiveView, либо карта assigns из функциональных компонентов.
Если вторым аргументом передан список ключевых слов или карта, они будут объединены с существующими assigns. Если передана функция, она получает текущие assigns в качестве аргумента, а её возвращаемое значение будет объединено с текущими assigns.
Примеры
iex> assign(socket, name: "Elixir", logo: "💧")
iex> assign(socket, %{name: "Elixir"})
iex> assign(socket, fn %{name: name, logo: logo} -> %{title: Enum.join([name, logo], " | ")} end) assign(socket_or_assigns, key, value)Источник
Добавляет пару key-value в socket_or_assigns.
Первый аргумент — это либо socket LiveView, либо карта assigns из функциональных компонентов.
Примеры
iex> assign(socket, :name, "Elixir")
assign_new(socket_or_assigns, key, fun)Источник
Добавляет заданный key со значением из fun в socket_or_assigns, если он ещё не существует.
Первый аргумент — это либо socket LiveView, либо карта assigns из функциональных компонентов.
Эта функция полезна для отложенного добавления значений и совместного использования assigns. Далее мы рассмотрим оба варианта использования.
Отложенное добавление assigns
Представьте, что у вас есть функциональный компонент, принимающий цвет:
<.my_component bg_color="red" />
Цвет также является необязательным, поэтому его можно не указывать:
<.my_component />
В таких случаях реализация может использовать assign_new, чтобы отложенно задать цвет, если он не указан. Сделаем так, чтобы при отсутствии цвета выбирался случайный:
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
Совместное использование assigns
Можно совместно использовать assigns между конвейером Plug и LiveView при отключённом рендеринге, а также между родительским и дочерним LiveView при подключении.
При отключении
При первом обращении пользователя к приложению с LiveView LiveView сначала отображается в отключённом состоянии в составе обычного HTML-ответа. Используя assign_new в обратном вызове mount вашего LiveView, вы можете указать LiveView повторно использовать любые assigns, уже заданные в conn в отключённом состоянии.
Представьте, что у вас есть Plug, который выполняет следующее:
# 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
Вы можете повторно использовать assign :current_user в LiveView при первоначальном рендеринге:
def mount(_params, %{"user_id" => user_id}, socket) do
{:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end
В этом случае будет использован conn.assigns.current_user, если он задан. Если такого assign :current_user нет или LiveView был смонтирован в рамках live-навигации, при которой конвейеры Plug не вызываются, вместо этого будет вызвана анонимная функция для выполнения запроса.
При подключении
LiveView также может совместно использовать assigns с дочерними LiveView через assign_new, если дочерний LiveView монтируется одновременно с родительским и не отображается с помощью sticky: true. Рассмотрим пример.
Если родительский LiveView задаёт assign :current_user, а дочерний LiveView также использует assign_new/3, чтобы получить :current_user в своём обратном вызове mount/3, как в предыдущем подразделе, assign будет получен из родительского LiveView, что вновь позволит избежать дополнительных запросов к базе данных.
Обратите внимание, что fun также предоставляет доступ к ранее заданным значениям:
assigns =
assigns
|> assign_new(:foo, fn -> "foo" end)
|> assign_new(:bar, fn %{foo: foo} -> foo <> "bar" end)
Совместное использование assigns выполняется, когда это возможно, но не гарантируется. Поэтому необходимо убедиться, что результат функции, переданной в assign_new/3, совпадает с результатом получения значения из родительского компонента. В противном случае передавайте значения дочернему LiveView в составе его session.
assigns_to_attributes(assigns, exclude \\ [])Источник
Фильтрует assigns и возвращает список ключевых слов для использования в динамических атрибутах тегов.
Предпочтительно использовать декларативные assigns и атрибуты :global вместо этой функции.
Примеры
Представьте следующий компонент my_link, который позволяет вызывающему коду передать assign new_window, а также любые другие атрибуты, которые он хочет добавить элементу, например class, атрибуты data и т. д.:
<.my_link to="/" id={@id} new_window={true} class="my-class">Home</.my_link>
Поддержать динамические атрибуты можно с помощью следующего компонента:
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
В результате будет сформирован следующий HTML:
<a href="/" target="_blank" id="1" class="my-class">Home</a>
Второй аргумент (необязательный) функции assigns_to_attributes — это список ключей, которые нужно исключить. Обычно в него входят зарезервированные ключи самого компонента, которые не должны присутствовать в разметке либо уже явно обрабатываются компонентом.
changed?(socket_or_assigns, key)Источник
Проверяет, изменился ли заданный ключ в socket_or_assigns.
Первый аргумент — это либо socket LiveView, либо карта assigns из функциональных компонентов.
Примеры
iex> changed?(socket, :count)
live_flash(other, key)Источник
Возвращает flash-сообщение из assign flash в LiveView.
Примеры
<p class="alert alert-info">{live_flash(@flash, :info)}</p>
<p class="alert alert-danger">{live_flash(@flash, :error)}</p> live_render(conn_or_socket, view, opts \\ [])Источник
Отображает LiveView внутри шаблона.
Это полезно в двух случаях:
При отображении дочернего LiveView внутри LiveView.
При отображении LiveView внутри обычного (не live) контроллера/представления.
В большинстве других случаев, связанных с общей функциональностью, включая управление состоянием и взаимодействие с пользователем, можно обойтись функциональными компонентами или LiveComponents
Параметры
:session— карта с бинарными ключами и дополнительными данными сессии, которые будут сериализованы и отправлены клиенту. Все данные сессии, имеющиеся в подключении, автоматически доступны в LiveView. Используйте этот параметр, чтобы передать дополнительные данные. Помните, что все данные сессии сериализуются и отправляются клиенту, поэтому в сессии следует хранить как можно меньше данных. Например, вместо хранения структуры User следует сохранить "user_id" и загрузить User при монтировании LiveView.:container— необязательный кортеж, задающий HTML-тег и атрибуты DOM контейнера LiveView. Например:{:li, style: "color: blue;"}. По умолчанию используется контейнер, заданный в определении модуля. Дополнительные сведения см. в разделе «Контейнеры» ниже.:id— идентификатор DOM и идентификатор для уникальной идентификации LiveView. При отображении корневых LiveView:idгенерируется автоматически, но при отображении дочернего LiveView этот параметр обязателен.:sticky— необязательный флаг, позволяющий сохранять LiveView при live-перенаправлениях, даже если он вложен в другой LiveView. Обратите внимание, что это работает только для LiveView в одной и той же live_session. Если вы отображаете закреплённое представление внутри другого LiveView, убедитесь, что само закреплённое представление не использует ту же раскладку. Для этого можно вернуть{:ok, socket, layout: false}из mount.
Примеры
При вызове из контроллера/представления можно написать:
{live_render(@conn, MyApp.ThermostatLive)}
Или:
{live_render(@conn, MyApp.ThermostatLive, session: %{"home_id" => @home.id})}
Внутри другого LiveView необходимо передать параметр :id:
{live_render(@socket, MyApp.ThermostatLive, id: "thermostat")}
Контейнеры
При отображении LiveView его содержимое помещается в контейнер. По умолчанию контейнером служит тег div с несколькими атрибутами, специфичными для LiveView.
Контейнер можно настроить несколькими способами:
-
Можно изменить значение
containerпо умолчанию вuse Phoenix.LiveView:use Phoenix.LiveView, container: {:tr, id: "foo-bar"} -
Можно переопределить тег контейнера и передать дополнительные атрибуты при вызове
live_render(а также при вызовеliveв роутере):live_render socket, MyLiveView, container: {:tr, class: "highlight"}
Если контейнер не должен влиять на макет, можно использовать CSS-свойство display: contents или класс, который его применяет, например .contents из Tailwind.
Будьте осторожны, если задаёте значение :body: любое содержимое, внедрённое внутрь body (например, функции Phoenix.LiveReload), будет удалено после подключения LiveView.
Тестирование
Обратите внимание: render_click/1 и другие функции тестирования отправляют события корневому LiveView. Чтобы взаимодействовать с вложенными LiveView в live-тестах, используйте find_live_child/2.
portal(assigns)Источник
Отображает портал.
Портал — это компонент, который перемещает своё содержимое в другое место DOM. Он полезен, когда содержимое нужно отобразить в другом месте, например из-за переполнения или контекста наложения.
Портал состоит из двух частей:
- Источник портала: компонент, который нужно переместить.
- Цель портала: элемент DOM, в котором будет отображаться содержимое источника портала.
Целью портала может быть любой элемент. В большинстве случаев цель отображается внутри раскладки приложения. Источники портала необходимо определять с помощью компонента .portal.
Примечание о тестировании
Поскольку порталы используют под капотом элементы <template>, при использовании Phoenix.LiveViewTest.element/3 и других функций LiveViewTest нельзя выполнять поиск элементов внутри портала.
Вместо этого преобразуйте сам элемент портала с помощью Phoenix.LiveViewTest.render/1 в строку HTML и выполняйте проверки для неё:
<.portal id="my-portal" target="body"> <div id="something-inside">...</div> </.portal>
# in your test, instead of
# assert has_element?(view, "#something-inside") <-- this won't work
html = view |> element("#my-portal") |> render()
assert html =~ "something-inside"Примеры
<.portal id="modal" target="body"> ... </.portal>
Атрибуты
-
id(:string) (обязательный) -
target(:string) (обязательный) — CSS-селектор, задающий цель. Цель должна быть уникальной. -
class(:any) — класс, применяемый к обёртке портала. По умолчанию —nil. -
container(:string) — HTML-тег, используемый для обёртки портала. По умолчанию —"div".
Слоты
-
inner_block(обязательный)
render_slot(slot, argument \\ nil)Источник
Отображает элемент слота с заданным необязательным argument.
{render_slot(@inner_block, @form)}
Если у слота нет элементов, возвращается nil.
Если для одного слота определено несколько элементов, render_slot/2 автоматически отобразит их все, объединив содержимое. Если необходимо использовать атрибуты элементов, следует перебрать список, чтобы получить доступ к каждому элементу слота по отдельности.
Например, представьте компонент таблицы:
<.table rows={@users}>
<:col :let={user} label="Name">
{user.name}
</:col>
<:col :let={user} label="Address">
{user.address}
</:col>
</.table>
На верхнем уровне мы передаём строки как assign и определяем слот :col для каждого столбца таблицы. У каждого столбца также есть label, который мы будем использовать в заголовке таблицы.
Внутри компонента можно отобразить таблицу с заголовками, строками и столбцами:
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 to_form(data_or_params, options \\ [])Источник
Преобразует заданную структуру данных в Phoenix.HTML.Form.
Эта функция обычно используется для преобразования карты или changeset Ecto в форму, передаваемую компоненту form/1.
Создание формы из параметров
Чтобы создать форму на основе параметров handle_event, можно написать:
def handle_event("submitted", params, socket) do
{:noreply, assign(socket, form: to_form(params))}
end
При передаче карты в to_form/1 предполагается, что она содержит параметры формы, у которых должны быть строковые ключи.
Также можно указать имя, чтобы вложить параметры:
def handle_event("submitted", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
end
Создание формы из changeset
При использовании changeset из него извлекаются исходные данные, параметры формы и ошибки. Параметр :as также вычисляется автоматически. Например, если у вас есть схема пользователя:
defmodule MyApp.Users.User do
use Ecto.Schema
schema "..." do
...
end
end
Затем создайте changeset и передайте его в to_form:
%MyApp.Users.User{}
|> Ecto.Changeset.change()
|> to_form()
В этом случае после отправки формы параметры будут доступны в %{"user" => user_params}.
Параметры
-
:as— префиксname, используемый в полях формы -
:id— префиксid, используемый в полях формы -
:errors— список ошибок в формате ключевых слов (используется только для карт) -
:action— действие, выполненное с формой. Это значение можно использовать, чтобы различать разные операции, например ввод данных пользователем в форму для проверки или отправку формы для добавления записи в базу данных. Например:to_form(changeset, action: :validate)илиto_form(changeset, action: :save). Переданное действие передаётся параметрам реализацииPhoenix.HTML.FormData.
При преобразовании исходных данных в форму могут приниматься дополнительные параметры. Например, карта принимает :errors для указания ошибок, но changeset такой параметр не поддерживает. :errors — это список ключевых слов из кортежей вида {error_message, options_list}. Пример:
to_form(%{"search" => nil}, errors: [search: {"Can't be blank", []}])
Если передана существующая структура Phoenix.HTML.Form, указанные выше параметры переопределят её существующие значения. Остальные параметры будут объединены с уже имеющимися параметрами формы.
Ошибки формы отображаются только в том случае, если поле changeset action задано (и его значение не равно :ignore). Кроме того, ошибки можно фильтровать в зависимости от того, использовались ли поля на клиенте. Дополнительные сведения см. в примечании о :errors.
update(socket_or_assigns, key, fun)Источник
Обновляет существующий key с помощью fun в заданном socket_or_assigns.
Первый аргумент — это либо socket LiveView, либо карта assigns из функциональных компонентов.
Функция обновления получает текущее значение ключа и возвращает обновлённое значение. Если ключ не существует, возникает ошибка.
Функция обновления также может иметь арность 2. В этом случае она получает текущее значение ключа первым аргументом, а текущие assigns — вторым. Если ключ не существует, возникает ошибка.
Примеры
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) upload_errors(conf)Источник
Возвращает ошибки загрузки в целом.
Чтобы получить ошибки, относящиеся к отдельной записи загрузки, используйте upload_errors/2.
Результат — список. Может быть возвращена следующая ошибка:
-
:too_many_files— количество выбранных файлов превышает ограничение:max_entries
Примеры
def upload_error_to_string(:too_many_files), do: "You have selected too many files"
<div :for={err <- upload_errors(@uploads.avatar)} class="alert alert-danger">
{upload_error_to_string(err)}
</div> upload_errors(conf, entry)Источник
Возвращает ошибки для записи загрузки.
Чтобы получить ошибки, относящиеся к загрузке в целом, используйте upload_errors/1.
Результат — список. Могут быть возвращены следующие ошибки:
-
:too_large— запись превышает ограничение:max_file_size -
:not_accepted— запись не соответствует типам MIME:accept -
:external_client_failure— сбой внешней загрузки -
{:writer_failure, reason}— сбой пользовательского механизма записи сreason -
reason— сбой пользовательского валидатора сreason
Примеры
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" defp upload_error_to_string(:custom_validator_error), do: "Custom validation error"
<%= 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 %> used_input?(form_field)Источник
Проверяет, использовалось ли поле ввода на клиенте.
Использованными считаются только поля ввода, на которых клиент установил фокус, с которыми взаимодействовал или которые отправил. В LiveView эта функция фильтрует ошибки из реализации Phoenix.HTML.FormData, чтобы не отображать сообщение «поле не может быть пустым» в случаях, когда клиент ещё не взаимодействовал с соответствующим полем.
Использованные поля отслеживаются внутренним механизмом: клиент отправляет соседний ключ, производный от имени каждого поля ввода, который указывает поля, ещё не использованные на клиенте. Например, если форма содержит поля email и title, а на клиенте пока изменено только поле title, будет отправлена следующая полезная нагрузка:
%{
"title" => "new title",
"email" => "",
"_unused_email" => ""
}
Ключ _unused_email указывает, что поле email не использовалось на клиенте; эта информация применяется для фильтрации ошибок в интерфейсе.
Поддерживаются также вложенные поля. Например, вложенное поле datetime считается использованным, если использован хотя бы один из вложенных параметров.
%{
"bday" => %{
"year" => "",
"month" => "",
"day" => "",
"_unused_day" => ""
}
}
Ключ _unused_day указывает, что поле дня не использовалось на клиенте, но поля года и месяца использовались, то есть поле даты рождения в целом было использовано.
Примеры
Представьте, что в шаблоне вы отображаете поля ввода title и email. После первоначальной загрузки конечный пользователь начинает вводить текст в поле title. Клиент отправит на сервер всю полезную нагрузку формы с введённым значением title и пустым значением email.
В этой ситуации реализация Phoenix.HTML.FormData сочтёт пустое поле email недопустимым, но пользователь не должен видеть ошибку, поскольку он ещё не использовал это поле. Для этого можно использовать used_input?/1, чтобы фильтровать ошибки на клиенте, обращаясь к метаданным параметров и определяя, какие поля ввода использовались. В компонентах, отличных от LiveView, все поля ввода считаются использованными.
<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>
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/Phoenix.Component.html