Spec-Zone.ru › htmx

Руководство программиста hx-live

Руководство программиста по hx-live.

hx-live — это расширение htmx для клиентского скриптинга в приложениях на базе htmx, вдохновлённое jQuery, Alpine.js и hyperscript. Оно предоставляет вспомогательные средства, которые делают императивный скриптинг удобнее, а также обеспечивает реактивность, сохраняя при этом подход htmx: локальность поведения и гипермедиа как механизм состояния приложения.

hx-live предоставляет следующие средства для скриптинга:

  • Помощник для поиска элементов, похожий на jQuery, q(), с селекторами относительно текущего элемента, такими как closest .field и next input
  • Полезные DOM-помощники для распространённых задач скриптинга: toggle(), take(), trigger(), insert() и matches()
  • Свойства для хранения типизированного состояния скриптов в DOM: data, attr, aria и class
  • Набор атрибутов реактивной привязки: :hidden, :text, :class и т. д.
  • Реактивный атрибут hx-live для произвольных реактивных выражений
  • Асинхронные помощники: debounce(), forEvent() и nextFrame()

Эти средства интегрируются с уже существующей функциональностью hx-on в htmx, делая скриптинг более мощным, лаконичным и увлекательным.

Начало работы

Установить hx-live можно так же, как любое другое расширение htmx:

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-live.min.js"></script>

Поиск элементов с помощью q()

Первое средство, предоставляемое hx-live, — это q(), метод поиска элементов в DOM. Он заменяет document.querySelector() и document.querySelectorAll():

<button hx-on:click="q('#pick-me').toggle('.hilite')">Toggle</button>
<p id="pick-me">Pick me</p>

Выбери меня

Как и в jQuery, результат q() — это прокси-объект, который может применять одно свойство или функцию к нескольким элементам:

<button hx-on:click="q('.pick-us').toggle('.hilite')">Toggle all</button>
<p class="pick-us">First</p>
<p class="pick-us">Second</p>
<p class="pick-us">Third</p>

Первый

Второй

Третий

q() также поддерживает относительные селекторы, которые находят элементы относительно контекста, в котором он выполняется:

<button hx-on:click="q('next p').toggle('.hilite')">Toggle the next one</button>
<p>I am next</p>
<p>I am not</p>

Я следующий

Я не следующий

DOM-помощники

hx-live предоставляет набор вспомогательных методов, доступных для результата q() и упрощающих распространённые операции с DOM.

toggle()

В примерах выше мы уже видели метод .toggle(). Его можно использовать для переключения классов, атрибутов ARIA и логических атрибутов:

<button hx-on:click="toggle('.hilite')">A class</button>
<button aria-expanded="false" hx-on:click="toggle('aria-expanded')">An ARIA attribute</button>
<button hx-on:click="q('next input').toggle('disabled')">An attribute</button>
<input value="I can be disabled">

Есть три разных варианта:

  • Начальная точка означает класс
  • Префикс aria- переключает строку между значениями "true" и "false"
  • Всё остальное считается логическим атрибутом: он добавляется или удаляется

Можно также передать несколько значений, и toggle() будет переключаться между ними:

<button class="warm" hx-on:click="toggle('.theme', 'warm|cool')">Change theme</button>

Здесь .theme — это имя группы (и признак того, что переключается класс), а toggle будет переключаться между классами warm и cool.

take()

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

Вот набор кнопок вкладок:

<button class="hilite" hx-on:click="take('.hilite')">Details</button>
<button hx-on:click="take('.hilite')">Reviews</button>
<button hx-on:click="take('.hilite')">Shipping</button>

take() принимает те же три варианта, что и toggle():

  • Начальная точка означает класс: он добавляется к этому элементу и удаляется с остальных
  • Префикс aria- задаёт этому элементу значение "true", а остальным — "false"
  • Всё остальное считается логическим атрибутом: он добавляется к этому элементу и удаляется с остальных

Полезно знать вариант с ARIA, поскольку в навигации должен быть ровно один текущий элемент:

<a href="#a" aria-current="true" hx-on:click="take('aria-current')">Home</a>
<a href="#b" hx-on:click="take('aria-current')">Docs</a>
<a href="#c" hx-on:click="take('aria-current')">Examples</a>
Главная Документация Примеры

По умолчанию take() ограничивает область действия дочерними элементами родителя целевого элемента.

Чтобы изменить область, в которой выбирается класс или свойство, можно передать второй аргумент:

<button hx-on:click="q('#one').take('.hilite', '.card')">Pick one</button>
<button hx-on:click="q('#two').take('.hilite', '.card')">Pick two</button>
<p id="one" class="card hilite">One</p>
<p id="two" class="card">Two</p>

Первый

Второй

Здесь кнопки не являются целевыми элементами. q() выбирает целевой элемент, а .card задаёт набор элементов, с которых переносится класс.

trigger()

trigger() предоставляет удобный способ отправить пользовательское событие.

<button hx-on:click="q('#bell').trigger('ring')">Ring the bell</button>
<p id="bell" hx-on:ring="this.textContent = 'Heard it'">Waiting</p>

Ожидание

По умолчанию события всплывают.

Второй аргумент можно использовать как event.detail.

Третий аргумент позволяет остановить всплытие:

trigger('ring', {times: 3})        // event.detail.times is 3
trigger('ring', {}, false)         // does not bubble

insert()

insert() добавляет HTML вокруг элемента или внутрь него. Первый аргумент insert() указывает, куда вставить содержимое:

Позиция Расположение HTML
'before' как соседний элемент перед текущим элементом
'start' как первый дочерний элемент
'end' как последний дочерний элемент
'after' как соседний элемент после текущего элемента
'into' вместо дочерних элементов
'replace' вместо самого элемента
<button hx-on:click="q('#list').insert('end', '<li>Another item</li>')">Add an item</button>
<ul id="list"><li>First item</li></ul>
  • Первый элемент

insert() обработает добавляемое содержимое с помощью htmx.process(), поэтому атрибуты htmx и hx-live в новом содержимом будут работать:

<button hx-on:click="q('#slot').insert('into', '<button hx-on:click=&quot;toggle(`.hilite`)&quot;>New button</button>')">
    Insert a live button
</button>
<span id="slot"></span>

Вспомогательные методы и hx-on

Каждый из описанных выше методов можно использовать в выражении hx-on без уточнения:

<button hx-on:click="toggle('.hilite')">toggle()</button>
<button hx-on:click="take('.hilite')">take()</button>
<button hx-on:click="insert('after', '<em> added</em>')">insert()</button>

При таком использовании контекстным элементом становится элемент, к которому относится обработчик hx-on.

Кроме того, на верхнем уровне доступен стандартный метод DOM matches():

<p hx-on:click="toggle('.hilite'); q('next span').textContent = matches('.hilite')">Click me</p>
<span>false</span>

Нажмите здесь

false

Состояние в DOM

hx-live следует концепции «Гипермедиа как механизм состояния приложения» и помогает хранить состояние скриптов непосредственно в DOM, а не в глобальных переменных JavaScript и т. п.

Свойство data

Чтобы упростить эту задачу, оно предоставляет свойство data.

Этот атрибут можно использовать для чтения и хранения данных, необходимых скриптам. Данные хранятся в DOM посредством атрибутов data-. При записи и чтении данные сериализуются и десериализуются, поэтому можно использовать типизированные данные без необходимости преобразовывать их.

Как и вспомогательные методы, свойство data можно получить через селектор q() или на верхнем уровне выражения hx-on. В последнем случае data будет связано с элементом, на котором находится атрибут hx-on.

Пример:

    <button data-count="0" hx-on:click="q('next output').innerText = ++data.count; ">Add 1</button>
    <output>0</output>
0

Если открыть инструменты разработчика в браузере и наблюдать за страницей во время нажатия кнопки, вы увидите, что обновляется текст тега output, а также атрибут data-count.

Важно понимать, что данные в data-count можно обрабатывать как число, а не как строку: hx-live десериализует и повторно сериализует значение.

Область действия data

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

  <div data-count="0">
    <button hx-on:click="q('next output').innerText = ++data.count; ">Add 1</button>
    <button hx-on:click="q('next output').innerText = --data.count; ">Subtract 1</button>
    <output>0</output>
  </div>
0

Здесь обе кнопки используют общие данные родительского элемента div. (Этот пример было бы проще реализовать с помощью реактивности — мы рассмотрим её чуть позже.)

Чтобы ограничить разрешение data- конкретным элементом, можно использовать свойство local:

  <button data-count="0" 
        hx-on:click="q('next output').innerText = ++this.data.count; ">Add 1</button>

Обычно в этом нет необходимости, но это может пригодиться, если нужно хранить данные на определённом элементе.

Свойство data также доступно для результата запросов q():

  <button hx-on:click="q('#some-div').data.count; ">Add 1</button>

В этой форме также можно использовать модификатор local.

Свойство aria

Во многих случаях клиентское состояние можно и нужно задавать с помощью свойств ARIA, а не атрибутов data, чтобы обеспечить максимальную доступность.

<button aria-expanded="false" aria-controls="details"
        hx-on:click="aria.expanded = !aria.expanded; 
                     q('#details').hidden = !aria.expanded">
    Details
</button>
<p id="details" hidden>
    This panel is driven by aria-expanded, not by a class.
</p>      

Эта панель управляется атрибутом aria-expanded, а не классом.

Обработчик считывает aria.expanded как логическое значение, записывает обратно его противоположное значение и использует его, чтобы скрыть или показать панель.

aria-controls указывает область, которую раскрывает кнопка, чтобы вспомогательные технологии могли перейти по ссылке.

Реактивные привязки

До сих пор мы использовали hx-live в императивном режиме. Он также поддерживает реактивный стиль программирования с помощью двух возможностей:

  • Реактивные атрибуты с префиксом :
  • Атрибут hx-live

Сначала рассмотрим синтаксис привязки атрибутов.

Реактивные атрибуты

Привязка Устанавливает
:<attr> этот атрибут. Логические атрибуты добавляются или удаляются, а null удаляет любой атрибут
:.<class> один класс. Истинное значение добавляет его, ложное — удаляет
:class несколько классов — из строки с именами или объекта, связывающего имена с условиями
:text textContent, поэтому значение никогда не разбирается как HTML
:html innerHTML
:style встроенные стили — из строки с объявлениями или объекта свойств

Вот пример текстового поля, в котором используются реактивный класс .too-much-text, атрибут :text и атрибут :hidden для динамического отображения обратной связи о длине сообщения:

<textarea id="msg" placeholder="Say something" :.too-much-text="q('#msg').value.length > 60"></textarea>
<p :text="`${q('#msg').value.length} / 60`"></p>
<p :hidden="q('#msg').value.length <= 60">Too long.</p>

Слишком длинное сообщение.

Вернёмся к рассмотренному ранее примеру счётчика с общими данными data и упростим его с помощью реактивности:

  <div data-count="0">
    <button hx-on:click="data.count++">Add 1</button>
    <button hx-on:click="data.count--">Subtract 1</button>
    <output :text="data.count">0</output>
  </div>
0

Как видите, реактивность позволяет аккуратно упростить этот фрагмент кода.

Свойство hx-live

Если нужно выполнить произвольную логику, а не только задать атрибут, внутренний текст и т. п., используйте атрибут hx-live.

Он принимает произвольную программу, которая будет повторно выполняться при изменениях.

Вот пример ограничения индикатора выполнения значением 20 % (ну, нужно же было что-то придумать!):

<div data-total="0">
    <button hx-on:click="data.total += 5">Add 5</button>
    <button hx-on:click="data.total = 0">Reset</button>
    <progress max="30" value="0" hx-live="this.value = Math.min(data.total, 20)"></progress>
    <p :text="`${data.total} / 20`"></p>
</div>

Это может пригодиться, когда нужно повторно выполнять произвольную логику после изменения данных.

Реактивность на основе DOM

hx-live использует крупнозернистую реактивность на основе DOM. Это означает, что при пересчёте реактивных элементов система, как правило, отслеживает изменения DOM, а не использует обратные вызовы эффектов, сигналы и т. п.

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

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

Если программно обновить свойство value элемента input, это не обновит DOM и не вызовет событие change. Поэтому, если не прибегать к крайне сомнительному перехвату свойств, который может самым неожиданным образом всё сломать, реактивное решение на основе DOM не сможет отследить такие изменения.

Однако в hx-live есть простое решение: задавайте значения полей ввода через средство поиска q() — тогда изменение будет отслежено и вызовет пересчёт реактивных элементов на странице.

Наконец, если ничего не помогает, пересчитать реактивные элементы можно вызовом htmx.live.refresh().

<button hx-on:click="window.count = (window.count ?? 0) + 1">Change a JS variable</button>
<button hx-on:click="htmx.live.refresh()">Refresh</button>
<p :text="`window.count is ${window.count ?? 0}`"></p>

Конечно, это противоречит духу hx-live, но жизнь бывает непростой, и, как говорят молодые: ничего не поделаешь.

Асинхронные помощники

Последний набор средств hx-live — это методы, которые упрощают асинхронное программирование, особенно в атрибутах hx-on.

debounce()

Метод debounce() позволяет отложить действие: для этого нужно вызвать его с помощью await и передать интервал в миллисекундах. Если за это время обработчик hx-on будет вызван повторно, действие не выполнится, а таймер будет сброшен.

<input placeholder="Type here"
       hx-on:input="await debounce(300); q('#settled').textContent = this.value">
<p>settled: <span id="settled"></span></p>

стабилизированное значение:

forEvent()

Метод forEvent() позволяет дождаться события перед продолжением выполнения. Также ему можно передать необязательный тайм-аут на случай, если событие не произойдёт за указанное время.

Например, с его помощью можно дождаться завершения перехода:

<button hx-on:click="
    class.add('nudge');
    await forEvent('transitionend', 500);
    class.remove('nudge')
">Nudge me</button>

nextFrame()

Метод nextFrame() ожидает следующего кадра анимации. Например, это позволяет запустить CSS-анимацию и дождаться её применения, прежде чем переходить к следующему действию.

<button hx-on:click="class.remove('shake'); class.add('shake')">Without nextFrame</button>
<button hx-on:click="class.remove('shake'); await nextFrame(); class.add('shake')">With nextFrame</button>

Заключение

Вот и всё. Надеемся, hx-live сделает клиентский скриптинг в htmx приятнее.

Licensed under the Zero-Clause BSD License.
https://four.htmx.org/docs/hx-live-guide/

Spec-Zone.ru

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