Переход расширений на htmx 4
Как перенести пользовательское расширение htmx 2.x на API хуков версии 4.0.
Если вы поддерживаете пользовательское расширение, написанное для htmx 2.x, учтите, что API расширений существенно изменился.
От обратных вызовов к хукам
В htmx 4 API расширений, основанный на обратных вызовах, заменён хуками на основе событий.
Вместо реализации методов обратного вызова расширения регистрируют обработчики событий жизненного цикла.
Самый простой способ переноса: переименовать defineExtension в registerExtension и сопоставить обратные вызовы с хуками.
// htmx 2.x
htmx.defineExtension('my-ext', {
onEvent: function(name, evt) {
if (name === 'htmx:beforeRequest') { /* ... */ }
}
});
// htmx 4
htmx.registerExtension('my-ext', {
htmx_before_request: (elt, detail) => { /* ... */ }
}); Что изменилось
Атрибут hx-ext больше не нужен
Чтобы загрузить расширения, достаточно подключить скрипт. Атрибут не требуется:
<script src="/path/to/htmx.js"></script> <script src="/path/to/ext/my-extension.js"></script>
Ограничение списка загружаемых расширений:
<meta name="htmx-config" content='{"extensions": "sse, ws"}'> Хуки событий заменяют обратные вызовы
Вместо единственного обратного вызова onEvent, который обрабатывает события в зависимости от их имён, для каждого события используется отдельный метод-хук. В названиях хуков двоеточия в названиях событий заменяются подчёркиваниями:
| Событие htmx 2.x | Хук htmx 4 |
|---|---|
htmx:configRequest |
htmx_config_request |
htmx:beforeRequest |
htmx_before_request |
htmx:afterRequest |
htmx_after_request |
htmx:beforeSwap |
htmx_before_swap |
htmx:afterSwap |
htmx_after_swap |
Все хуки получают (elt, detail). Чтобы отменить действие, верните false.
handle_swap — особый случай
В отличие от других хуков, handle_swap вызывается напрямую с позиционными параметрами (без префикса htmx_ и объекта detail):
handle_swap: (swapStyle, target, fragment, swapSpec) => {
if (swapStyle === 'my-swap') {
target.appendChild(fragment);
return true;
}
return false;
} Объект detail заменяет свойства события
Все хуки получают detail.ctx с полным контекстом запроса и ответа:
-
detail.ctx.request.body(FormData вhtmx_config_request) -
detail.ctx.request.headers(обычный изменяемый объект) detail.ctx.response.status-
detail.ctx.text(тело ответа; можно изменить вhtmx_after_request) detail.ctx.target
Удаление обёртки при OOB-замене
При OOB-замене для стилей замены, отличных от outer, элемент-обёртка удаляется автоматически. Чтобы сохранить обёртку, задавайте имена пользовательских стилей замены с префиксом “outer” (например, outerMorph).
Сопоставление обратных вызовов при переносе
init
// htmx 2.x
init: function(api) {
return null;
}
// htmx 4
init: (internalAPI) => {
api = internalAPI;
} Сохраните ссылку internalAPI, чтобы использовать её в других хуках. Возвращать значение не нужно.
getSelectors
Удалён. Для проверки атрибутов используйте htmx_after_init:
// htmx 2.x
getSelectors: function() {
return ['[my-custom-attr]'];
},
onEvent: function(name, evt) {
if (name === 'htmx:afterProcessNode') {
initializeCustomBehavior(evt.target);
}
}
// htmx 4
htmx_after_init: (elt) => {
if (api.attributeValue(elt, 'my-custom-attr')) {
initializeCustomBehavior(elt);
}
} onEvent
Замените отдельными хуками:
// htmx 2.x
onEvent: function(name, evt) {
if (name === 'htmx:beforeSwap' && evt.detail.xhr.status !== 200) {
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
if (target) {
evt.detail.shouldSwap = true;
evt.detail.target = target;
}
}
}
// htmx 4
htmx_before_swap: (elt, detail) => {
if (detail.ctx.response.status !== 200) {
var target = getRespCodeTarget(elt, detail.ctx.response.status);
if (target) {
detail.ctx.target = target;
}
}
} transformResponse
Удалён. Изменяйте detail.ctx.text в htmx_after_request:
// htmx 2.x
transformResponse: function(text, xhr, elt) {
var tpl = htmx.closest(elt, '[mustache-template]');
if (tpl) {
var data = JSON.parse(text);
var template = htmx.find('#' + tpl.getAttribute('mustache-template'));
return Mustache.render(template.innerHTML, data);
}
return text;
}
// htmx 4
htmx_after_request: (elt, detail) => {
var tpl = elt.closest('[mustache-template]');
if (tpl) {
var data = JSON.parse(detail.ctx.text);
var template = document.querySelector('#' + tpl.getAttribute('mustache-template'));
detail.ctx.text = Mustache.render(template.innerHTML, data);
}
} Порядок обработки события: получен ответ, установлено ctx.text, срабатывает htmx:after:request, ctx.text преобразуется во фрагмент, htmx:before:swap.
encodeParameters
Удалён. Изменяйте итоговое значение detail.ctx.request.body в htmx_before_request:
// htmx 2.x
onEvent: function(name, evt) {
if (name === 'htmx:configRequest') {
evt.detail.headers['Content-Type'] = 'application/json';
}
},
encodeParameters: function(xhr, parameters, elt) {
var object = {};
parameters.forEach(function(value, key) {
if (Object.hasOwn(object, key)) {
if (!Array.isArray(object[key])) object[key] = [object[key]];
object[key].push(value);
} else {
object[key] = value;
}
});
return JSON.stringify(object);
}
// htmx 4
htmx_before_request: (elt, detail) => {
detail.ctx.request.headers['Content-Type'] = 'application/json';
var object = {};
detail.ctx.request.body.forEach(function(value, key) {
if (Object.hasOwn(object, key)) {
if (!Array.isArray(object[key])) object[key] = [object[key]];
object[key].push(value);
} else {
object[key] = value;
}
});
detail.ctx.request.body = JSON.stringify(object);
} ctx.request.body — это FormData в htmx_config_request. После этого хука значения GET и DELETE перемещаются в URL, тела запросов, отличные от multipart, преобразуются в URLSearchParams, а multipart-тела остаются FormData. В htmx_before_request замените итоговое тело на любой допустимый BodyInit.
isInlineSwap и handleSwap
Оба заменены на handle_swap:
// htmx 2.x
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';
},
handleSwap: function(swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
morphdom(target, fragment.firstElementChild || fragment.firstChild);
return [target];
}
}
// htmx 4
handle_swap: (swapStyle, target, fragment) => {
if (swapStyle === 'morphdom') {
morphdom(target, fragment.firstElementChild || fragment.firstChild);
return true;
}
return false;
} Если обработка выполнена, верните истинное значение, иначе — ложное. Для отслеживания завершения обработки можно вернуть массив элементов.
Удалённые обратные вызовы
| Обратный вызов htmx 2.x | Замена в htmx 4 |
|---|---|
getSelectors() |
хук htmx_after_init |
onEvent(name, evt) |
Отдельные хуки htmx_* |
transformResponse(text, xhr, elt) |
хук htmx_after_request (изменение detail.ctx.text) |
encodeParameters(xhr, params, elt) |
хук htmx_before_request (изменение итогового detail.ctx.request.body) |
isInlineSwap(swapStyle) |
handle_swap или имя стиля замены с префиксом “outer” |
handleSwap(style, target, frag, info) |
handle_swap(style, target, frag, spec) |
Licensed under the Zero-Clause BSD License.
https://four.htmx.org/docs/extension-htmx-4-migration-guide/