Руководство программиста 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="toggle(`.hilite`)">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> Если открыть инструменты разработчика в браузере и наблюдать за страницей во время нажатия кнопки, вы увидите, что обновляется текст тега 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> Здесь обе кнопки используют общие данные родительского элемента 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> Как видите, реактивность позволяет аккуратно упростить этот фрагмент кода.
Свойство 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/