Spec-Zone.ru › Phoenix

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], а не через переменную let form

  • Changeset 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, который перехватывается библиотекой JavaScript phoenix_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 не обязательно будет использоваться в качестве DOM id. Это определяется самим компонентом.

  • 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 есть встроенные конструкции, которые рассмотрим далее.

Фигурные скобки в тексте внутри тела тега

Если в тексте внутри тела тега есть фигурные скобки, можно использовать &lbrace; или <%= "{" %>, чтобы они не воспринимались как начало интерполяции.

Специальные атрибуты

Помимо обычных атрибутов 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)Источник

Эта функция устарела. Используйте Phoenix.Flash.get/2 в Phoenix v1.7 и новее.

Возвращает 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. Он полезен, когда содержимое нужно отобразить в другом месте, например из-за переполнения или контекста наложения.

Портал состоит из двух частей:

  1. Источник портала: компонент, который нужно переместить.
  2. Цель портала: элемент 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>

Пакет Hex Предпросмотр Hex Просмотреть llms.txt Скачать версию ePub

Создано с помощью ExDoc (v0.40.3) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/Phoenix.Component.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API