Взаимодействие с JavaScript
Исходный кодЧтобы обеспечить взаимодействие клиента и сервера LiveView, мы создаём экземпляр LiveSocket. Например:
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()
Все параметры передаются непосредственно конструктору Phoenix.Socket, за исключением следующих параметров, специфичных для LiveView:
-
bindingPrefix— префикс для привязок Phoenix. По умолчанию"phx-" -
params—connect_params, передаваемый в callback монтирования представления. Это может быть литеральный объект или замыкание, возвращающее объект. Если передано замыкание, функция получает элемент представления. -
hooks— ссылка на пространство имён пользовательских хуков, содержащее клиентские callback-функции для взаимодействия клиента и сервера. Подробности см. в разделе Клиентские хуки ниже. -
uploaders— ссылка на пространство имён пользовательских загрузчиков, содержащее клиентские callback-функции для прямой загрузки файлов из клиента в облако. Подробности см. в руководстве по внешней загрузке файлов. -
metadata— дополнительные пользовательские метаданные, отправляемые вместе с событиями на сервер. Пример см. в разделе События клавиатуры руководства по привязкам.
Экземпляр liveSocket предоставляет следующие методы:
-
connect()— вызовите этот метод один раз после создания экземпляра, чтобы подключиться к серверу -
enableDebug()— включает отладочное журналирование; см. Отладка клиентских событий -
disableDebug()— отключает отладочное журналирование -
enableLatencySim(milliseconds)— включает имитацию задержки; см. Имитация задержки -
disableLatencySim()— отключает имитацию задержки -
execJS(el, encodedJS)— выполняет закодированную JS-команду в контексте элемента -
js()— возвращает объект с методами для управления DOM и выполнения JS-команд. Применённые изменения согласуются с обновлением DOM на сервере. См. раздел JS-команды.
Отладка клиентских событий
Чтобы упростить отладку на клиенте при поиске и устранении проблем, функции enableDebug() и disableDebug() доступны в экземпляре JavaScript LiveSocket. Вызов enableDebug() включает отладочное журналирование, в которое входят события жизненного цикла LiveView и события с данными, передаваемые между клиентом и сервером. На практике для быстрого доступа к экземпляру из консоли браузера можно присвоить его window, например:
// app.js let liveSocket = new LiveSocket(...) liveSocket.connect() window.liveSocket = liveSocket // in the browser's web console >> liveSocket.enableDebug()
Состояние отладки использует встроенный в браузер sessionStorage, поэтому оно сохраняется на протяжении всего сеанса работы в браузере.
Имитация задержки
Правильная обработка задержек критически важна для удобства использования. CSS-состояния загрузки LiveView позволяют клиенту сообщать пользователю о происходящем в ожидании ответа сервера. В среде разработки почти нулевая задержка при работе с localhost не позволяет легко воспроизвести или проверить задержку, поэтому в JavaScript-клиент LiveView включён симулятор задержки, который помогает обеспечить комфортную работу приложения. Как и упомянутая выше функция enableDebug(), экземпляр LiveSocket предоставляет функции enableLatencySim(milliseconds) и disableLatencySim(), действующие на протяжении текущего сеанса браузера. Функция enableLatencySim принимает целое число миллисекунд, обозначающее задержку в одну сторону при передаче данных на сервер и обратно. Например:
// app.js
let liveSocket = new LiveSocket(...)
liveSocket.connect()
window.liveSocket = liveSocket
// in the browser's web console
>> liveSocket.enableLatencySim(1000)
[Log] latency simulator enabled for the duration of this browser session.
Call disableLatencySim() to disable
Обработка событий, отправленных сервером
Когда сервер использует Phoenix.LiveView.push_event/3, в браузере будет отправлено событие с префиксом phx:. Например, представим следующий шаблон, в котором нужно подсветить существующий элемент, чтобы привлечь к нему внимание пользователя:
<div id={"item-#{item.id}"} class="item">
{item.title}
</div>
Затем сервер может запустить подсветку с помощью стандартного push_event:
def handle_info({:item_updated, item}, socket) do
{:noreply, push_event(socket, "highlight", %{id: "item-#{item.id}"})}
end
Наконец, обработчик события окна может отслеживать событие и при совпадении элемента запускать команду подсветки:
let liveSocket = new LiveSocket(...)
window.addEventListener("phx:highlight", (e) => {
let el = document.getElementById(e.detail.id)
if(el) {
// logic for highlighting
}
})
При желании эту функциональность также можно интегрировать с JS-командами Phoenix, выполняя JS-команды для указанного элемента при каждом запуске подсветки. Для этого сначала обновите элемент, добавив JS-команду в атрибут данных:
<div id={"item-#{item.id}"} class="item" data-highlight={JS.transition("highlight")}>
{item.title}
</div>
Теперь в обработчике события используйте LiveSocket.execJS, чтобы запустить все JS-команды из нового атрибута:
let liveSocket = new LiveSocket(...)
window.addEventListener("phx:highlight", (e) => {
document.querySelectorAll(`[data-highlight]`).forEach(el => {
if(el.id == e.detail.id){
liveSocket.execJS(el, el.getAttribute("data-highlight"))
}
})
})
Клиентские хуки с помощью phx-hook
Чтобы обрабатывать пользовательский JavaScript на клиенте при добавлении, обновлении или удалении сервером элемента, можно передать объект хука через phx-hook. phx-hook должен указывать на объект со следующими callback-функциями жизненного цикла:
-
mounted— элемент добавлен в DOM, а его серверное представление LiveView завершило монтирование -
beforeUpdate— элемент будет обновлён в DOM. Примечание: вызов здесь должен быть синхронным, поскольку операцию нельзя отложить или отменить. -
updated— сервер обновил элемент в DOM. Примечание: во время выполнения этого callbackwindow.locationможет не соответствовать текущему URL. Для логики, учитывающей навигацию, используйте событиеphx:navigate. -
destroyed— элемент удалён со страницы: либо при обновлении родительского элемента, либо при полном удалении родителя -
disconnected— родительское представление LiveView элемента отключилось от сервера -
reconnected— родительское представление LiveView элемента снова подключилось к серверу
Примечание: хуки также выполняются на обычных страницах, которые не являются LiveView — см. раздел Хуки и JS-команды за пределами LiveView. В этом случае вызывается только mounted, а отслеживаются лишь элементы, присутствующие на странице при готовности DOM. Для динамического отслеживания DOM при добавлении, удалении и обновлении элементов следует использовать LiveView.
Указанные выше callback-функции жизненного цикла имеют доступ к следующим атрибутам в своей области видимости:
-
el— атрибут, содержащий ссылку на связанный узел DOM -
liveSocket— ссылка на базовый экземплярLiveSocket -
pushEvent(event, payload, (reply, ref) => ...)— метод для отправки события с клиента серверу LiveView. Если callback не указан, возвращается промис, который разрешается вreply. Примечание: при использовании варианта с callback ошибки игнорируются без уведомления. -
pushEventTo(selectorOrTarget, event, payload, (reply, ref) => ...)— метод для отправки адресных событий из клиента в LiveView и LiveComponent. ЗначениемselectorOrTargetможет быть элемент DOM (например,this.el) или строка с селектором запроса. Событие отправляется LiveComponent или LiveView, которому принадлежат целевые элементы. Если селектор соответствует нескольким элементам, событие отправляется всем, даже если они принадлежат одному и тому же LiveComponent или LiveView. Если callback не указан, возвращается промис, соответствующийPromise.allSettled(), в котором выполненные значения имеют формат{ reply, ref }. Примечание: при использовании варианта с callback ошибки игнорируются без уведомления. -
handleEvent(event, (payload) => ...)— метод для обработки события, отправленного сервером. Возвращает значение, которое можно передать вremoveHandleEventдля удаления обработчика события. -
removeHandleEvent(ref)— метод для удаления обработчика события, добавленного с помощьюhandleEvent -
upload(name, files)— метод для передачи в загрузчик списка объектов, подобных файлам. -
uploadTo(selectorOrTarget, name, files)— метод для передачи в загрузчик списка объектов, подобных файлам. Хук отправит файлы в загрузчик с параметрамиname, заданными на стороне сервера с помощьюallow_upload/3. Добавление новых загрузок вызывает событие изменения input, которое будет отправлено LiveComponent или LiveView, где определёнselectorOrTarget; его значением может быть селектор запроса или непосредственно элемент DOM. Если селектор запроса возвращает более одного активного поля ввода файла, будет записана ошибка. -
js()— возвращает объект с методами для управления DOM и выполнения JavaScript. Применённые изменения согласуются с обновлением DOM на сервере. См. раздел JS-команды.
Например, разметку контролируемого поля ввода для форматирования телефонного номера можно записать так:
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook="PhoneNumber" />
Затем можно определить объект callback-функций хука и передать его сокету:
/**
* @type {import("phoenix_live_view").HooksOptions}
*/
let Hooks = {}
Hooks.PhoneNumber = {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
...
Примечание: при использовании phx-hook всегда необходимо задавать уникальный идентификатор DOM.
Для интеграции с клиентскими библиотеками, которым требуется более широкий доступ к управлению DOM, конструктор LiveSocket принимает параметр dom с callback-функцией onBeforeElUpdated. Узлы DOM fromEl и toEl передаются функции непосредственно перед выполнением операций обновления DOM в LiveView. Это позволяет внешним библиотекам повторно инициализировать элементы DOM или копировать атрибуты по мере выполнения LiveView собственных операций обновления. Операцию обновления нельзя отменить или отложить, а возвращаемое значение игнорируется.
Например, следующий параметр можно использовать, чтобы гарантировать сохранение некоторых атрибутов, заданных на стороне клиента:
...
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: Hooks,
dom: {
onBeforeElUpdated(from, to) {
for (const attr of from.attributes) {
if (attr.name.startsWith("data-js-")) {
to.setAttribute(attr.name, attr.value);
}
}
}
}
})
В приведённом выше примере при обновлении DOM в LiveView не будут заменяться атрибуты, начинающиеся с data-js-.
Хук также можно определить как подкласс ViewHook:
import { ViewHook } from "phoenix_live_view"
class MyHook extends ViewHook {
mounted() {
...
}
}
let liveSocket = new LiveSocket(..., {
hooks: {
MyHook
}
})
Совместно размещённые хуки / совместно размещённый JavaScript
При написании компонентов, требующих более точного управления DOM, часто неудобно определять хук в отдельном файле. Вместо этого хочется разместить логику хука рядом с кодом компонента. Для таких случаев HEEx поддерживает Phoenix.LiveView.ColocatedHook и Phoenix.LiveView.ColocatedJS.
Рассмотрим пример:
def phone_number_input(assigns) do
~H"""
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
<script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
export default {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
</script>
"""
end
Когда LiveView обнаруживает элемент <script> с :type={ColocatedHook}, он извлекает код хука во время компиляции и записывает его в специальную папку внутри каталога _build/. Чтобы использовать хуки, достаточно импортировать манифест в JS-пакет; в новых приложениях Phoenix 1.8 это автоматически делается в файле app.js, созданном с помощью mix phx.new:
...
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
+ import {hooks as colocatedHooks} from "phoenix-colocated/my_app"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
+ hooks: {...colocatedHooks}
})
Пакет "phoenix-colocated" — это папка внутри Mix.Project.build_path(), которая включена по умолчанию в конфигурацию esbuild новых проектов Phoenix (требуется {:esbuild, "~> 0.10"} или более поздняя версия):
config :esbuild,
...
my_app: [
args:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]
При отображении компонента, содержащего совместно размещённый хук, тег <script> не включается в результат рендеринга. Кроме того, во избежание конфликтов с другими компонентами совместно размещённые хуки требуют специального синтаксиса с точкой при указании имени хука, а также в атрибуте phx-hook. Во время компиляции LiveView добавит к имени хука префикс с именем текущего модуля. Это также означает, что если хук предназначен для использования в нескольких компонентах проекта, его следует определять как обычный, не совместно размещённый хук.
Подробнее о совместно размещённых хуках можно прочитать в документации модуля ColocatedHook. LiveView также поддерживает совместное размещение другого JavaScript-кода; дополнительную информацию см. в разделе Phoenix.LiveView.ColocatedJS.
Взаимодействие клиента и сервера
Хук может отправлять события в LiveView с помощью функции pushEvent и получать ответ сервера через возвращаемое значение {:reply, map, socket}. Данные ответа передаются в необязательный callback pushEvent.
Связь сервера с хуком можно организовать, считывая атрибуты данных элемента хука, либо используя на сервере Phoenix.LiveView.push_event/3, а на клиенте — handleEvent.
Пример обработки :reply может выглядеть так:
<div phx-hook="ClickMeHook" id="click-me"> Click me for a message! </div>
Hooks.ClickMeHook = {
mounted() {
this.el.addEventListener("click", () => {
// Push event to LiveView with callback for reply
this.pushEvent("get_message", {}, (reply) => {
console.debug(reply.message);
});
});
}
}
Затем в callback-функции можно ответить с помощью {:reply, map, socket}
def handle_event("get_message", _params, socket) do
# Use :reply to respond to the pushEvent
{:reply, %{message: "Hello from LiveView!"}, socket}
end
В качестве другого примера можно передать текущую страницу через атрибуты данных для реализации бесконечной прокрутки:
<div id="infinite-scroll" phx-hook="InfiniteScroll" data-page={@page}>
А на клиенте:
/**
* @type {import("phoenix_live_view").Hook}
*/
Hooks.InfiniteScroll = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-more", {})
}
})
},
updated(){ this.pending = this.page() }
}
Однако атрибуты данных не подходят, если нужно часто отправлять данные клиенту. Чтобы отправлять клиенту события вне основного потока, например для отображения точек на графике, можно сделать так:
<div id="chart" phx-hook="Chart">
А на клиенте:
/**
* @type {import("phoenix_live_view").Hook}
*/
Hooks.Chart = {
mounted(){
this.handleEvent("points", ({points}) => MyChartLib.addPoints(points))
}
}
После этого можно отправлять события следующим образом:
{:noreply, push_event(socket, "points", %{points: new_points})}
События, отправленные сервером через push_event, являются глобальными и будут переданы всем активным хукам на клиенте, обрабатывающим это событие. Если нужно ограничить область действия событий (например, при отправке события из живого компонента, у которого есть соседние компоненты в текущем представлении LiveView), их необходимо помещать в пространство имён:
def update(%{id: id, points: points} = assigns, socket) do
socket =
socket
|> assign(assigns)
|> push_event("points-#{id}", points)
{:ok, socket}
end
А на клиенте:
Hooks.Chart = {
mounted(){
this.handleEvent(`points-${this.el.id}`, (points) => MyChartLib.addPoints(points));
}
}
Примечание: если LiveView отправляет события и отображает содержимое, callback-функции handleEvent вызываются после обновления страницы. Поэтому, если LiveView одновременно перенаправляет пользователя и отправляет события, callback-функции не будут вызваны для элементов старой страницы. Они будут вызваны для элементов хуков, смонтированных на новой странице после перенаправления.
JS-команды
Примечание: по возможности создавайте команды на Elixir с помощью Phoenix.LiveView.JS и запускайте их через привязки DOM Phoenix.
Хотя Phoenix.LiveView.JS позволяет создавать декларативное представление команды, оно может не охватывать все сценарии использования. Кроме того, можно выполнять команды, которые согласуются с обновлением DOM на сервере, используя JavaScript через:
- Клиентские хуки:
this.js()или - Экземпляр LiveSocket:
liveSocket.js().
Интерфейс команд, возвращаемый указанным выше js(), предоставляет следующие функции:
-
show(el, opts = {})— показывает элемент. Параметры:display,transition,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.show/1. -
hide(el, opts = {})— скрывает элемент. Параметры:transition,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.hide/1. -
toggle(el, opts = {})— переключает видимость элемента. Параметры:display,in,out,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.toggle/1. -
addClass(el, names, opts = {})— добавляет элементу CSS-класс или классы. Параметры:transition,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.add_class/1. -
removeClass(el, names, opts = {})— удаляет CSS-класс или классы элемента. Параметры:transition,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.remove_class/1. -
toggleClass(el, names, opts = {})— переключает CSS-класс или классы элемента. Параметры:transition,time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.toggle_class/1. -
transition(el, transition, opts = {})— применяет к элементу CSS-переход. Параметры:time,blocking. Подробности см. в разделеPhoenix.LiveView.JS.transition/1. -
setAttribute(el, attr, val)— задаёт атрибут элемента -
removeAttribute(el, attr)— удаляет атрибут элемента -
toggleAttribute(el, attr, val1, val2)— переключает атрибут элемента между двумя значениями -
push(el, type, opts = {})— отправляет событие на сервер. Чтобы адресовать LiveComponent по его ID, передайте отдельныйtargetв параметрах. Параметры:target,loading,page_loading,value. Подробности см. в разделеPhoenix.LiveView.JS.push/1. -
navigate(href, opts = {})— отправляет серверу событие навигации и обновляет историю pushState браузера. Параметры:replace. Подробности см. в разделеPhoenix.LiveView.JS.navigate/1. -
patch(href, opts = {})— отправляет серверу событие обновления и обновляет историю pushState браузера. Параметры:replace. Подробности см. в разделеPhoenix.LiveView.JS.patch/1. -
exec(encodedJS)— только для клиентского хукаthis.js(): выполняет закодированную JS-команду в контексте корневого узла хука. Закодированную JS-команду следует создавать с помощьюPhoenix.LiveView.JS; обычно она хранится в атрибуте HTML. Пример:this.js().exec(this.el.getAttribute('phx-remove')). -
exec(el, encodedJS)— только дляliveSocket.js(): выполняет закодированную JS-команду в контексте любого элемента.
Управление идентификаторами на клиенте
Если нужно задавать идентификаторы элементов из клиентского JavaScript (например, автоматически генерировать идентификаторы для обеспечения доступности), необходимо использовать метод js().setAttribute():
Hooks.MyHook = {
mounted() {
this.js().setAttribute(this.el, "id", "my-generated-id")
}
}
Назначение идентификаторов напрямую через node.id = "..." или с помощью других методов непосредственного управления DOM вызовет проблемы при обновлении DOM. Всегда используйте вместо этого js().setAttribute().
Если сервер уже назначил элементу идентификатор, заменить его на другой со стороны клиента нельзя. Идентификаторы на стороне клиента следует задавать только для элементов, которым сервер не назначил идентификатор.
Хуки и JS-команды за пределами LiveView
Хуки (phx-hook) и команды Phoenix.LiveView.JS используются не только в LiveView. Они также работают на обычных страницах, отрисованных обычным контроллером Phoenix, — страницах без активного соединения (их иногда называют мёртвыми представлениями, мёртвыми рендерами, статическими страницами или просто разметкой за пределами LiveView).
Сюда относится и разметка, расположенная за пределами контейнера LiveView на странице, где есть LiveView, например корневой макет: такие элементы относятся к обычному представлению на уровне body, а не к LiveView.
Чтобы включить эту функциональность, страница должна загрузить и подключить LiveSocket точно так же, как страница LiveView:
import {LiveSocket} from "phoenix_live_view"
const liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})
liveSocket.connect()
Что работает в обычном представлении
-
phx-hook— вызывается callbackmounted, только для элементов, присутствующих при готовности DOM. Другие callback-функции (updated,beforeUpdate,destroyed,disconnected,reconnected) не вызываются, поскольку обычное представление не получает обновлений от сервера. -
phx-mounted— выполняется, когда документ готов (DOMContentLoaded) и вызванliveSocket.connect()(см. раздел Привязки). -
phx-clickи другие привязки событий, запускающие полностью клиентские командыJS, — например,JS.toggle/1,JS.show/1,JS.hide/1,JS.add_class/1,JS.dispatch/1иJS.transition/1. Они выполняются целиком в браузере, поэтому работают без обращения к серверу. -
JS.navigate/1иJS.patch/1— если на странице есть подключённый LiveView (например, ссылка в корневом макете за пределами контейнера LiveView), они выполняют обычную live-навигацию с его помощью. На полностью статической странице без LiveView они корректно переключаются на полноэкранную навигацию браузера к целевому URL.
Что не работает в обычном представлении
- Всё, что требует подключённого LiveView:
JS.push/1, привязки форм (phx-change,phx-submit) и другие привязки событий, отправляющие данные на сервер, — им некуда обращаться, поскольку процесса LiveView нет, поэтому они не работают. - Привязки
phx-connectedиphx-disconnected— они действуют только внутри контейнера LiveView и не работают в обычном представлении.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/js-interop.html