LEARN X · ЗА 13 МИН

htmx

htmx за 13 минут: AJAX через HTML-атрибуты без JavaScript. hx-get, hx-target, hx-swap, триггеры, индикаторы, hx-boost и живой поиск на примерах.

htmx — это маленькая библиотека, которая возвращает гипермедиа в центр веб-разработки. Вместо того чтобы писать JavaScript и гонять JSON, вы добавляете атрибуты прямо в HTML: ссылка или кнопка сама шлёт AJAX-запрос, а сервер отвечает готовым куском HTML, который htmx вставляет на страницу. Этот тур — вся суть htmx на одной странице, через плотно закомментированный код. Читайте сверху вниз.

Что такое htmx

htmx расширяет обычный HTML несколькими атрибутами. Любой элемент может делать HTTP-запросы, а ответ — это фрагмент разметки, а не данные.

<!-- Обычная ссылка перезагружает страницу целиком -->
<a href="/news">Новости</a>

<!-- С htmx кнопка шлёт GET /news через AJAX -->
<!-- и подставляет ответ внутрь #content без перезагрузки -->
<button hx-get="/news" hx-target="#content">Новости</button>
<div id="content"><!-- сюда придёт HTML с сервера --></div>

<!-- Идея htmx: HTML-first и гипермедиа. -->
<!-- Логика живёт в атрибутах, сервер отдаёт готовую разметку, -->
<!-- а не JSON, который надо превращать в DOM руками. -->

Подключение

Достаточно одного тега <script>. Никакой сборки и npm не требуется.

<!-- Подключаем htmx с CDN в <head> или перед </body> -->
<script src="https://unpkg.com/[email protected]"></script>

<!-- Для продакшена обычно указывают integrity-хеш -->
<!-- или кладут файл локально и раздают со своего сервера. -->
<!-- После подключения все hx-атрибуты на странице -->
<!-- начинают работать автоматически. -->

HTTP-запросы

Четыре атрибута соответствуют HTTP-методам. Значение — это URL, на который уйдёт запрос.

<!-- GET: получить данные (чаще всего для чтения) -->
<button hx-get="/articles">Загрузить статьи</button>

<!-- POST: создать новую сущность -->
<button hx-post="/articles">Создать</button>

<!-- PUT: обновить существующую сущность целиком -->
<button hx-put="/articles/42">Сохранить</button>

<!-- DELETE: удалить сущность -->
<button hx-delete="/articles/42">Удалить</button>

<!-- Без hx-target ответ по умолчанию заменит -->
<!-- внутренность (innerHTML) самого элемента. -->

Цель обновления: hx-target и hx-swap

hx-target указывает, какой элемент обновить, а hx-swap — как именно вставить ответ.

<!-- hx-target принимает CSS-селектор -->
<button hx-get="/profile" hx-target="#box">Профиль</button>
<div id="box"></div>

<!-- hx-swap: способ вставки ответа -->
<!-- innerHTML  — заменить содержимое цели (по умолчанию) -->
<!-- outerHTML  — заменить сам элемент целиком -->
<!-- beforeend  — добавить в конец цели (дописать) -->
<!-- afterbegin — добавить в начало цели -->
<!-- delete     — удалить цель после ответа -->
<button hx-get="/more"
        hx-target="#list"
        hx-swap="beforeend">Ещё</button>
<ul id="list"><!-- новые <li> допишутся в конец --></ul>

<!-- Особые значения hx-target: -->
<!-- this — сам элемент, closest tr — ближайший предок tr -->
<button hx-delete="/row/7" hx-target="closest tr">X</button>

Триггеры: hx-trigger

Атрибут hx-trigger задаёт событие, по которому уходит запрос. У каждого элемента есть разумное событие по умолчанию.

<!-- По умолчанию: click для кнопок, change для select, -->
<!-- submit для форм, change для input. Это можно переопределить. -->

<!-- Запрос при потере фокуса полем -->
<input hx-get="/check" hx-trigger="blur">

<!-- keyup: на каждое нажатие клавиши -->
<input hx-get="/search" hx-trigger="keyup">

<!-- load: запрос сразу после появления элемента в DOM -->
<div hx-get="/widget" hx-trigger="load">Загрузка...</div>

<!-- Модификаторы события: -->
<!-- delay:500ms — подождать паузу (сбрасывается при новом событии) -->
<!-- throttle:1s — не чаще раза в секунду -->
<!-- changed    — только если значение реально изменилось -->
<input hx-get="/search"
       hx-trigger="keyup changed delay:500ms">

Передача данных

Формы отправляют свои поля сами. Дополнительные значения добавляют через hx-vals, а чужие поля подмешивают через hx-include.

<!-- Форма: все её поля уходят с запросом автоматически -->
<form hx-post="/login" hx-target="#result">
  <input name="email">
  <input name="password" type="password">
  <button>Войти</button>
</form>

<!-- hx-vals: добавить статичные значения (JSON) к запросу -->
<button hx-post="/save"
        hx-vals='{"source": "button", "lang": "ru"}'>Сохранить</button>

<!-- hx-include: подмешать значения полей вне элемента -->
<input id="q" name="query">
<button hx-get="/search" hx-include="#q">Искать</button>

Индикаторы загрузки: hx-indicator

Пока запрос в полёте, htmx навешивает класс на индикатор. Через hx-indicator можно показать спиннер.

<!-- На время запроса элемент с классом htmx-indicator -->
<!-- становится видимым; по завершении снова скрывается. -->
<button hx-get="/slow" hx-indicator="#spin">
  Загрузить
</button>
<img id="spin" class="htmx-indicator" src="/spinner.gif">

<!-- htmx сам управляет классами: -->
<!-- htmx-request    — есть на элементе во время запроса -->
<!-- htmx-indicator  — базовая невидимость индикатора -->
<!-- Стили этих классов вы пишете сами в CSS. -->

История и URL: hx-push-url

hx-push-url кладёт новый адрес в историю браузера, чтобы кнопка «Назад» работала, а ссылку можно было сохранить.

<!-- true: занести URL запроса в адресную строку и историю -->
<a hx-get="/page/2"
   hx-target="#content"
   hx-push-url="true">Страница 2</a>

<!-- Можно явно указать, какой адрес показать пользователю -->
<a hx-get="/api/page/2"
   hx-target="#content"
   hx-push-url="/page/2">Страница 2</a>

<!-- Кнопка «Назад» вернёт предыдущий фрагмент из истории. -->

Подтверждение: hx-confirm

Для опасных действий hx-confirm показывает нативное окно подтверждения перед запросом.

<!-- Перед DELETE покажется window.confirm с этим текстом. -->
<!-- Если пользователь отменит — запрос не уйдёт. -->
<button hx-delete="/account"
        hx-confirm="Точно удалить аккаунт? Это необратимо.">
  Удалить аккаунт
</button>

Бустинг ссылок и форм: hx-boost

hx-boost превращает обычные ссылки и формы внутри элемента в AJAX-запросы — прогрессивное улучшение без переписывания разметки.

<!-- Все <a> и <form> внутри будут грузиться через AJAX, -->
<!-- а body страницы-ответа подставится в текущий body. -->
<!-- Без JS (или без htmx) ссылки работают как обычно. -->
<nav hx-boost="true">
  <a href="/about">О нас</a>
  <a href="/blog">Блог</a>
</nav>

<!-- Отключить буст для отдельной ссылки -->
<a href="/file.pdf" hx-boost="false">Скачать PDF</a>

События и hx-on

htmx во время своего жизненного цикла шлёт DOM-события. На них можно вешать обработчики через hx-on или обычный addEventListener.

<!-- hx-on:: реагирует на htmx-события прямо в разметке -->
<!-- Двоеточия в имени события заменяются дефисами. -->
<button hx-get="/data"
        hx-on::before-request="this.disabled = true"
        hx-on::after-request="this.disabled = false">
  Запросить
</button>

<!-- Ключевые события htmx (всплывают на document): -->
<!-- htmx:beforeRequest — перед отправкой -->
<!-- htmx:afterRequest  — после ответа -->
<!-- htmx:afterSwap     — после вставки HTML в DOM -->
<!-- htmx:responseError  — сервер ответил ошибкой (4xx/5xx) -->

Серверный ответ: фрагменты HTML, а не JSON

Ключевая концепция: сервер отдаёт готовую разметку. Это меняет роль бэкенда — он рендерит куски страницы, а не сериализует данные.

<!-- Клиент шлёт запрос... -->
<button hx-get="/users/7" hx-target="#card">Показать</button>
<div id="card"></div>

<!-- ...а сервер отвечает НЕ так: -->
<!-- { "name": "Аня", "role": "admin" }  <- это JSON, не для htmx -->

<!-- ...а ВОТ так — готовым фрагментом HTML: -->
<!-- (этот ответ htmx просто вставит в #card) -->
<article>
  <h3>Аня</h3>
  <p>Роль: администратор</p>
</article>

<!-- Вёрстку делает сервер любым шаблонизатором: -->
<!-- Jinja, Blade, ERB, Django-templates, Go html/template. -->

Типичный пример: живой поиск

Соберём всё вместе. Поле ищет по мере набора, шлёт запрос с паузой и показывает индикатор — и всё это без единой строчки JavaScript.

<!-- Живой поиск: -->
<!-- 1) keyup changed delay:500ms — ждём паузу в наборе -->
<!-- 2) hx-post шлёт значение поля name="q" на сервер -->
<!-- 3) ответ (список результатов) кладём в #results -->
<!-- 4) на время запроса показываем спиннер -->
<input type="search"
       name="q"
       placeholder="Поиск..."
       hx-post="/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#results"
       hx-indicator="#spin">

<span id="spin" class="htmx-indicator">Ищем...</span>
<div id="results"><!-- сюда сервер кладёт HTML с результатами --></div>

<!-- Бесконечная прокрутка строится похоже: последний элемент -->
<!-- списка делает hx-get с hx-trigger="revealed" и -->
<!-- hx-swap="beforeend", дописывая следующую порцию. -->
Поддержать проект