Phoenix.LiveView поведение
Исходный кодLiveView — это процесс, который получает события, обновляет свое состояние и отображает изменения на странице в виде различий.
Чтобы приступить к работе, ознакомьтесь с руководством по началу работы. Этот модуль содержит расширенную документацию и сведения о возможностях LiveView.
Жизненный цикл
LiveView начинает работу как обычный HTTP-запрос с HTML-ответом, а при подключении клиента преобразуется в представление с состоянием. При этом обычная HTML-страница будет доступна даже при отключенном JavaScript. При каждом изменении представления с состоянием или обновлении назначений сокета оно автоматически перерисовывается, а изменения отправляются клиенту.
Назначения сокета — это значения с состоянием, которые хранятся на стороне сервера в Phoenix.LiveView.Socket. Это отличается от распространенной модели HTTP без сохранения состояния, в которой состояние соединения отправляется клиенту в виде токена или cookie, а сервер восстанавливает его для обработки каждого запроса.
Обычно работа начинается с отображения LiveView из маршрутизатора. При первом отображении LiveView вызывается обратный вызов mount/3 с текущими параметрами, текущим сеансом и сокетом LiveView. Как и при обычном запросе, params содержит общедоступные данные, которые пользователь может изменить. session всегда содержит закрытые данные, заданные самим приложением. Обратный вызов mount/3 настраивает назначения сокета, необходимые для отображения представления. После монтирования вызывается handle_params/3 для обработки URI и параметров запроса. Наконец, вызывается render/1, и HTML отправляется клиенту как обычный HTML-ответ.
После отображения статической страницы LiveView подключается с клиента к серверу, где запускаются представления с состоянием, отправляющие отображаемые обновления браузеру и принимающие события клиента через привязки phx-. Как и при первом отображении, вызывается mount/3 с параметрами, сеансом и состоянием сокета. Однако при подключении клиента на сервере запускается процесс LiveView, который снова выполняет handle_params/3, а затем отправляет клиенту результат render/1 и продолжает работу до завершения соединения. Если на любом этапе жизненного цикла с сохранением состояния происходит сбой или соединение с клиентом прерывается, клиент корректно подключается к серверу повторно, снова вызывая mount/3 и handle_params/3.
LiveView также позволяет добавлять хуки на определенные этапы жизненного цикла с помощью attach_hook/4.
Совместное размещение шаблонов
В LiveView можно отображать содержимое двумя способами. Первый — явно определить функцию render, которая получает assigns и возвращает шаблон HEEx, определенный с помощью сигила ~H.
defmodule MyAppWeb.DemoLive do
# In a typical Phoenix app, the following line would usually be `use MyAppWeb, :live_view`
use Phoenix.LiveView
def render(assigns) do
~H"""
Hello world!
"""
end
end
Для больших шаблонов можно поместить их в файл в том же каталоге и с тем же именем, что и у LiveView. Например, если файл выше находится по пути lib/my_app_web/live/demo_live.ex, можно также полностью удалить функцию render/1 и поместить код шаблона в lib/my_app_web/live/demo_live.html.heex.
Асинхронные операции
Выполнение асинхронных задач — обычная практика в LiveView и LiveComponent. Это позволяет пользователю быстро получить работающий интерфейс, пока система загружает данные в фоновом режиме или обращается к внешней службе, не блокируя отображение или обработку событий. При выполнении асинхронных задач обычно также нужно обрабатывать различные состояния операции, такие как загрузка, ошибка и успешный результат. Кроме того, необходимо перехватывать ошибки и завершения процессов и преобразовывать их в осмысленные обновления интерфейса, а не допускать сбоя пользовательского сценария.
Асинхронные назначения
Функция assign_async/3 получает сокет, ключ или список ключей, которым будут назначены значения асинхронно, и функцию. Эта функция будет обернута в task функцией assign_async, поэтому вернуть результат будет просто. Функция должна возвращать кортеж {:ok, assigns} или {:error, reason}, где assigns — это карта ключей, переданных в assign_async. Если функция возвращает что-либо другое, возникает ошибка.
Задача запускается только при подключенном сокете.
Например, предположим, что мы хотим асинхронно получить из базы данных организацию пользователя, а также его профиль и рейтинг:
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
Предупреждение
При использовании асинхронных операций важно не передавать сокет в функцию: в противном случае вся структура сокета будет скопирована в процесс Task, что может обойтись очень дорого.
Вместо этого:
assign_async(:org, fn -> {:ok, %{org: fetch_org(socket.assigns.slug)}} end)
Следует сделать так:
slug = socket.assigns.slug
assign_async(:org, fn -> {:ok, %{org: fetch_org(slug)}} end)
См. https://elixir.hexdocs.pm/process-anti-patterns.html#sending-unnecessary-data
Состояние асинхронной операции хранится в назначениях сокета как Phoenix.LiveView.AsyncResult. Оно содержит состояния загрузки и ошибки, а также результат. Например, чтобы показать в интерфейсе состояния загрузки для :org, можно условно отображать состояния в шаблоне:
<div :if={@org.loading}>Loading organization...</div>
<div :if={org = @org.ok? && @org.result}>{org.name} loaded!</div>
Функциональный компонент Phoenix.Component.async_result/1 также позволяет декларативно отображать различные состояния с помощью слотов:
<.async_result :let={org} assign={@org}>
<:loading>Loading organization...</:loading>
<:failed :let={_failure}>there was an error loading the organization</:failed>
{org.name}
</.async_result>
Произвольные асинхронные операции
Иногда требуется более низкоуровневый контроль над асинхронными операциями, но при этом необходимо сохранить изоляцию процессов и обработку ошибок. Для этого можно использовать start_async/3 и непосредственно модуль Phoenix.LiveView.AsyncResult:
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_task, fn -> fetch_org!(id) end)}
end
def handle_async(:my_task, {:ok, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_task, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
start_async/3 используется для асинхронной загрузки данных об организации. Обратный вызов handle_async/3 вызывается при завершении задачи или выходе из нее; результаты передаются в виде {:ok, result} или {:exit, reason}. Модуль AsyncResult предоставляет функции для обновления состояния асинхронной операции, но при необходимости можно также напрямую назначить любое значение сокету и самостоятельно обрабатывать состояние.
Примечание о связанных процессах
LiveView обеспечивает остановку асинхронных операций при завершении самого LiveView, чтобы предотвратить ненужную работу, например длительные операции с базой данных, которые продолжались бы после ухода пользователя со страницы. Для этого процесс LiveView связывается с процессом, выполняющим функцию, переданную в assign_async/4 / start_async/4. LiveView перехватывает любые ошибки из запущенного процесса, однако если этот процесс связан с другим процессом, который затем аварийно завершается, это приведет к завершению всех связанных процессов в цепочке, в том числе и вашего LiveView.
Распространенный источник связей
Один из наиболее распространенных источников связей в коде — модуль Task:
assign_async(socket, :org, fn -> # Task.async links the new process to the caller! ... = Task.async(fn -> raise "oops" end) |> Task.await() end)
Поскольку Task.async/1 связывает запущенный процесс с вызывающим процессом, при завершении запущенного процесса по любой причине завершится и LiveView.
Даже если вы напрямую не вызываете Task.async/1 в одной из асинхронных функций LiveView, вызов, создающий связь, может находиться глубже в вызываемой функции, поэтому его легко не заметить.
Есть несколько способов решить эту проблему.
- Не допускайте завершения связанных процессов: перехватывайте исключения и выходы в вызовах, которые не должны приводить к сбою LiveView.
- Не связывайте процессы: используйте
Task.SupervisorиTask.Supervisor.async_nolink/3(учтите, что в этом случае асинхронные процессы не остановятся при завершении LiveView!). - Перехватывайте выходы в LiveView, вызвав
Process.flag(:trap_exit, true). Как правило, перехватывать выходы не рекомендуется, поскольку это меняет реакцию LiveView на все выходы, а не только на связанные с асинхронными операциями.
Настройка конечной точки
LiveView принимает следующие параметры конфигурации в конечной точке в разделе :live_view:
:signing_salt(обязательный) — соль, используемая для подписи данных, отправляемых клиенту:hibernate_after(необязательный) — время бездействия в миллисекундах, по истечении которого LiveView сжимает собственную память и состояние. По умолчанию — 15000 мс (15 секунд)
Сводка
Типы
- handle_async(name, async_fun_result, socket)
Вызывается, когда становится доступен результат операции
start_async/3.- handle_call(msg, tuple, socket)
Вызывается для обработки вызовов из других процессов Elixir.
- handle_cast(msg, socket)
Вызывается для обработки сообщений cast из других процессов Elixir.
- handle_event(event, payload, socket)
Вызывается для обработки событий, отправленных клиентом.
- handle_info(msg, socket)
Вызывается для обработки сообщений от других процессов Elixir.
- handle_params(params, uri, socket)
Вызывается после монтирования и при каждом событии live patch.
- mount(params, session, socket)
Точка входа LiveView.
- render(assigns)
Отображает шаблон.
- terminate(reason, socket)
Вызывается при завершении LiveView.
Обратные вызовы
- __live__(opts \\ [])
Определяет метаданные LiveView.
- __using__(opts)
Подключает LiveView в текущем модуле, помечая его как LiveView.
- allow_upload(socket, name, options)
Разрешает загрузку для указанного имени.
- assign_async(socket, key_or_keys, func, opts \\ [])
Асинхронно назначает значения ключам.
- attach_hook(socket, name, stage, fun)
Добавляет указанный
funс помощьюnameна этапе жизненного циклаstageвsocket.- cancel_async(socket, async_or_keys, reason \\ {:shutdown, :cancel})
Отменяет асинхронную операцию, если она существует.
- cancel_upload(socket, name, entry_ref)
Отменяет загрузку указанной записи.
- clear_flash(socket)
Очищает flash-сообщения.
- clear_flash(socket, key)
Удаляет ключ из flash-сообщений.
- connected?(socket)
Возвращает true, если сокет подключен.
- consume_uploaded_entries(socket, name, func)
Обрабатывает загруженные записи.
- consume_uploaded_entry(socket, entry, func)
Обрабатывает отдельную загруженную запись.
- detach_hook(socket, name, stage)
Удаляет хук с указанным
nameиз этапа жизненного циклаstage.- disallow_upload(socket, name)
Отзывает ранее выданное функцией
allow_upload/3разрешение на загрузку.- get_connect_info(socket, key)
Получает из сокета заданное значение сведений о подключении.
- get_connect_params(socket)
Получает параметры подключения, отправленные клиентом для использования при подключенном монтировании.
- on_mount(mod_or_mod_arg)
Объявляет обратный вызов модуля, который будет вызван при монтировании LiveView.
- push_event(socket, event, payload, opts \\ [])
Отправляет событие клиенту.
- push_navigate(socket, opts)
Помечает сокет для перехода к другому LiveView в том же
live_session.- push_patch(socket, opts)
Помечает сокет для перехода в пределах текущего LiveView.
- put_flash(socket, kind, msg)
Добавляет в сокет flash-сообщение для отображения.
- put_private(socket, key, value)
Добавляет в сокет новую закрытую пару «ключ — значение».
- redirect(socket, opts \\ [])
Помечает сокет для перенаправления по указанному пути назначения.
- render_with(socket, component)
Настраивает функцию для отображения LiveView/LiveComponent.
- send_update(pid \\ self(), module_or_cid, assigns)
Асинхронно обновляет
Phoenix.LiveComponent, назначая ему новые значения.- send_update_after(pid \\ self(), module_or_cid, assigns, time_in_milliseconds)
Аналогична функции
send_update/3, но обновление будет отложено на указанноеtime_in_milliseconds.- start_async(socket, name, func, opts \\ [])
Оборачивает функцию в асинхронную задачу и вызывает обратный вызов
nameдля обработки результата.- static_changed?(socket)
Возвращает true, если сокет подключен и отслеживаемые статические ресурсы изменились.
- stream(socket, name, items, opts \\ [])
Назначает сокету новый поток или добавляет элементы в существующий поток. Возвращает обновленный
socket.- stream_async(socket, name, func, opts \\ [])
Асинхронно добавляет данные в поток.
- stream_configure(socket, name, opts)
Настраивает поток.
- stream_delete(socket, name, item)
Удаляет элемент из потока.
- stream_delete_by_dom_id(socket, name, id)
Удаляет из потока элемент по вычисленному идентификатору DOM.
- stream_insert(socket, name, item, opts \\ [])
Добавляет новый элемент в поток или обновляет существующий.
- transport_pid(socket)
Возвращает PID транспорта сокета.
- uploaded_entries(socket, name)
Возвращает завершенные и выполняющиеся записи загрузки.
Функции
Типы
unsigned_params()Исходный код
@type unsigned_params() :: map()
Обратные вызовы
handle_async(name, async_fun_result, socket)Исходный код
@callback handle_async(
name :: term(),
async_fun_result :: {:ok, term()} | {:exit, term()},
socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()} Вызывается, когда становится доступен результат операции start_async/3.
Подробнее об использовании этого обратного вызова см. в разделе «Произвольные асинхронные операции».
handle_call(msg, tuple, socket)Исходный код
@callback handle_call(
msg :: term(),
{pid(), reference()},
socket :: Phoenix.LiveView.Socket.t()
) ::
{:noreply, Phoenix.LiveView.Socket.t()}
| {:reply, term(), Phoenix.LiveView.Socket.t()} Вызывается для обработки вызовов от других процессов Elixir.
Дополнительную информацию см. в документации GenServer.call/3 и GenServer.handle_call/3.
handle_cast(msg, socket)Исходный код
@callback handle_cast(msg :: term(), socket :: Phoenix.LiveView.Socket.t()) ::
{:noreply, Phoenix.LiveView.Socket.t()} Вызывается для обработки сообщений cast от других процессов Elixir.
Дополнительную информацию см. в документации GenServer.cast/2 и GenServer.handle_cast/2. Функция всегда должна возвращать {:noreply, socket}, где :noreply означает, что процессу, отправившему сообщение cast, не передаются дополнительные данные.
handle_event(event, payload, socket)Исходный код
@callback handle_event(
event :: binary(),
payload :: term(),
socket :: Phoenix.LiveView.Socket.t()
) ::
{:noreply, Phoenix.LiveView.Socket.t()}
| {:reply, map(), Phoenix.LiveView.Socket.t()} Вызывается для обработки событий, отправленных клиентом.
Функция получает имя event, полезную нагрузку события и сокет.
Функция должна возвращать {:noreply, socket}, где :noreply означает, что клиенту не передаются дополнительные данные, или {:reply, map(), socket}, где заданное значение map() кодируется и отправляется клиенту в качестве ответа.
Примечание по безопасности
Событие payload содержит недоверенные данные от клиента. Прежде чем использовать эти данные для получения или изменения ресурсов, необходимо проверить их и убедиться, что у пользователя есть соответствующие права. Дополнительную информацию см. в руководстве «Рекомендации по безопасности».
handle_info(msg, socket)Исходный код
@callback handle_info(msg :: term(), socket :: Phoenix.LiveView.Socket.t()) ::
{:noreply, Phoenix.LiveView.Socket.t()} Вызывается для обработки сообщений от других процессов Elixir.
Дополнительную информацию см. в документации Kernel.send/2 и GenServer.handle_info/2. Функция всегда должна возвращать {:noreply, socket}, где :noreply означает, что процессу, отправившему сообщение, не передаются дополнительные данные.
handle_params(params, uri, socket)Исходный код
@callback handle_params(
params :: unsigned_params(),
uri :: String.t(),
socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()} Вызывается после mount и при каждом событии live patch.
Функция получает текущий params, включая параметры маршрутизатора, текущий uri от клиента и socket. Она вызывается после mount или при каждом событии live-навигации, вызванном с помощью push_patch/2 или <.link patch={...}>.
Функция всегда должна возвращать {:noreply, socket}, где :noreply означает, что клиенту не передаются дополнительные данные.
Примечание по безопасности
Аргумент params содержит недоверенные данные от клиента. Прежде чем использовать эти данные для получения или изменения ресурсов, необходимо проверить их и убедиться, что у пользователя есть соответствующие права. Дополнительную информацию см. в руководстве «Рекомендации по безопасности».
Примечание
handle_params разрешён только для LiveView, смонтированных в маршрутизаторе, поскольку в качестве второго параметра он принимает текущий URL страницы.
mount(params, session, socket)Исходный код
@callback mount(
params :: unsigned_params() | :not_mounted_at_router,
session :: map(),
socket :: Phoenix.LiveView.Socket.t()
) ::
{:ok, Phoenix.LiveView.Socket.t()}
| {:ok, Phoenix.LiveView.Socket.t(), keyword()} Точка входа LiveView.
Для каждого LiveView в корне шаблона mount/3 вызывается дважды: сначала для первоначальной загрузки страницы, а затем для установки live-сокета.
Функция принимает три аргумента:
-
params— карта со строковыми ключами, содержащая общедоступную информацию, которую может задать пользователь. Карта содержит параметры запроса, а также параметры пути маршрутизатора. Если LiveView смонтирован не в маршрутизаторе, этот аргумент — атом:not_mounted_at_router -
session— сессия соединения -
socket— сокет LiveView
Функция должна возвращать либо {:ok, socket}, либо {:ok, socket, options}, где options — один из следующих вариантов:
:temporary_assigns— ключевой список assigns, которые являются временными и должны сбрасываться до исходных значений после каждого рендеринга. Обратите внимание: после сброса значения повторный рендеринг не произойдёт, пока для него явно не будет задано новое значение:layout— необязательный макет для LiveView. Установка этого параметра переопределит ранее заданный макет, указанный с помощьюPhoenix.LiveView.Router.live_session/2или вuse Phoenix.LiveView
Примечание по безопасности
Аргумент params содержит недоверенные данные от клиента. Прежде чем использовать эти данные для получения или изменения ресурсов, необходимо проверить их и убедиться, что у пользователя есть соответствующие права. Дополнительную информацию см. в руководстве «Рекомендации по безопасности».
render(assigns)Исходный код
@callback render(assigns :: Phoenix.LiveView.Socket.assigns()) :: Phoenix.LiveView.Rendered.t()
Рендерит шаблон.
Этот обратный вызов вызывается каждый раз, когда LiveView обнаруживает, что необходимо отрендерить новое содержимое и отправить его клиенту.
Если вы определяете эту функцию, она должна возвращать шаблон, определённый с помощью Phoenix.Component.sigil_H/2.
Если вы не определяете эту функцию, LiveView попытается отрендерить шаблон из того же каталога, что и LiveView. Например, если у вас есть LiveView с именем MyApp.MyCustomView в lib/my_app/live_views/my_custom_view.ex, Phoenix будет искать шаблон по пути lib/my_app/live_views/my_custom_view.html.heex.
terminate(reason, socket)Исходный код
@callback terminate(reason, socket :: Phoenix.LiveView.Socket.t()) :: term()
when reason: :normal | :shutdown | {:shutdown, :left | :closed | term()} Вызывается при завершении работы LiveView.
В случае ошибок этот обратный вызов вызывается только в том случае, если LiveView перехватывает выходы. Подробнее см. в документации GenServer.terminate/2.
Функции
__live__(opts \\ [])Исходный код
Определяет метаданные для LiveView.
Этот код должен возвращаться из обратного вызова __live__.
Принимаются следующие параметры:
:container— необязательный кортеж с HTML-тегом и атрибутами DOM, которые будут использоваться для контейнера LiveView. Например:{:li, style: "color: blue;"}.:layout— настраивает макет, в котором будет отображаться LiveView. Этот макет можно переопределить вmount/3или с помощью параметра:layoutвPhoenix.LiveView.Router.live_session/2:log— настраивает уровень журналирования для LiveView:falseили уровень журналирования:on_mount— список кортежей с именами модулей и аргументами, которые будут вызываться как хукиon_mount
__using__(opts)Исходный код
Подключает LiveView в текущем модуле, помечая его как LiveView.
use Phoenix.LiveView,
container: {:tr, class: "colorized"},
layout: {MyAppWeb.Layouts, :app},
log: :info
Параметры
:container— необязательный кортеж с HTML-тегом и атрибутами DOM, которые будут использоваться для контейнера LiveView. Например:{:li, style: "color: blue;"}. Дополнительные сведения и примеры см. вPhoenix.Component.live_render/3.:global_prefixes— глобальные префиксы для компонентов. Дополнительные сведения см. в разделеGlobal AttributesвPhoenix.Component.:layout— настраивает макет, в котором будет отображаться LiveView. Этот макет можно переопределить вmount/3или с помощью параметра:layoutвPhoenix.LiveView.Router.live_session/2:log— настраивает уровень журналирования для LiveView:falseили уровень журналирования
allow_upload(socket, name, options)Исходный код
Разрешает загрузку для указанного имени.
Параметры
:accept— обязательный параметр. Список уникальных расширений файлов (например, ".jpeg") или MIME-типов (например, "image/jpeg" или "image/*"). Вместо списка можно также передать атом:any, чтобы разрешить загрузку файлов любого типа. Например:[".jpeg"],:anyи т. д.:max_entries— максимальное количество файлов, которые можно выбрать для одного поля ввода файла. По умолчанию — 1.:max_file_size— максимальный размер загружаемого файла в байтах. По умолчанию — 8 МБ. Например:12_000_000.:chunk_size— размер фрагмента в байтах для отправки при загрузке. По умолчанию —64_000.:chunk_timeout— время ожидания в миллисекундах перед закрытием канала загрузки, если не получен новый фрагмент. По умолчанию —10_000.:external— функция с двумя аргументами для создания метаданных внешних загрузчиков на стороне клиента. Эта функция должна возвращать либо{:ok, meta, socket}, либо{:error, meta, socket}, где meta — это map. Пример использования см. в разделе «Загрузки».:progress— необязательная функция с тремя аргументами для получения событий прогресса.:auto_upload— указывает клиенту загружать файл автоматически при его выборе, не дожидаясь отправки формы. По умолчанию —false.:writer— анонимная функция с тремя аргументами, возвращающая кортеж с модулем, реализующим поведениеPhoenix.LiveView.UploadWriter, и его параметрами для записи загружаемых фрагментов. По умолчанию данные записываются во временный файл для последующей обработки. О пользовательской настройке см. документациюPhoenix.LiveView.UploadWriter.:validator— необязательная функция с одним аргументом для пользовательской проверки каждой записи загрузки. Функция получает запись загрузки и должна возвращать либо:ok, либо{:error, reason}. Если возвращается кортеж с ошибкой, запись помечается как неудачная, а ошибка предоставляется какreasonчерезupload_errors/2.
Вызывает исключение, если ранее разрешенная загрузка с тем же именем еще активна.
Примеры
allow_upload(socket, :avatar, accept: ~w(.jpg .jpeg), max_entries: 2)
allow_upload(socket, :avatar, accept: :any)
allow_upload(socket, :avatar, validator: fn entry ->
if String.length(entry.client_name) > 100 do
{:error, :filename_too_long}
else
:ok
end
end)
Чтобы автоматически обрабатывать файлы по мере их загрузки, можно использовать auto_upload: true вместе с пользовательской функцией прогресса, которая обрабатывает записи по завершении загрузки. Например:
allow_upload(socket, :avatar, accept: :any, progress: &handle_progress/3, auto_upload: true)
defp handle_progress(:avatar, entry, socket) do
if entry.done? do
uploaded_file =
consume_uploaded_entry(socket, entry, fn %{} = meta ->
{:ok, ...}
end)
{:noreply, put_flash(socket, :info, "file #{uploaded_file.name} uploaded")}
else
{:noreply, socket}
end
end assign_async(socket, key_or_keys, func, opts \\ [])Исходный код
Асинхронно присваивает значения ключам.
Переданная функция выполняется в задаче, связанной с вызывающим процессом; ошибки оборачиваются. Каждому ключу, переданному в assign_async/3, будет присвоена структура Phoenix.LiveView.AsyncResult, содержащая состояние операции и результат после завершения функции.
Функция должна возвращать либо {:ok, assigns}, либо {:error, reason}, где assigns — это map с ключами, переданными в assign_async/3.
Задача запускается только при подключенном сокете.
Параметры
-
:supervisor— позволяет указатьTask.Supervisorдля супервизии задачи. -
:reset— при значении true удаляет предыдущие результаты на время асинхронной операции. Возможные значения:true,falseили список ключей для сброса. По умолчанию —false.
Примеры
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
Дополнительные сведения см. в разделе Асинхронные операции.
assign_async/3 и send_update/3
Поскольку код внутри assign_async/3 выполняется в отдельном процессе, send_update(Component, data) не работает внутри assign_async/3, так как send_update/2 предполагает, что он выполняется внутри процесса LiveView. Решение — явно отправить обновление в LiveView:
parent = self() assign_async(socket, :org, fn -> # ... send_update(parent, Component, data) end)
Тестирование асинхронных операций
При тестировании LiveView и LiveComponent с асинхронными присваиваниями используйте Phoenix.LiveViewTest.render_async/2, чтобы тест дожидался завершения асинхронных операций перед проверками или окончанием теста. Например:
{:ok, view, _html} = live(conn, "/my_live_view")
html = render_async(view)
assert html =~ "My assertion"
Если не вызвать render_async/2, чтобы дождаться завершения всех асинхронных присваиваний, могут возникнуть ошибки в случаях, когда ваш процесс имеет побочные эффекты:
[error] MyXQL.Connection (#PID<0.308.0>) disconnected: ** (DBConnection.ConnectionError) client #PID<0.794.0>
attach_hook(socket, name, stage, fun)Исходный код
Добавляет заданный fun по name для этапа жизненного цикла stage в socket.
Примечание
Эта функция предназначена для обратных вызовов жизненного цикла на стороне сервера. О хуках на стороне клиента см. в руководстве Взаимодействие с JavaScript.
Хуки позволяют подключаться к ключевым этапам жизненного цикла LiveView, чтобы привязывать и обновлять присваивания, при необходимости перехватывать события, патчи и обычные сообщения, а также добавлять общую функциональность. Используйте attach_hook/4 на любом из следующих этапов жизненного цикла: :handle_params, :handle_event, :handle_info, :handle_async и :after_render. Чтобы добавить хук на этап :mount, используйте on_mount/1.
Поддержка LiveComponents
В LiveComponents в настоящее время поддерживаются только хуки :after_render, :handle_event и :handle_async.
Возвращаемые значения
Хуки жизненного цикла выполняются непосредственно перед вызовом соответствующего обратного вызова жизненного цикла в LiveView. За исключением :after_render, хук может вернуть {:halt, socket}, чтобы остановить сведение; в противном случае он должен вернуть {:cont, socket}, чтобы операция продолжилась до вызова всех хуков для текущего этапа.
Для хуков :after_render необходимо вернуть сам socket. Любые изменения присваиваний сокета не вызовут повторный рендеринг или вычисление diff для клиента.
Остановка жизненного цикла
Обратите внимание: остановка из хука остановит весь этап жизненного цикла. Это означает, что если хук возвращает {:halt, socket}, обратный вызов LiveView не будет вызван. Это имеет некоторые последствия.
Последствия для авторов плагинов
При создании плагина, обрабатывающего определенные обратные вызовы, необходимо определить общее условие, поскольку ваш хук будет вызываться даже для событий, которые могут быть вам неинтересны.
Последствия для конечных пользователей
Возможность остановить вызов обратного вызова позволяет прикреплять хуки, перехватывать определенные события и затем удалять их, позволяя при этом другим событиям обрабатываться обычным образом.
Ответ на события
Хуки, прикрепленные к этапу :handle_event, могут отвечать на события клиента, возвращая {:halt, reply, socket}. Это особенно полезно для взаимодействия с JavaScript, поскольку клиентский хук может отправить событие и получить ответ.
Совместное использование логики обработки событий
Хуки жизненного цикла — отличный способ вынести связанные события из родительского LiveView в отдельные модули, не прибегая без необходимости к LiveComponents для организации кода.
defmodule DemoLive do
use Phoenix.LiveView
def render(assigns) do
~H"""
<div>
<div>
Counter: {@counter}
<button phx-click="inc">+</button>
</div>
<MySortComponent.display lists={[first_list: @first_list, second_list: @second_list]} />
</div>
"""
end
def mount(_params, _session, socket) do
first_list = for(i <- 1..9, do: "First List #{i}") |> Enum.shuffle()
second_list = for(i <- 1..9, do: "Second List #{i}") |> Enum.shuffle()
socket =
socket
|> assign(:counter, 0)
|> assign(first_list: first_list)
|> assign(second_list: second_list)
|> attach_hook(:sort, :handle_event, &MySortComponent.hooked_event/3) # 2) Delegated events
{:ok, socket}
end
# 1) Normal event
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :counter, &(&1 + 1))}
end
end
defmodule MySortComponent do
use Phoenix.Component
def display(assigns) do
~H"""
<div :for={{key, list} <- @lists}>
<ul><li :for={item <- list}>{item}</li></ul>
<button phx-click="shuffle" phx-value-list={key}>Shuffle</button>
<button phx-click="sort" phx-value-list={key}>Sort</button>
</div>
"""
end
def hooked_event("shuffle", %{"list" => key}, socket) do
key = String.to_existing_atom(key)
shuffled = Enum.shuffle(socket.assigns[key])
{:halt, assign(socket, key, shuffled)}
end
def hooked_event("sort", %{"list" => key}, socket) do
key = String.to_existing_atom(key)
sorted = Enum.sort(socket.assigns[key])
{:halt, assign(socket, key, sorted)}
end
def hooked_event(_event, _params, socket), do: {:cont, socket}
end
Другие примеры
Добавление и удаление хука:
def mount(_params, _session, socket) do
socket =
attach_hook(socket, :my_hook, :handle_event, fn
"very-special-event", _params, socket ->
# Handle the very special event and then detach the hook
{:halt, detach_hook(socket, :my_hook, :handle_event)}
_event, _params, socket ->
{:cont, socket}
end)
{:ok, socket}
end
Ответ на событие клиента:
/**
* @type {import("phoenix_live_view").HooksOption}
*/
let Hooks = {}
Hooks.ClientHook = {
mounted() {
this.pushEvent("ClientHook:mounted", {hello: "world"}, (reply) => {
console.log("received reply:", reply)
})
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
def render(assigns) do
~H"""
<div id="my-client-hook" phx-hook="ClientHook"></div>
"""
end
def mount(_params, _session, socket) do
socket =
attach_hook(socket, :reply_on_client_hook_mounted, :handle_event, fn
"ClientHook:mounted", params, socket ->
{:halt, params, socket}
_, _, socket ->
{:cont, socket}
end)
{:ok, socket}
end cancel_async(socket, async_or_keys, reason \\ {:shutdown, :cancel})Исходный код
Отменяет асинхронную операцию, если она существует.
Принимает либо %AsyncResult{} при использовании assign_async/3, либо ключ, переданный в start_async/3.
Базовый процесс будет завершен с указанной причиной или с {:shutdown, :cancel}, если причина не задана. Для операций assign_async/3 поле :failed будет установлено в {:exit, reason}. Для операций start_async/3 обратный вызов handle_async/3 получит {:exit, reason} в качестве результата.
Возвращает %Phoenix.LiveView.Socket{}.
Примеры
cancel_async(socket, :preview) cancel_async(socket, :preview, :my_reason) cancel_async(socket, socket.assigns.preview)
cancel_upload(socket, name, entry_ref)Исходный код
Отменяет загрузку указанной записи.
Примеры
<%= for entry <- @uploads.avatar.entries do %>
...
<button phx-click="cancel-upload" phx-value-ref={entry.ref}>cancel</button>
<% end %>
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :avatar, ref)}
end clear_flash(socket)Исходный код
Очищает flash.
Примеры
iex> clear_flash(socket)
Очистку flash также можно инициировать на клиенте и обработать средствами LiveView с помощью события lv:clear-flash.
Например:
<p class="alert" phx-click="lv:clear-flash">
{Phoenix.Flash.get(@flash, :info)}
</p> clear_flash(socket, key)Исходный код
Очищает ключ из flash.
Примеры
iex> clear_flash(socket, :info)
Очистку flash также можно инициировать на клиенте и обработать средствами LiveView с помощью события lv:clear-flash.
Например:
<p class="alert" phx-click="lv:clear-flash" phx-value-key="info">
{Phoenix.Flash.get(@flash, :info)}
</p> connected?(socket)Исходный код
Возвращает true, если сокет подключен.
Полезно для проверки состояния подключения при монтировании представления. Например, при первоначальном отображении страницы представление монтируется статически, отображается, а HTML отправляется клиенту. После подключения клиента к серверу LiveView запускается и монтируется с сохранением состояния в процессе. Используйте connected?/1, чтобы условно выполнять работу с сохранением состояния, например подписываться на темы pubsub, отправлять сообщения и т. д.
Примеры
defmodule DemoWeb.ClockLive do
use Phoenix.LiveView
...
def mount(_params, _session, socket) do
if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
{:ok, assign(socket, date: :calendar.local_time())}
end
def handle_info(:tick, socket) do
{:noreply, assign(socket, date: :calendar.local_time())}
end
end consume_uploaded_entries(socket, name, func)Исходный код
Обрабатывает загруженные записи.
Вызывает исключение, если остались записи, загрузка которых еще выполняется. Обычно вызывается при отправке формы, чтобы обработать загруженные записи вместе с данными формы. При отправке формы гарантируется, что все записи будут загружены до вызова события отправки. После обработки записи удаляются из загрузки.
Функция, переданная для обработки, может возвращать помеченный кортеж вида {:ok, my_result} для сбора результатов обработки записей или {:postpone, my_result} для сбора результатов с отложенной обработкой файлов.
Возвращается список всех значений my_result, созданных переданной функцией, независимо от того, были ли они обработаны сразу или отложены.
Примеры
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end consume_uploaded_entry(socket, entry, func)Исходный код
Обрабатывает отдельную загруженную запись.
Вызывает исключение, если загрузка записи еще выполняется. Обычно вызывается при отправке формы, чтобы обработать загруженные записи вместе с данными формы. После обработки записи удаляются из загрузки.
Это низкоуровневая функция по сравнению с consume_uploaded_entries/3; она полезна в случаях, когда нужно обрабатывать записи по мере завершения их загрузки.
Как и в consume_uploaded_entries/3, функция, переданная для обработки, может возвращать помеченный кортеж вида {:ok, my_result} для сбора результатов обработки записей или {:postpone, my_result} для сбора результатов с отложенной обработкой файлов.
Примеры
def handle_event("save", _params, socket) do
case uploaded_entries(socket, :avatar) do
{[_|_] = entries, []} ->
uploaded_files = for entry <- entries do
consume_uploaded_entry(socket, entry, fn %{path: path} ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
end)
end
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
_ ->
{:noreply, socket}
end
end detach_hook(socket, name, stage)Исходный код
Удаляет хук с заданным name с этапа жизненного цикла stage.
Примечание
Эта функция предназначена для обратных вызовов жизненного цикла на стороне сервера. О хуках на стороне клиента см. в руководстве Взаимодействие с JavaScript.
Если хук не найден, функция ничего не делает.
Примеры
def handle_event(_, _, socket) do
{:noreply, detach_hook(socket, :hook_that_was_attached, :handle_event)}
end disallow_upload(socket, name)Исходный код
Отзывает разрешение на ранее разрешенную загрузку, выданное функцией allow_upload/3.
Примеры
disallow_upload(socket, :avatar)
get_connect_info(socket, key)Исходный код
Получает из сокета указанное значение информации о подключении.
Поддерживаются следующие ключи: :peer_data, :trace_context_headers, :x_headers, :uri и :user_agent.
Информация о подключении доступна только во время монтирования. При рендеринге без подключения доступны все ключи. При рендеринге с подключением доступны только ключи, явно объявленные в сокете. Полное описание ключей см. в Phoenix.Endpoint.socket/3.
Примеры
Сначала объявите connect_info, которые хотите получить. Обычно в них как минимум включают сессию, но необходимо добавить и все остальные ключи, к которым нужен доступ при подключенном монтировании, например :peer_data:
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:peer_data, session: @session_options]]
Теперь к этим значениям можно обращаться при подключенном монтировании с помощью get_connect_info/2:
def mount(_params, _session, socket) do
peer_data = get_connect_info(socket, :peer_data)
{:ok, assign(socket, ip: peer_data.address)}
end
Если ключ недоступен, обычно потому что он не был указан в connect_info, возвращается nil.
get_connect_params(socket)Исходный код
Получает параметры подключения, отправленные клиентом для использования при подключенном монтировании.
Параметры подключения отправляются клиентом при каждом подключении и повторном подключении. Параметры клиента можно вычислять динамически, что позволяет передавать состояние клиента на сервер. Например, их можно использовать для вычисления и передачи часового пояса пользователя из JavaScript-клиента:
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: (_liveViewName) => {
return {
_csrf_token: csrfToken,
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
}
})
Вычисление параметров с помощью функции позволяет повторно выполнять код при каждом подключении и получать самые актуальные данные.
В LiveView для чтения данных используется get_connect_params/1; данные доступны только во время монтирования. При вызове в отключенном состоянии возвращается nil, а при вызове после монтирования возникает RuntimeError.
Зарезервированные параметры
Следующие параметры имеют специальное значение в LiveView:
-
"_csrf_token"— токен CSRF, который пользователь должен явно задать при подключении -
"_mounts"— количество монтирований текущего LiveView. При первом монтировании оно равно 0, затем увеличивается при каждом повторном подключении. Сбрасывается при переходе с текущего LiveView или при ошибках -
"_track_static"— задается автоматически и содержит список всех href/src из тегов с аннотациейphx-track-static. Если таких тегов нет, ничего не отправляется -
"_live_referer"— отправляется клиентом как URL реферера при переходе с помощью live-навигации изpush_navigateили клиентской навигации по ссылке.
Примеры
def mount(_params, _session, socket) do
{:ok, assign(socket, width: get_connect_params(socket)["width"] || @width)}
end on_mount(mod_or_mod_arg)Исходный код
Объявляет обратный вызов модуля, который будет вызван при монтировании LiveView.
Функция указанного модуля, которая должна называться on_mount, будет вызвана перед монтированием как без подключения, так и с подключением. Хук может остановить процесс монтирования или позволить ему продолжиться как обычно. Если требуется перенаправить LiveView, необходимо остановить процесс, иначе будет вызвана ошибка.
Совет: если нужно определить несколько обратных вызовов on_mount, не создавайте несколько модулей. Вместо этого передайте кортеж и используйте сопоставление с образцом для обработки различных случаев:
def on_mount(:admin, _params, _session, socket) do
{:cont, socket}
end
def on_mount(:user, _params, _session, socket) do
{:cont, socket}
end
Затем вызовите его так:
on_mount {MyAppWeb.SomeHook, :admin}
on_mount {MyAppWeb.SomeHook, :user}
Регистрация хуков on_mount может быть полезна для аутентификации, а также для добавления пользовательского поведения в другие обратные вызовы с помощью attach_hook/4.
Обратный вызов on_mount может возвращать список ключевых слов в качестве третьего элемента кортежа результата. Эти параметры совпадают с параметрами, которые можно вернуть в mount/3.
Примеры
Ниже показан пример добавления хука с помощью Phoenix.LiveView.Router.live_session/3:
# lib/my_app_web/live/init_assigns.ex
defmodule MyAppWeb.InitAssigns do
@moduledoc """
Ensures common `assigns` are applied to all LiveViews attaching this hook.
"""
import Phoenix.LiveView
import Phoenix.Component
def on_mount(:default, _params, _session, socket) do
{:cont, assign(socket, :page_title, "DemoWeb")}
end
def on_mount(:user, params, session, socket) do
# code
end
def on_mount(:admin, _params, _session, socket) do
{:cont, socket, layout: {DemoWeb.Layouts, :admin}}
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# pipelines, plugs, etc.
live_session :default, on_mount: MyAppWeb.InitAssigns do
scope "/", MyAppWeb do
pipe_through :browser
live "/", PageLive, :index
end
end
live_session :authenticated, on_mount: {MyAppWeb.InitAssigns, :user} do
scope "/", MyAppWeb do
pipe_through [:browser, :require_user]
live "/profile", UserLive.Profile, :index
end
end
live_session :admins, on_mount: {MyAppWeb.InitAssigns, :admin} do
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_user, :require_admin]
live "/", AdminLive.Index, :index
end
end
end push_event(socket, event, payload, opts \\ [])Исходный код
Отправляет событие клиенту.
События можно обрабатывать двумя способами:
Их можно обрабатывать на
windowс помощьюaddEventListener. К имени события будет добавлен префикс "phx:".Их можно обрабатывать внутри хука с помощью
handleEvent.
События отправляются всем активным хукам на клиенте, обрабатывающим заданный event. Если нужно ограничить область действия событий, это необходимо сделать, добавив к их именам пространство имен.
События, отправленные во время push_navigate (или любого перенаправления), отправляются клиенту до выполнения перенаправления.
Пример с хуком
Если отправить событие "scores" из LiveView:
{:noreply, push_event(socket, "scores", %{points: 100, user: "josé"})}
Хук, объявленный с помощью phx-hook, может обработать его с помощью handleEvent:
this.handleEvent("scores", data => ...)
Пример window
Все события также отправляются в window. Это позволяет обрабатывать их с помощью слушателей. Например, чтобы удалить элемент со страницы, можно сделать следующее:
{:noreply, push_event(socket, "remove-el", %{id: "foo-bar"})}
Затем в app.js можно зарегистрировать событие и обработать его:
window.addEventListener( "phx:remove-el", e => document.getElementById(e.detail.id).remove() )
Указание этапа отправки
По умолчанию события, отправленные с помощью push_event/3, отправляются только после обновления LiveView. Однако в некоторых случаях может быть полезно обработать событие до обновления LiveView. Для этого в качестве четвертого аргумента можно передать параметр dispatch:
{:noreply, push_event(socket, "scores", %{points: 100, user: "josé"}, dispatch: :before)} push_navigate(socket, opts)Исходный код
Помечает сокет для перехода к другому LiveView в том же live_session.
Текущий LiveView будет остановлен, а вместо него будет смонтирован новый без перезагрузки всей страницы. Это также можно использовать для повторного монтирования того же LiveView, если нужно начать с чистого состояния. Чтобы перейти к тому же LiveView без повторного монтирования, используйте вместо этого push_patch/2.
Параметры
-
:to— обязательный путь для перехода. Он всегда должен быть локальным -
:replace— флаг, определяющий, заменить ли текущую запись истории или добавить новое состояние. По умолчанию —false.
Примеры
{:noreply, push_navigate(socket, to: "/")}
{:noreply, push_navigate(socket, to: "/", replace: true)} push_patch(socket, opts)Исходный код
Помечает сокет для навигации в пределах текущего LiveView.
При переходе к текущему LiveView немедленно вызывается handle_params/3 для обработки изменения параметров и состояния URL. Затем новое состояние отправляется клиенту без перезагрузки всей страницы и с сохранением текущей позиции прокрутки. Для live-перехода к другому LiveView в том же live_session используйте push_navigate/2. В остальных случаях используйте redirect/2.
Параметры
-
:to— обязательный путь для перехода. Он всегда должен быть локальным -
:replace— флаг, определяющий, заменить ли текущую запись истории или добавить новое состояние. По умолчанию —false.
Примеры
{:noreply, push_patch(socket, to: "/")}
{:noreply, push_patch(socket, to: "/", replace: true)} put_flash(socket, kind, msg)Источник
Добавляет в сокет сообщение flash для отображения.
Сообщение flash сохраняется, пока его не прочитают. Если выполнить перенаправление или навигацию, сообщение будет подписано и временно сохранено на стороне клиента. Поэтому важно использовать сообщения flash только для уведомлений пользователям. Не храните в сообщениях flash конфиденциальную информацию.
В типичном приложении LiveView сообщение будет отображаться компонентом flash/1 из CoreComponents. Эта функция определяет, какие типы сообщений она поддерживает. По умолчанию обрабатываются типы :info и :error.
Примечание: Чтобы поддерживались сообщения flash в LiveView, необходимо также добавить плагин Phoenix.LiveView.Router.fetch_live_flash/2 в конвейер браузера вместо fetch_flash, например:
import Phoenix.LiveView.Router pipeline :browser do ... plug :fetch_live_flash end
Примеры
iex> put_flash(socket, :info, "It worked!") iex> put_flash(socket, :error, "You can't access that page")
Внутри компонентов
Можно использовать put_flash/3 внутри Phoenix.LiveComponent, у компонентов есть собственные assigns @flash. Assign @flash в компоненте копируется в родительский LiveView, только если компонент вызывает push_navigate/2 или push_patch/2.
put_private(socket, key, value)Источник
Добавляет в сокет новую приватную пару «ключ — значение».
Приватные данные не отслеживаются при изменениях. Это хранилище предназначено для пользователей и библиотек, которым нужно хранить состояние, не требующее отслеживания изменений. Ключи должны иметь префикс с именем приложения или библиотеки.
Примеры
Значения можно поместить в приватное хранилище:
put_private(socket, :myapp_meta, %{foo: "bar"})
А затем получить:
socket.private[:myapp_meta]
redirect(socket, opts \\ [])Источник
Помечает сокет для перенаправления на указанный путь.
Примечание: Перенаправления LiveView инструктируют клиент выполнить обновление window.location по указанному адресу перенаправления. Вся страница будет перезагружена, а состояние будет сброшено.
Вызов redirect останавливает канал LiveView. Если нужно программно открыть внешнюю ссылку, не останавливая LiveView, например, из-за схем URL mailto: или tel:, рассмотрите возможность использовать push_event/3 с собственным обработчиком на стороне клиента.
Параметры
-
:to— путь для перенаправления. Он всегда должен быть локальным -
:status— код состояния HTTP для перенаправления. По умолчанию 302. -
:external— внешний путь для перенаправления. Строка или{scheme, url}для перенаправления на пользовательскую схему
Примеры
{:noreply, redirect(socket, to: "/")}
{:noreply, redirect(socket, to: "/", status: 301)}
{:noreply, redirect(socket, external: "https://example.com")} render_with(socket, component)Источник
Настраивает функцию, используемую для отображения LiveView/LiveComponent.
По умолчанию LiveView вызывает функцию render/1 в том же модуле, где определён LiveView/LiveComponent, передавая ей единственный аргумент — assigns. Эта функция позволяет задать другую функцию отображения.
Одно из возможных применений этой функции — задать другой шаблон для автономного отображения. При первом обращении пользователя к LiveView выполняется автономное отображение, результат которого отправляется в браузер. Это полезно по нескольким причинам, например, позволяет сократить время до первой отрисовки и индексировать страницу поисковыми системами.
Однако если доступ к LiveView ограничен страницей аутентификации, может быть полезно отобразить заглушку при автономном отображении и выполнить полное отображение после подключения WebSocket. Это можно сделать с помощью render_with/2; такой подход особенно полезен для сложных страниц, например панелей мониторинга и отчётов.
Для этого достаточно вызвать render_with(socket, &some_function_component/1) и настроить сокет с новой функцией отображения.
Примеры
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
{:ok,
socket
|> assign(:foos, Context.list_foos())
|> assign(:bars, Context.list_bars())}
else
{:ok, render_with(socket, &loading/1)}
end
end
defp loading(assigns) do
~H"""
<div class="...">
Loading...
</div>
"""
end send_update(pid \\ self(), module_or_cid, assigns)Источник
Асинхронно обновляет Phoenix.LiveComponent новыми assigns.
Аргумент pid необязателен; по умолчанию используется текущий процесс, то есть инструкция обновления будет отправлена компоненту, работающему в том же LiveView. Если текущий процесс не является LiveView или нужно отправить обновления LiveComponent, работающему в другом LiveView, следует явно передать pid LiveView.
Вторым аргументом может быть либо значение @myself, либо модуль LiveComponent. Если передан модуль, то :id, идентифицирующий компонент, должен быть передан в составе assigns.
Когда компонент получает обновление, будет вызван update_many/1, если он определён; в противном случае вызывается update/2 с новыми assigns. Если update/2 не определён, все assigns просто объединяются с сокетом. Assigns, полученные в качестве первого аргумента callback-функции update/2, будут содержать только новые assigns, переданные этой функцией. Прежние assigns можно найти в socket.assigns.
Компонент всегда можно обновить из родительского компонента, изменив assigns родителя, что приведёт к повторному отображению дочернего компонента и вызову update/2 для дочернего компонента. Однако send_update/3 удобен для обновления компонента, который полностью управляет собственным состоянием, а также для обмена сообщениями между компонентами, смонтированными в одном LiveView.
Примеры
def handle_event("cancel-order", _, socket) do
...
send_update(Cart, id: "cart", status: "cancelled")
{:noreply, socket}
end
def handle_event("cancel-order-asynchronously", _, socket) do
...
pid = self()
Task.Supervisor.start_child(MyTaskSup, fn ->
# Do something asynchronously
send_update(pid, Cart, id: "cart", status: "cancelled")
end)
{:noreply, socket}
end
def render(assigns) do
~H"""
<.some_component on_complete={&send_update(@myself, completed: &1)} />
"""
end send_update_after(pid \\ self(), module_or_cid, assigns, time_in_milliseconds)Источник
Похожа на send_update/3, но обновление будет отложено на указанный time_in_milliseconds.
Возвращает ссылку, которую можно отменить с помощью Process.cancel_timer/1.
Примеры
def handle_event("cancel-order", _, socket) do
...
send_update_after(Cart, [id: "cart", status: "cancelled"], 3000)
{:noreply, socket}
end
def handle_event("cancel-order-asynchronously", _, socket) do
...
pid = self()
Task.start(fn ->
# Do something asynchronously
send_update_after(pid, Cart, [id: "cart", status: "cancelled"], 3000)
end)
{:noreply, socket}
end start_async(socket, name, func, opts \\ [])Источник
Оборачивает функцию в асинхронную задачу и вызывает callback name для обработки результата.
Задача связана с вызывающим процессом, а ошибки и выходы оборачиваются. Результат задачи передаётся callback-функции handle_async/3 вызывающего LiveView или LiveComponent.
Если уже выполняется задача с тем же name, результат более позднего вызова start_async будет иметь приоритет, а результат предыдущей задачи будет проигнорирован. Чтобы заменить существующую задачу, можно вызвать cancel_async/3 перед start_async/3. Для name можно использовать не только атомы, но и любые термы, например кортеж.
Задача запускается только при подключённом сокете.
Параметры
-
:supervisor— позволяет указатьTask.Supervisorдля супервизии задачи.
Примеры
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_task, fn -> fetch_org!(id) end)}
end
def handle_async(:my_task, {:ok, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_task, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
Дополнительные сведения см. в документации модуля.
static_changed?(socket)Источник
Возвращает true, если сокет подключён и отслеживаемые статические ресурсы изменились.
Эта функция помогает определить, использует ли клиент устаревшую версию помеченных статических файлов. Она сравнивает пути к статическим ресурсам, отправленные клиентом, с путями на сервере.
Примечание: эта возможность требует Phoenix версии 1.5.2 или новее.
Чтобы использовать эту возможность, сначала укажите, какие статические файлы нужно отслеживать в LiveView, с помощью phx-track-static. Например:
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
Теперь при каждом подключении LiveView к серверу клиент будет отправлять копию атрибутов src или href всех отслеживаемых статических ресурсов и сравнивать эти значения с последними записями, вычисленными на сервере с помощью mix phx.digest.
В подавляющем большинстве случаев отслеживаемые статические ресурсы на клиенте будут совпадать с ресурсами на сервере. Однако после нового развёртывания эти значения могут различаться. Эту функцию можно использовать для обнаружения таких случаев и отображения пользователю баннера с предложением перезагрузить страницу. Для этого сначала задайте assign при монтировании:
def mount(params, session, socket) do
{:ok, assign(socket, static_changed?: static_changed?(socket))}
end
Затем в представлениях:
<div :if={@static_changed?} id="reload-static">
The app has been updated. Click here to <a href="#" onclick="window.location.reload()">reload</a>.
</div>
В крупных проектах это можно вынести в хук:
# MyAppWeb.CheckStaticChanged
def on_mount(:default, _params, _session, socket) do
{:cont, assign(socket, static_changed?: static_changed?(socket))}
end
Затем добавьте его в существующий макрос live_view в файле my_app_web.ex или включите в хуки live_session. Если хотите, можно также отправить скрипт JavaScript, который немедленно перезагрузит страницу, но тогда клиент потеряет всю незавершённую работу.
Примечание: задавайте phx-track-static только для собственных ресурсов. Например, не задавайте его для внешних файлов JavaScript:
<script defer phx-track-static type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
Поскольку файл выше фактически не обслуживается вами, LiveView сочтёт этот статический ресурс отсутствующим, и функция вернёт true.
stream(socket, name, items, opts \\ [])Источник
@spec stream( socket :: Phoenix.LiveView.Socket.t(), name :: atom() | String.t(), items :: Enumerable.t(), opts :: Keyword.t() ) :: Phoenix.LiveView.Socket.t()
Назначает сокету новый поток или добавляет элементы в существующий поток. Возвращает обновлённый socket.
Потоки позволяют управлять большими коллекциями на клиенте, не храня ресурсы на сервере.
-
name— строковое имя или атом ключа, под которым размещаются данные в assign@streams. -
items— перечислимый набор элементов для добавления.
Поддерживаются следующие параметры:
-
:at— индекс для вставки или обновления элементов коллекции на клиенте. По умолчанию используется-1, который добавляет элементы в конец родительского DOM-контейнера. Значение0добавляет элементы в начало.Обратите внимание: эта операция эквивалентна последовательной вставке элементов по одному с указанным индексом. Поэтому при вставке нескольких элементов с индексом, отличным от
-1, в интерфейсе они будут отображаться в обратном порядке:stream(socket, :songs, [song1, song2, song3], at: 0)
В этом случае в начало будут добавлены
song1, затемsong2, а затемsong3, поэтому будут отображатьсяsong3,song2,song1и все элементы, добавленные ранее.Чтобы вставить элементы в порядке списка, используйте
Enum.reverse/1:stream(socket, :songs, Enum.reverse([song1, song2, song3]), at: 0)
:reset— логическое значение, определяющее, нужно ли сбросить поток на клиенте. По умолчанию —false.-
:limit— необязательное положительное или отрицательное число, ограничивающее количество результатов в интерфейсе на клиенте. По мере поступления новых элементов интерфейс будет удалять существующие, чтобы соблюдать ограничение. Например, чтобы ограничить поток последними 10 элементами и добавлять новые элементы в конец, передайте отрицательное значение:stream(socket, :songs, songs, at: -1, limit: -10)
Аналогично, чтобы ограничить поток первыми 10 элементами и добавлять новые элементы в начало, передайте положительное значение:
stream(socket, :songs, songs, at: 0, limit: 10)
После определения потока становится доступен новый assign @streams, содержащий имена определённых потоков. Например, в приведённом выше определении на поток можно ссылаться в шаблоне как на @streams.songs. Элементы потока являются временными и удаляются из состояния сокета сразу после вызова функции render/1 (или отображения шаблона с диска).
По умолчанию вызов stream/4 для существующего потока пакетно добавляет новые элементы на клиенте, оставляя существующие элементы без изменений. При вызове stream/4 потоки также можно сбросить; это описано ниже.
Сброс потока
Чтобы очистить контейнер потока на клиенте, передайте :reset с пустым списком:
stream(socket, :songs, [], reset: true)
Или замените поток на клиенте новой коллекцией целиком:
stream(socket, :songs, new_songs, reset: true)
Ограничение размера потока
Часто бывает полезно ограничить количество элементов в интерфейсе и при этом позволить серверу передавать новые элементы в режиме «отправить и забыть». Это не даёт серверу перегрузить клиент новыми результатами и одновременно позволяет использовать такие возможности, как виртуальная бесконечная прокрутка. Полный пример двунаправленной бесконечной прокрутки с ограничениями потока см. в руководстве по событиям прокрутки.
Когда поток на клиенте превышает ограничение, существующие элементы удаляются с учётом количества элементов в контейнере потока и направления ограничения. При положительном ограничении элементы удаляются из конца контейнера, а при отрицательном — из начала.
Обратите внимание: ограничение не применяется при первом отображении mount/3 (до установки соединения WebSocket), поскольку в этом случае загружается больше данных, чем необходимо. В таких случаях в поток следует загружать и передавать только нужное количество элементов.
При вставке отдельных элементов с помощью stream_insert/4 для применения ограничения на клиенте его нужно передать в качестве параметра:
stream_insert(socket, :songs, song, limit: -10)
Обязательные атрибуты DOM
Чтобы отслеживать элементы потока на клиенте, необходимо выполнить следующие требования:
- Родительский DOM-контейнер должен содержать атрибут
phx-update="stream"и уникальный DOM id. - У каждого элемента потока должен быть свой DOM id.
Примечание
Если не разместить phx-update="stream" у непосредственного родителя для каждого потока, поведение будет нарушено.
Также не изменяйте автоматически созданные DOM id, например, добавляя к ним префикс. Это нарушит работу.
При использовании потока в шаблоне DOM id и элемент передаются в виде кортежа, что позволяет удобно добавить DOM id для каждого элемента. Например:
<table>
<tbody id="songs" phx-update="stream">
<tr
:for={{dom_id, song} <- @streams.songs}
id={dom_id}
>
<td>{song.title}</td>
<td>{song.duration}</td>
</tr>
</tbody>
</table>
Поток используется в выражении for comprehension со ссылкой на assign @streams.songs. Вычисленный DOM id добавляется в атрибут <tr>, после чего строка таблицы отображается как обычно.
Теперь можно вызывать stream_insert/3 и stream_delete/3, чтобы добавлять новые строки на клиенте или удалять их.
Обработка пустого списка
При отображении списка элементов обычно показывают сообщение, если список пуст. Но при использовании потоков нельзя полагаться на Enum.empty?/1 или похожие способы проверки пустоты списка. Вместо этого можно использовать CSS-селектор :only-child и показать сообщение на стороне клиента:
<table>
<tbody id="songs" phx-update="stream">
<tr id="songs-empty" class="only:table-row hidden">
<td colspan="2">No songs found</td>
</tr>
<tr
:for={{dom_id, song} <- @streams.songs}
id={dom_id}
>
<td>{song.title}</td>
<td>{song.duration}</td>
</tr>
</tbody>
</table>
Для пустой строки важно задать уникальный ID, иначе её нельзя будет отследить в контейнере потока, и последующие обновления будут дублировать узел.
Элементы, не относящиеся к потоку, в контейнерах потоков
В разделе об обработке пустого списка мы показали, как отобразить сообщение при пустом потоке, добавив элемент, не относящийся к потоку, внутрь контейнера потока.
При размещении элементов, не относящихся к потоку, внутри контейнера phx-update="stream" следует учитывать следующее:
Элементы, не относящиеся к потоку, должны иметь уникальный DOM id.
-
Элементы можно добавлять и обновлять, но нельзя удалять, даже при сбросе потока.
Это означает, что если попытаться условно отобразить элемент, не относящийся к потоку, внутри контейнера потока, он не будет удалён, если уже отображался.
-
На элементы влияет параметр
:at.Например, если отобразить элемент, не относящийся к потоку, в начале контейнера потока, а затем добавить элементы в начало потока с помощью
at: 0, этот элемент сместится вниз.
stream_async(socket, name, func, opts \\ [])Источник
Асинхронно добавляет данные в поток.
Оборачивает функцию в задачу, связанную с вызывающим процессом; ошибки оборачиваются. Ключ, переданный в stream_async/3, будет использоваться как имя потока. Кроме того, обычному assign с тем же именем присваивается структура Phoenix.LiveView.AsyncResult, содержащая состояние операции. Перед запуском асинхронной функции поток инициализируется пустым списком, поэтому всегда можно обратиться к @streams.name.
Функция должна возвращать {:ok, Enumerable.t()} или {:ok, Enumerable.t(), opts}, где opts совпадают с параметрами в stream/4. Перечислимый набор содержит значения для передачи в поток.
Если функция возвращает {:error, any()}, assign AsyncResult помечается как неудачный, а поток не обновляется.
Задача запускается только при подключённом сокете.
Параметры
-
:supervisor— позволяет указатьTask.Supervisorдля супервизии задачи. -
:reset— логическое значение, определяющее, нужно ли удалять предыдущие результаты во время асинхронной операции. По умолчанию —false.
Примеры
def mount(%{"slug" => slug}, _, socket) do
current_scope = socket.assigns.current_scope
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(current_scope)}} end)
|> stream_async(:posts, fn -> {:ok, list_posts!(current_scope), limit: 10} end)
end
Обратите внимание: параметр reset управляет асинхронным assign, а не потоком:
def mount(_, _, socket) do
{:ok,
socket
# IMPORTANT: reset here does NOT reset the stream, but only the loading state
|> stream_async(:my_stream, fn -> {:ok, list_items!()} end, reset: true)
# This resets the stream
|> stream_async(:my_reset_stream, fn -> {:ok, list_items!(), reset: true} end)
end
Параметры потока нужно возвращать как необязательный третий аргумент в возвращаемом значении асинхронной функции.
stream_configure(socket, name, opts)Источник
@spec stream_configure( socket :: Phoenix.LiveView.Socket.t(), name :: atom() | String.t(), opts :: Keyword.t() ) :: Phoenix.LiveView.Socket.t()
Настраивает поток.
Поддерживаются следующие параметры:
-
:dom_id— необязательная функция для генерации DOM id каждого элемента потока. Функция принимает каждый элемент потока и преобразует его в строковый id. По умолчанию используется поле:idкарты или структуры, если оно есть; перед ним добавляетсяname, а части разделяются дефисами. Например, следующие варианты эквивалентны:stream(socket, :songs, songs) socket |> stream_configure(:songs, dom_id: &("songs-#{&1.id}")) |> stream(:songs, songs)
Поток необходимо настроить до добавления элементов, и после настройки повторно настроить его нельзя. Чтобы поток настраивался только один раз в LiveComponent, используйте callback mount/1. Например:
def mount(socket) do
{:ok, stream_configure(socket, :songs, dom_id: &("songs-#{&1.id}"))}
end
def update(assigns, socket) do
{:ok, stream(socket, :songs, ...)}
end
Возвращает обновлённый socket.
stream_delete(socket, name, item)Источник
@spec stream_delete( socket :: Phoenix.LiveView.Socket.t(), name :: atom() | String.t(), item :: any() ) :: Phoenix.LiveView.Socket.t()
Удаляет элемент из потока.
DOM элемента вычисляется на основе :dom_id, заданного в определении stream/3. Информация об удалении этого DOM id отправляется клиенту, а элемент удаляется из DOM в соответствии с обычным поведением при удалении элементов: например, вызываются команды phx-remove и callback-функции клиентского хука destroyed().
Примеры
def handle_event("delete", %{"id" => id}, socket) do
song = get_song!(id)
{:noreply, stream_delete(socket, :songs, song)}
end
См. stream_delete_by_dom_id/3, чтобы удалить элемент без исходной структуры данных.
Возвращает обновлённый socket.
stream_delete_by_dom_id(socket, name, id)Источник
@spec stream_delete_by_dom_id( socket :: Phoenix.LiveView.Socket.t(), name :: atom() | String.t(), id :: String.t() ) :: Phoenix.LiveView.Socket.t()
Удаляет элемент из потока по вычисленному DOM id.
Возвращает обновлённый socket.
Работает так же, как stream_delete/3, но принимает предварительно вычисленный DOM id. Это позволяет удалять элемент из потока, не получая и не создавая исходную структуру данных потока.
Примеры
def render(assigns) do
~H"""
<table>
<tbody id="songs" phx-update="stream">
<tr
:for={{dom_id, song} <- @streams.songs}
id={dom_id}
>
<td>{song.title}</td>
<td><button phx-click={JS.push("delete", value: %{id: dom_id})}>delete</button></td>
</tr>
</tbody>
</table>
"""
end
def handle_event("delete", %{"id" => dom_id}, socket) do
{:noreply, stream_delete_by_dom_id(socket, :songs, dom_id)}
end stream_insert(socket, name, item, opts \\ [])Источник
@spec stream_insert( socket :: Phoenix.LiveView.Socket.t(), name :: atom() | String.t(), item :: any(), opts :: Keyword.t() ) :: Phoenix.LiveView.Socket.t()
Вставляет новый элемент в поток или обновляет существующий.
Возвращает обновлённый socket.
См. stream/4, чтобы вставить сразу несколько элементов.
Поддерживаются следующие параметры:
:at— индекс для вставки или обновления элемента коллекции на клиенте. По умолчанию элемент добавляется в конец родительского DOM-контейнера. Это эквивалентно передаче значения-1. Если элемент уже есть в родительском DOM-контейнере, он будет обновлён на месте.:limit— ограничение на количество элементов в интерфейсе. Ограничение, переданное вstream/4, не влияет на последующие вызовыstream_insert/4, поэтому для его применения его нужно передавать и здесь. Дополнительные сведения об ограничении потоков см. в разделеstream/4.:update_only— логическое значение, указывающее, нужно ли только обновить элемент в потоке. Если элемента нет на клиенте, он не будет вставлен. По умолчанию —false.
Примеры
Предположим, при монтировании вы определяете поток с одним элементом:
stream(socket, :songs, [%Song{id: 1, title: "Song 1"}])
Затем в callback-функции, например handle_info или handle_event, можно добавить новую песню в конец:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"})
Или добавить новую песню в начало с помощью at: 0:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"}, at: 0)
Или обновить существующую песню (в этом случае параметр :at не влияет на результат):
stream_insert(socket, :songs, %Song{id: 1, title: "Song 1 updated"}, at: 0)
Или добавить новую песню в конец, ограничив поток последними 10 элементами:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"}, limit: -10)
Обновление элементов
Как показано выше, существующий элемент на клиенте можно обновить, выполнив stream_insert для этого элемента. При обновлении существующего элемента на клиенте он остаётся на прежнем месте и не перемещается в конец дочерних элементов родительского контейнера. Чтобы одновременно обновить существующий элемент и переместить его в другое место, выполните stream_delete, а затем stream_insert. Например:
song = get_song!(id) socket |> stream_delete(:songs, song) |> stream_insert(:songs, song, at: -1)
Дополнительные сведения об удалении элементов см. в разделе stream_delete/3.
transport_pid(socket)Источник
Возвращает pid транспорта сокета.
Вызывает исключение ArgumentError, если сокет не подключён.
Примеры
iex> transport_pid(socket) #PID<0.107.0>
uploaded_entries(socket, name)Источник
Возвращает завершённые и выполняющиеся записи загрузки.
Примеры
case uploaded_entries(socket, :photos) do
{[_ | _] = completed, []} ->
# all entries are completed
{[], [_ | _] = in_progress} ->
# all entries are still in progress
end
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/Phoenix.LiveView.html