Turbo Stream Actions: интерактивный атлас DOM-операций

Turbo Stream Actions: интерактивный атлас DOM-операций

Turbo Streams без сервера

Turbo Stream - это маленькая декларативная команда над уже существующим DOM: найти цель, взять HTML из <template> и выполнить одну операцию. Чтобы понять эту механику, не нужны Rails-контроллер, WebSocket или даже сетевой запрос. Достаточно увидеть, какие именно узлы остаются, перемещаются и исчезают.

Ниже - интерактивный атлас всех восьми стандартных действий из Turbo Handbook. Лаборатория симулирует DOM-эффект прямо в статье. Она намеренно не объясняет доставку сообщений, broadcasting и серверный рендеринг: только формат элемента и результат его встраивания в страницу.

Одна команда состоит из четырёх частей

<turbo-stream
  action="append"
  target="messages">
  <template>
    <article>Новое сообщение</article>
  </template>
</turbo-stream>
actionЧто сделать с найденной целью.
target / targetsОдин DOM ID или CSS-селектор для нескольких узлов.
templateHTML, который будет вставлен, если он нужен действию.
сам streamПосле выполнения Turbo удаляет командный элемент из DOM.
Цель не обязана быть <turbo-frame>. Это может быть <div>, строка таблицы, карточка, счётчик или любой другой элемент документа, который находится по ID либо CSS-селектору.

Лаборатория: что визуально делает каждое action

Выберите действие, затем одиночную или множественную цель и нажмите «Применить». Синяя рамка показывает узлы, которые Turbo найдёт до операции. Команда слева меняется вместе с настройками.

DOM laboratory
Количество целей
Будет найден #message_2
Stream elementHTML

    
Documentживой DOM
example.test/inbox
Входящиеpage render #1
0 узлов Выберите действие. Пока DOM не изменён.

Восемь действий, сгруппированные по геометрии

Названия легче запомнить не списком, а по тому, где находится граница изменения.

append

Добавляет содержимое шаблона последними дочерними узлами внутрь цели. Сама цель остаётся.

prepend

Добавляет содержимое первыми дочерними узлами внутрь цели. Сама цель остаётся.

before

Вставляет шаблон рядом с целью, непосредственно перед ней. Новый узел становится её sibling.

after

Вставляет шаблон рядом с целью, непосредственно после неё. Новый узел становится её sibling.

replace

Удаляет сам целевой элемент и ставит на его место шаблон. Старая оболочка и её listeners исчезают.

update

Сохраняет целевой элемент, но заменяет его дочерние узлы. Оболочка, атрибуты и привязанные к ней listeners остаются.

×remove

Удаляет цель. <template> не нужен, потому что вставлять нечего.

refresh

Запускает обновление страницы. У него нет цели и шаблона: это действие над визитом, а не над отдельным узлом.

actionЧто сохраняетсяГде оказывается templateТипичный смысл
appendцель и текущие childrenв конце целидобавить запись в список
prependцель и текущие childrenв начале целипоказать самое новое первым
beforeцель целикомперед цельювставить соседнюю строку
afterцель целикомпосле целидобавить пояснение или продолжение
replaceничего от старой целивместо целиперерисовать компонент
updateоболочка целивнутрь очищенной целисменить содержимое счётчика или панели
removeсоседи целишаблона нетубрать запись
refreshзависит от page refresh strategyшаблона нетобновить страницу целиком

Тонкость append и prepend: совпавший ID заменяется

Если первый элемент шаблона имеет ID, который уже принадлежит прямому ребёнку целевого контейнера, Turbo заменит существующего ребёнка вместо добавления дубликата. Это делает повторное добавление записи идемпотентнее, но проверка относится именно к прямым children.

morph - не девятое action. Это метод выполнения для replace или update: method="morph". Обычный вариант без атрибута жёстко меняет узел или children; morph пытается согласовать старое и новое дерево и сохранить подходящие DOM-узлы. Поэтому его эффект зависит от структуры и стабильных ID.

Actions With Multiple Targets

target="message_2" означает поиск одного элемента по DOM ID. targets=".pending" означает querySelectorAll(".pending"): одна и та же операция применяется ко всем найденным элементам, а содержимое шаблона клонируется для каждой цели.

CSS selector playground

Попробуйте селекторы .pending, .priority, [data-owner="me"] или .task:not(.done). Селектор вычисляется в момент выполнения команды. Если совпадений нет, DOM просто не изменится. Если CSS-селектор синтаксически неверен, команда не сможет разрешить цели.

Несколько целей не означают несколько разных изменений. Один элемент с targets повторяет одно action над набором узлов. Чтобы сделать prepend в одном месте, update в другом и remove в третьем, нужны несколько элементов <turbo-stream>.

Несколько команд выполняются по порядку

Одно stream-сообщение может содержать несколько соседних <turbo-stream>. Turbo исполняет их в порядке появления. Это отдельная ось композиции: targets размножает одну операцию, а несколько stream-элементов описывают сценарий из разных операций.

Две операции, один локальный сценарий

<turbo-stream action="prepend"
  target="wishlist_items">
  <template>...Работа...</template>
</turbo-stream>

<turbo-stream action="update"
  target="quick_create">
  <template>Список создан</template>
</turbo-stream>
Списки желаний2 списка
Измените любой существующий чекбокс, затем выполните обе операции. Его локальное состояние сохранится.

Первая команда добавляет только новую строку. Вторая меняет только содержимое формы быстрого создания. Уже существующие строки не перерисовываются, поэтому несохранённый выбор чекбокса остаётся в браузере. Это и есть преимущество маленьких DOM-операций: граница изменения видна прямо в разметке сообщения.

Как выбирать действие

Список растёт?append или prepend. Выберите сторону, где пользователь ожидает новую запись.
Меняется компонент?replace, если нужна новая оболочка; update, если оболочка является стабильным контрактом.
Одинаковая правка везде?targets с узким CSS-селектором. Для разных правок используйте несколько stream-элементов.

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

Короткая проверка перед использованием

  1. Стабилен ли ID цели между рендерами?
  2. Нужно сохранить сам элемент или только его место? Это выбор между update и replace.
  3. Есть ли внутри цели несохранённое браузерное состояние?
  4. Нужна одна операция для набора элементов или разные операции для разных мест?
  5. Останется ли новая разметка доступной для следующей команды по тому же ID или селектору?

Для общей картины о навигационных областях вернитесь к статье «Turbo Frames с нуля». Там Frame рассматривается как контекст перехода, а Stream - как точечная DOM-команда. Здесь мы намеренно разобрали только вторую половину.