Spec-Zone.ru › Phoenix

Синхронизация изменений и оптимистичные интерфейсы

Исходный код

При использовании LiveView изменения состояния в процессе LiveView автоматически отправляются клиенту и применяются на нём.

Однако во многих случаях у клиента может быть собственное состояние: поля ввода, кнопки, элементы интерфейса в фокусе и многое другое. Чтобы обновления с сервера не уничтожали состояние клиента, LiveView предлагает несколько функций и удобных решений, доступных из коробки.

Для начала обсудим проблемы, которые могут возникнуть при взаимодействии клиента и сервера и которые характерны для любого веб-приложения, а затем рассмотрим, как LiveView автоматически их решает. Если вас интересуют главным образом практические аспекты, перейдите к следующим разделам или посмотрите видео ниже:

Суть проблемы

Представьте, что в вашем веб-приложении есть форма. В ней одно поле для ввода адреса электронной почты и кнопка. Нужно проверить, уникален ли адрес в нашей базе данных, и отобразить рядом с полем маленький символ «✗» или «✓» в зависимости от результата. Поскольку мы используем рендеринг на стороне сервера, мы применяем debounce/throttle для отправки изменений формы на сервер. А чтобы избежать повторной отправки, хотим отключать кнопку сразу после нажатия.

Вот что может произойти. Пользователь ввёл «hello@example.», и срабатывает debounce, в результате чего клиент отправляет событие на сервер. В этот момент клиент выглядит так:

[ hello@example.    ]

    ------------
       SUBMIT
    ------------

Пока сервер обрабатывает эту информацию, пользователь заканчивает вводить адрес электронной почты и нажимает кнопку отправки. Клиент отправляет событие отправки на сервер, затем отключает кнопку и меняет её текст на «ОТПРАВКА»:

[ hello@example.com ]

    ------------
     SUBMITTING
    ------------

Сразу после нажатия кнопки отправки клиент получает обновление с сервера, но это обновление вызвано событием debounce! Если бы клиент просто отобразил это обновление сервера, форма вернулась бы к предыдущему состоянию, показанному ниже, что привело бы к катастрофе:

[ hello@example.    ] ✓

    ------------
       SUBMIT
    ------------

Это простой пример того, как состояние клиента и сервера может изменяться и расходиться на некоторое время из-за задержки (расстояния) между ними. Такая ситуация возможна в любом веб-приложении, а не только в LiveView.

LiveView решает эту проблему двумя способами:

  • JavaScript-клиент всегда является источником истины для текущих значений полей ввода

  • LiveView отслеживает количество событий, которые в данный момент обрабатываются для конкретного поля ввода, кнопки или формы. Изменения формы применяются в фоновом режиме по мере их поступления, но LiveView отображает их, только когда обработка всех событий завершена

Иными словами, в большинстве распространённых случаев LiveView автоматически синхронизирует состояние клиента и сервера. Это большое преимущество LiveView: во многих других стеках разработчикам пришлось бы решать эти проблемы самостоятельно. Подробную информацию о работе LiveView с формами см. в разделе особенности JavaScript-клиента на странице «Привязки форм».

Оптимистичные интерфейсы с помощью классов загрузки

Когда HTML-элемент отправляет событие на сервер, LiveView добавляет к нему класс -loading. Например, для следующей разметки:

<button phx-click="clicked" phx-window-keydown="key">...</button>

При нажатии будет добавлен класс phx-click-loading, а при нажатии клавиши — класс phx-keydown-loading. Классы загрузки CSS сохраняются до получения клиентом подтверждения для отправленного события. Если событие элемента срабатывает несколько раз, состояние загрузки снимается только после обработки всех событий.

Это значит, что простейшие улучшения оптимистичного интерфейса в LiveView можно реализовать, просто добавив правило CSS. Например, представьте, что при нажатии вы хотите сделать текст элемента бледнее, пока ожидается ответ:

.phx-click-loading.opaque-on-click {
  opacity: 50%;
}

Теперь, добавив класс opaque-on-click к любому элементу, вы обеспечите мгновенную реакцию на нажатие.

Классы загрузки CSS добавляются при следующих событиях:

  • phx-click — phx-click-loading
  • phx-change — phx-change-loading
  • phx-submit — phx-submit-loading
  • phx-focus — phx-focus-loading
  • phx-blur — phx-blur-loading
  • phx-window-keydown — phx-keydown-loading
  • phx-window-keyup — phx-keyup-loading

Для событий, происходящих внутри формы, состояние применяется и к элементу, и к форме. При изменении поля ввода класс phx-change-loading применяется и к полю, и к форме. При отправке формы кнопка и форма получают классы phx-submit-loading. В частности, для кнопок также поддерживается атрибут phx-disable-with, позволяющий изменить текст кнопки при нажатии:

<button phx-disable-with="Submitting...">Submit</button>

Интеграция с Tailwind

Если вы используете Tailwind, можно воспользоваться плагином addVariant, чтобы упростить настройку состояния загрузки элементов.

plugins: [
  plugin(({ addVariant }) => {
    addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &",]);
    addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &",]);
    addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &",]);
  }),
],

Теперь, чтобы сделать элемент бледнее при нажатии, достаточно добавить:

<button phx-click="clicked" class="phx-click-loading:opacity-50">...</button>

Оптимистичные интерфейсы с помощью команд JS

Классы загрузки очень удобны, но применяются только к элементу, на который нажали. Иногда может понадобиться нажать кнопку «Удалить», но пометить как загружаемую всю строку, в которой находится кнопка (например, чтобы она постепенно исчезла).

С помощью команд JS можно указать LiveView, к каким элементам следует применить состояние загрузки:

<button phx-click={JS.push("delete", loading: "#post-row-13")}>Delete</button>

Помимо пользовательских элементов загрузки, с помощью команд JS можно выполнять самые разные операции: добавлять и удалять классы, переключать атрибуты, скрывать элементы, задавать переходы и многое другое.

Например, если нужно сразу удалить элемент со страницы при нажатии, это можно сделать так:

<button phx-click={JS.push("delete") |> JS.hide()}>Delete</button>

Если нужно удалить не нажатую кнопку, а её родительский или другой элемент, можно передать селектор для скрытия:

<button phx-click={JS.push("delete") |> JS.hide(to: "#post-row-13")}>Delete</button>

Или вместо этого можно добавить класс:

<button phx-click={JS.push("delete") |> JS.add_class("opacity-50")}>Delete</button>

Одно из ключевых свойств команд JS, таких как hide и add_class, — их осведомлённость об изменениях DOM. Поэтому операции, применённые с помощью API JS, сохраняются для элементов при обновлениях с сервера.

Команды JS также включают функцию dispatch, которая отправляет событие DOM-элементу, чтобы запустить специфичную для клиента функциональность. Например, чтобы скопировать данные в буфер обмена, можно реализовать следующий обработчик события:

window.addEventListener("app:clipcopy", (event) => {
  if ("clipboard" in navigator) {
    if (event.target.tagName === "INPUT") {
      navigator.clipboard.writeText(event.target.value);
    } else {
      navigator.clipboard.writeText(event.target.textContent);
    }
  } else {
    alert(
      "Sorry, your browser does not support clipboard copy.\nThis generally requires a secure origin — either HTTPS or localhost.",
    );
  }
});

Затем вызовите его следующим образом:

<button phx-click={JS.dispatch("app:clipcopy", to: "#printed-output")}>Copy</button>

Для добавления переходов нужно всего несколько символов:

<div id="item">My Item</div>
<button phx-click={JS.transition("shake", to: "#item")}>Shake!</button>

Дополнительные примеры и документацию см. в разделе Phoenix.LiveView.JS.

Оптимистичные интерфейсы с помощью хуков JS

В самых сложных случаях можно взять управление DOM-элементом на себя и точно контролировать, как и когда к элементу на странице применяются обновления сервера. Подробнее см. в разделе «Клиентские хуки с помощью phx-hook» на странице взаимодействия с JavaScript.

Live-навигация

LiveView также предоставляет механизмы для настройки событий навигации и взаимодействия с ними.

Классы навигации

Следующие классы применяются к родительскому контейнеру LiveView:

  • "phx-connected" — применяется, когда представление подключено к серверу
  • "phx-loading" — применяется, когда представление не подключено к серверу
  • "phx-error" — применяется при возникновении ошибки на сервере. Обратите внимание: этот класс применяется вместе с "phx-loading", если соединение с сервером потеряно.

События навигации

При навигации по страницам в режиме live с помощью <.link navigate={...}> и <.link patch={...}>, их серверных аналогов push_navigate и push_patch, а также при отправке форм с помощью phx-submit, в window отправляются события JavaScript "phx:page-loading-start" и "phx:page-loading-stop". Это удобно, например, для отображения состояния загрузки основной страницы:

// app.js
import topbar from "topbar"
window.addEventListener("phx:page-loading-start", info => topbar.show(500))
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

Внутри функции обратного вызова info.detail будет объектом, содержащим ключ kind. Его значение зависит от вызвавшего события:

  • "redirect" — событие вызвано перенаправлением
  • "patch" — событие вызвано обновлением
  • "initial" — событие вызвано первоначальной загрузкой страницы
  • "element" — событие вызвано элементом с привязкой phx-, например phx-click
  • "error" — событие вызвано ошибкой, например сбоем представления или отключением сокета

Кроме того, Phoenix.LiveView.JS.push/3 может отправлять события загрузки страницы, если передать параметр page_loading: true.

Для всех типов событий загрузки страницы, кроме "element", в метаданные info будет добавлен ключ to, указывающий href, связанный с загрузкой страницы. В случае события загрузки страницы "element" в info будет содержаться ключ "target" со ссылкой на DOM-элемент, вызвавший состояние загрузки страницы.

Событие более низкого уровня phx:navigate также вызывается каждый раз, когда Phoenix программно изменяет URL в адресной строке браузера или пользователь переходит вперёд либо назад. В info.detail содержится следующая информация:

  • "href" — адрес, на который перешли в адресной строке.
  • "patch" — логическое значение, указывающее, был ли переход навигацией с обновлением.
  • "pop" — логическое значение, указывающее, был ли переход выполнен с помощью popstate при переходе пользователя вперёд или назад по истории.

Для логики, зависящей от навигации, предпочтительнее использовать phx:navigate, а не хуки обратного вызова, такие как updated(), поскольку хуки могут сработать до обновления window.location.

← Предыдущая страница Взаимодействие с JavaScript
Следующая страница → Атрибуты HTML phx-*

Пакет Hex Предварительный просмотр Hex (текущий файл) Просмотреть llms.txt Скачать версию ePub

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

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

Spec-Zone.ru

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