Привязки
Исходный кодPhoenix поддерживает привязки элементов DOM для взаимодействия клиента и сервера. Например, чтобы реагировать на нажатие кнопки, нужно отобразить элемент:
<button phx-click="inc_temperature">+</button>
Затем на сервере все привязки LiveView обрабатываются с помощью обратного вызова handle_event, например:
def handle_event("inc_temperature", _value, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
| Привязка | Атрибуты |
|---|---|
| Параметры | phx-value-* |
| События щелчка |
phx-click, phx-click-away
|
| События формы |
phx-change, phx-submit, phx-disable-with, phx-trigger-action, phx-auto-recover
|
| События фокуса |
phx-blur, phx-focus, phx-window-blur, phx-window-focus
|
| События клавиш |
phx-keydown, phx-keyup, phx-window-keydown, phx-window-keyup, phx-key
|
| События прокрутки |
phx-viewport-top, phx-viewport-bottom
|
| Обновление DOM |
phx-update, phx-mounted, phx-remove
|
| Взаимодействие с JS | phx-hook |
| События жизненного цикла |
phx-connected, phx-disconnected
|
| Ограничение частоты |
phx-debounce, phx-throttle
|
| Отслеживание статических изменений | phx-track-static |
Если вам нужно запускать команды через JavaScript, см. раздел Взаимодействие с JavaScript.
События щелчка
Привязка phx-click используется для отправки событий щелчка на сервер. Когда отправляется любое клиентское событие, например щелчок phx-click, значение, отправляемое на сервер, выбирается в следующем порядке приоритета:
-
Значение
:value, указанное вPhoenix.LiveView.JS.push/3, например:<div phx-click={JS.push("inc", value: %{myvar1: @val1})}> -
Любое количество необязательных атрибутов с префиксом
phx-value-, например:<div phx-click="inc" phx-value-myvar1="val1" phx-value-myvar2="val2">
отправит на сервер следующую карту параметров:
def handle_event("inc", %{"myvar1" => "val1", "myvar2" => "val2"}, socket) doЕсли используется префикс
phx-value-, данные, отправляемые на сервер, также будут содержать"value", если у элемента есть атрибут value. Обратите внимание: если у элемента есть свойствоvalue, как у элемента<li>, оно имеет приоритет надphx-value-value. Если нужно переопределить ключ"value"в данных, можно использоватьPhoenix.LiveView.JS.push/3с параметромvalue. -
Данные также будут включать любые дополнительные пользовательские метаданные клиентского события. Например, следующий клиентский параметр
LiveSocketотправит координаты и сведенияaltKeyдля всех щелчков:let liveSocket = new LiveSocket("/live", Socket, { params: {_csrf_token: csrfToken}, metadata: { click: (e, el) => { return { altKey: e.altKey, clientX: e.clientX, clientY: e.clientY } } } })
Событие phx-click-away срабатывает, когда щелчок происходит вне элемента. Это удобно для скрытия переключаемых контейнеров, например выпадающих списков.
События фокуса и потери фокуса
События фокуса и потери фокуса можно привязать к элементам DOM, которые генерируют такие события, с помощью привязок phx-blur и phx-focus, например:
<input name="email" phx-focus="myfocus" phx-blur="myblur"/>
Чтобы определить, когда сама страница получила или потеряла фокус, можно указать phx-window-focus и phx-window-blur. Эти события уровня окна также могут понадобиться, если рассматриваемый элемент (чаще всего div без tabindex) не может получить фокус. Как и для других привязок, для привязанного элемента можно указать phx-value-*, и эти значения будут отправлены в составе данных. Например:
<div class="container"
phx-window-focus="page-active"
phx-window-blur="page-inactive"
phx-value-page="123">
...
</div>
События клавиш
События onkeydown и onkeyup поддерживаются с помощью привязок phx-keydown и phx-keyup. Каждая привязка поддерживает атрибут phx-key, который запускает событие при нажатии определённой клавиши. Если phx-key не задан, событие запускается при нажатии любой клавиши. При отправке на сервер передаётся значение "key" нажатой клавиши, а также любые пользовательские метаданные. Например, нажатие клавиши Escape выглядит так:
%{"key" => "Escape"}
Чтобы захватить дополнительные пользовательские метаданные, для событий keydown можно передать параметр metadata конструктору LiveSocket. Например:
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
metadata: {
keydown: (e, el) => {
return {
key: e.key,
metaKey: e.metaKey,
repeat: e.repeat
}
}
}
})
Чтобы определить, какая клавиша была нажата, следует использовать значение key. Доступные варианты перечислены на сайте MDN или в средстве просмотра событий клавиатуры.
Примечание: phx-keyup и phx-keydown не поддерживаются для полей ввода. Вместо них используйте привязки форм, например phx-change, phx-submit и т. д.
Примечание: некоторые функции браузера, например автозаполнение, могут вызывать события клавиш, при этом в карте значений, отправляемой на сервер, не будет поля "key". Поэтому мы рекомендуем всегда иметь резервный обработчик событий для всех привязок клавиш LiveView. По умолчанию обработчиком событий будет привязанный элемент, однако привязку на уровне окна можно указать с помощью phx-window-keydown или phx-window-keyup, например:
def render(assigns) do
~H"""
<div id="thermostat" phx-window-keyup="update_temp">
Current temperature: {@temperature}
</div>
"""
end
def handle_event("update_temp", %{"key" => "ArrowUp"}, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", %{"key" => "ArrowDown"}, socket) do
{:ok, new_temp} = Thermostat.dec_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", _, socket) do
{:noreply, socket}
end
Ограничение частоты событий с помощью debounce и throttle
Частоту всех событий на клиенте можно ограничить с помощью привязок phx-debounce и phx-throttle, за исключением привязки phx-blur, которая срабатывает немедленно.
Для событий с ограниченной частотой и debounce действует следующее поведение:
phx-debounce— принимает целочисленное значение тайм-аута (в миллисекундах) или"blur". Если указано целое число, отправка события откладывается на заданное количество миллисекунд. Если указано"blur", отправка события откладывается до тех пор, пока пользователь не снимет фокус с поля. Если значение не указано, используется значение по умолчанию — 300 мс. Debounce обычно применяется к элементам ввода.phx-throttle— принимает целочисленное значение тайм-аута, задающее ограничение частоты события в миллисекундах. В отличие от debounce, throttle отправляет событие немедленно, а затем ограничивает частоту отправки одним разом за указанный интервал. Если значение не указано, используется значение по умолчанию — 300 мс. Throttle обычно используется для ограничения частоты щелчков, действий мышью и клавиатурой.
Например, чтобы не проверять адрес электронной почты, пока поле не потеряет фокус, и проверять имя пользователя не чаще одного раза в две секунды после изменения поля:
<form id="my-form" phx-change="validate" phx-submit="save"> <input type="text" name="user[email]" phx-debounce="blur"/> <input type="text" name="user[username]" phx-debounce="2000"/> </form>
А чтобы ограничить частоту щелчков по кнопке увеличения громкости одним разом в секунду:
<button phx-click="volume_up" phx-throttle="1000">+</button>
Аналогично, можно ограничить частоту удерживаемых клавиш при keydown:
<div phx-window-keydown="keydown" phx-throttle="500"> ... </div>
Если удержание клавиш не требуется, обычно лучше использовать привязки phx-keyup, которые срабатывают только при отпускании клавиши, тем самым ограничивая частоту самостоятельно. Однако phx-keydown полезен для игр и других сценариев, где требуется постоянное нажатие клавиши. В таких случаях всегда следует использовать throttle.
Особое поведение debounce и throttle
Для форм и привязок keydown предусмотрено следующее особое поведение:
При срабатывании
phx-submitилиphx-changeдля другого поля ввода все текущие таймеры debounce или throttle для существующих полей сбрасываются.Привязка
phx-keydownограничивает частоту только для повторных нажатий клавиши. Последовательные нажатия разных клавиш вызывают события нажатия для каждой клавиши.
Команды JS
Привязки LiveView поддерживают интерфейс команд JavaScript через модуль Phoenix.LiveView.JS. Он позволяет задавать служебные операции, выполняемые на клиенте при срабатывании событий привязок phx-, например phx-click, phx-change и т. д. Команды можно объединять, чтобы отправлять события, добавлять классы элементам, анимировать их появление и исчезновение и выполнять другие действия. Полное описание использования см. в документации Phoenix.LiveView.JS.
Для краткого примера возможностей представьте, что нужно показывать и скрывать модальное окно на странице, не обращаясь к серверу для отображения содержимого:
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.show(to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.hide(to: "#modal", transition: "fade-out")}>
hide modal
</button>
<button phx-click={JS.toggle(to: "#modal", in: "fade-in", out: "fade-out")}>
toggle modal
</button>
Или, если ваша библиотека пользовательского интерфейса использует классы для показа и скрытия элементов:
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.add_class("show", to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
Команды можно объединять. Например, можно отправить событие на сервер и сразу же скрыть модальное окно на клиенте:
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.push("modal-closed") |> JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
Также удобно выносить команды в отдельные функции:
alias Phoenix.LiveView.JS
def hide_modal(js \\ %JS{}, selector) do
js
|> JS.push("modal-closed")
|> JS.remove_class("show", to: selector, transition: "fade-out")
end
<button phx-click={hide_modal("#modal")}>hide modal</button>
Команда Phoenix.LiveView.JS.push/3 особенно полезна, поскольку позволяет настраивать событие, отправляемое на сервер. Например, представьте, что у вас есть знакомая кнопка phx-click, которая при нажатии отправляет сообщение на сервер:
<button phx-click="clicked">click</button>
Теперь представьте, что нужно настроить поведение при отправке события "clicked": например, указать, какой компонент должен получить событие, какой элемент должен получить CSS-классы состояния загрузки и т. д. Это можно сделать с помощью параметров команды push JS. Например:
<button phx-click={JS.push("clicked", target: @myself, loading: ".container")}>click</button>
Все поддерживаемые параметры см. в разделе Phoenix.LiveView.JS.push/3.
Обновление DOM
Для настройки обновления DOM контейнер можно пометить атрибутом phx-update. Поддерживаются следующие значения:
replace— действие по умолчанию. Заменяет элемент содержимымstream— поддерживает операции с потоками. Потоки позволяют управлять большими коллекциями в интерфейсе, не сохраняя коллекцию на сервереignore— игнорирует обновления DOM независимо от изменений содержимого. Это полезно для взаимодействия на стороне клиента с существующими библиотеками, которые выполняют собственные операции с DOM
При использовании phx-update контейнеру всегда необходимо задавать уникальный идентификатор DOM. При использовании «stream» идентификатор DOM также необходимо задавать каждому дочернему элементу. Если вставляемые элементы потока содержат идентификатор, уже присутствующий в контейнере, LiveView заменит существующий элемент новым содержимым. Дополнительную информацию см. в разделе Phoenix.LiveView.stream/3.
Поведение «ignore» часто используется при интеграции с другой библиотекой JS. Обновления содержимого и атрибутов элемента с сервера игнорируются, за исключением атрибутов data. Изменения, добавления и удаления атрибутов data на сервере объединяются с игнорируемым элементом; это можно использовать для передачи данных обработчику JS.
Чтобы реагировать на добавление элементов в DOM, можно использовать привязку phx-mounted. Например, чтобы анимировать элемент при добавлении:
<div phx-mounted={JS.transition("animate-ping", time: 500)}>
Если phx-mounted используется при первоначальной отрисовке страницы, он сработает при первой возможности. Для элементов вне LiveView это произойдёт сразу после выполнения liveSocket.connect(). Для элементов внутри LiveView — только после установки начального соединения с сокетом и монтирования LiveView. См. раздел Хуки и команды JS вне LiveView.
Чтобы реагировать на удаление элементов из DOM, можно указать привязку phx-remove, в которой можно задать команду Phoenix.LiveView.JS для выполнения. Команда phx-remove выполняется только для удалённого родительского элемента. Она не применяется к дочерним элементам.
Чтобы реагировать на обновление элементов в DOM, потребуется использовать хук, который предоставляет полный доступ к жизненному циклу элемента.
События жизненного цикла
LiveView поддерживает привязки phx-connected и phx-disconnected для реакции на события жизненного цикла соединения с помощью команд JS. Например, чтобы показывать элемент при потере соединения LiveView и скрывать его после восстановления соединения:
<div id="status" class="hidden" phx-disconnected={JS.show()} phx-connected={JS.hide()}>
Attempting to reconnect...
</div>
phx-connected и phx-disconnected выполняются только внутри контейнера LiveView. В статических шаблонах они не влияют на результат. См. раздел Хуки и команды JS вне LiveView.
Префикс событий LiveView
Префикс событий lv: поддерживает специальные функции LiveView, которые обрабатываются LiveView без вызова пользовательских обратных вызовов handle_event/3. В настоящее время поддерживаются следующие события:
-
lv:clear-flash— очищает flash при отправке на сервер. Если указанphx-value-key, из flash будет удалён соответствующий ключ.
Например:
<p class="alert" phx-click="lv:clear-flash" phx-value-key="info">
{Phoenix.Flash.get(@flash, :info)}
</p>
События прокрутки и бесконечная пагинация
Привязки phx-viewport-top и phx-viewport-bottom позволяют определить, когда первый дочерний элемент контейнера достигает верхнего края области просмотра или последний дочерний элемент достигает её нижнего края. Это полезно для бесконечной прокрутки, когда нужно отправлять события пагинации для следующей или предыдущей группы результатов по мере того, как пользователь прокручивает страницу вверх или вниз и достигает верхнего или нижнего края области просмотра.
При реализации бесконечной прокрутки приложения обычно добавляют отступы над и под контейнером, чтобы прокрутка оставалась плавной во время загрузки результатов. В сочетании с Phoenix.LiveView.stream/3 привязки phx-viewport-top и phx-viewport-bottom позволяют создать бесконечный виртуализированный список, в DOM которого хранится лишь небольшое количество реальных элементов. Например:
def mount(_, _, socket) do
{:ok,
socket
|> assign(page: 1, per_page: 20)
|> paginate_posts(1)}
end
defp paginate_posts(socket, new_page) when new_page >= 1 do
%{per_page: per_page, page: cur_page} = socket.assigns
posts = Blog.list_posts(offset: (new_page - 1) * per_page, limit: per_page)
{posts, at, limit} =
if new_page >= cur_page do
{posts, -1, per_page * 3 * -1}
else
{Enum.reverse(posts), 0, per_page * 3}
end
case posts do
[] ->
assign(socket, end_of_timeline?: at == -1)
[_ | _] = posts ->
socket
|> assign(end_of_timeline?: false)
|> assign(:page, new_page)
|> stream(:posts, posts, at: at, limit: limit)
end
end
Наша функция paginate_posts загружает страницу публикаций и определяет, запрашивает ли пользователь предыдущую или следующую страницу. В зависимости от направления пагинации поток либо дополняется в начале, либо в конце с помощью at для 0 или -1 соответственно. Мы также задаём для потока limit, равный тройному значению per_page, чтобы в интерфейсе отображалось достаточно публикаций для создания бесконечного списка, но при этом сохранялась высокая производительность. Кроме того, мы задаём присваивание @end_of_timeline?, чтобы отслеживать, достиг ли пользователь конца результатов. Наконец, мы обновляем присваивание @page и поток публикаций. Теперь можно настроить контейнер для поддержки событий области просмотра:
<ul
id="posts"
phx-update="stream"
phx-viewport-top={@page > 1 && JS.push("prev-page", page_loading: true)}
phx-viewport-bottom={!@end_of_timeline? && JS.push("next-page", page_loading: true)}
class={[
if(@end_of_timeline?, do: "pb-10", else: "pb-[calc(200vh)]"),
if(@page == 1, do: "pt-10", else: "pt-[calc(200vh)]")
]}
>
<li :for={{id, post} <- @streams.posts} id={id}>
<.post_card post={post} />
</li>
</ul>
<div :if={@end_of_timeline?} class="mt-5 text-[50px] text-center">
🎉 You made it to the beginning of time 🎉
</div>
Здесь не так много кода, и в этом смысл! Этот небольшой фрагмент интерфейса реализует полностью виртуализированный список с бесконечной прокруткой в обоих направлениях. Мы используем привязку phx-viewport-top для отправки события "prev-page" в LiveView, но только если пользователь находится за пределами первой страницы. Загружать результаты для отрицательного номера страницы бессмысленно, поэтому в таких случаях мы полностью удаляем привязку. Затем мы настраиваем phx-viewport-bottom для отправки события "next-page", но только если мы ещё не достигли конца временной шкалы. Наконец, в зависимости от текущей пагинации мы условно применяем CSS-классы, задающие большие верхний и нижний отступы — вдвое больше высоты области просмотра, — чтобы обеспечить плавную прокрутку.
Чтобы завершить решение, нужно лишь обработать события "prev-page" и "next-page" в LiveView:
def handle_event("next-page", _, socket) do
{:noreply, paginate_posts(socket, socket.assigns.page + 1)}
end
def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, paginate_posts(socket, 1)}
end
def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, paginate_posts(socket, socket.assigns.page - 1)}
else
{:noreply, socket}
end
end
Этот код просто вызывает функцию paginate_posts, которую мы определили на первом шаге, используя текущую или следующую страницу для получения результатов. Обратите внимание, что в событии "prev-page" мы сопоставляем специальный параметр "_overran" => true. События области просмотра отправляют этот параметр, когда пользователь «переместился за пределы» её верхней или нижней границы. Представьте, что пользователь прокручивает назад через множество страниц результатов, но затем захватывает ползунок прокрутки и сразу возвращается в начало страницы. Это означает, что верхняя граница области просмотра вышла за пределы контейнера <ul id="posts">, и нужно сбросить интерфейс, чтобы отобразить первую страницу.
При тестировании для проверки событий области просмотра можно использовать Phoenix.LiveViewTest.render_hook/3:
view
|> element("#posts")
|> render_hook("next-page")
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/bindings.html