Spec-Zone.ru › htmx

Переход расширений на 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/

Spec-Zone.ru

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