🚀 pbFetch: асинхронные запросы через HTML-атрибуты
pbFetch — это нативный JavaScript-класс для работы с AJAX-запросами, вдохновлённый подходом HTMX, но с собственным неймспейсом pb-. Реализует отправку GET, POST, PUT и DELETE-запросов без перезагрузки страницы и позволяет динамически обновлять контент через декларативные атрибуты.
🔗 Подключение
- Включите настройку
pageblocks_load_scripts, чтобы скрипт подключился автоматически. - Или подключите вручную:
<script src="/assets/components/pageblocks/js/web/pb.fetch.v300.js"></script>
<script>
pbFetch.init();
</script>init() принимает ключ контекста MODX, который уйдёт в заголовке X-CONTEXT-KEY:
pbFetch.init('web');При автоматическом подключении ключ подставляется текущим контекстом страницы.
⚡️ Основные возможности
- Асинхронная загрузка HTML или JSON-ответов.
- Обновление контента через
pb-targetиpb-swap. - Автоматический показ сообщений через
pbMessage. - Подтверждение действия перед запросом (
pb-confirm). - Поддержка индикаторов загрузки.
- Обработка событий каждого этапа.
- Поддержка множественных событий в
pb-trigger. - Поддержка автоматической загрузки при
pb-trigger="load". - Отмена «устаревших» запросов через
AbortController.
🗂️ Базовые атрибуты
| Атрибут | Описание |
|---|---|
pb-get | Отправка GET-запроса. Значение атрибута — URL. |
pb-post | Отправка POST-запроса. |
pb-put | Отправка PUT-запроса. |
pb-delete | Отправка DELETE-запроса. |
pb-target | CSS-селектор элемента, куда будет вставлен ответ. |
pb-swap | Метод вставки: innerHTML, beforeend, afterbegin, beforebegin, afterend, outerHTML. Особые значения для JSON: active, inactive, delete, hide |
pb-expect | Формат ожидаемого ответа: html (по умолчанию) или json. |
pb-trigger | События, которые запускают запрос: click, submit, change, load (можно указать несколько через запятую). По умолчанию — click. |
pb-vals | JSON-строка с данными для тела запроса. |
pb-include | CSS-селектор полей формы, которые нужно включить в запрос. |
pb-indicator | CSS-селектор индикатора загрузки. |
pb-confirm | Текст подтверждения. Запрос уйдёт только после согласия пользователя. |
pb-redirect | Куда перейти после успешного JSON-ответа. Значение reload перезагружает страницу. |
PATCH только из JavaScript
HTML-атрибута pb-patch нет: делегированный обработчик ловит только [pb-get], [pb-post], [pb-put], [pb-delete]. PATCH доступен через JS-метод pbFetch.patch().
Плейсхолдеры в URL
В значении pb-get / pb-post / pb-put / pb-delete можно использовать подстановки — они берутся с самого элемента и экранируются через encodeURIComponent:
{value}→ текущее значение элемента (el.value){id}→ атрибутidэлемента{name}→ атрибутnameэлемента
Пустой URL — это, как правило, серверный route(), который не нашёл маршрута и вернул пустую строку. pbFetch в такой ситуации не отправляет запрос, а пишет в консоль pbFetch: empty url on element.
🔸 HTML (pb-expect="html")
Вставка происходит только если задан pb-target. Без него ответ просто вернётся из метода, а DOM не изменится.
1. innerHTML (по умолчанию)
Заменяет содержимое целевого элемента.
<div id="box">Старый контент</div>
<button pb-trigger="click"
pb-get="/get/new-content"
pb-target="#box"
pb-expect="html"
pb-swap="innerHTML">
Заменить контент
</button>➡ После запроса внутри #box появится новый HTML, старый будет удалён.
2. outerHTML
Полностью заменяет сам элемент вместе с его тегом.
<div id="box">Контент</div>
<button pb-trigger="click"
pb-get="/get/full-box"
pb-target="#box"
pb-expect="html"
pb-swap="outerHTML">
Заменить элемент
</button>➡ #box будет заменён целиком (вместе с <div>).
3. beforebegin
Вставляет HTML перед элементом.
<div id="item">Пункт</div>
<button pb-trigger="click"
pb-get="/get/row"
pb-target="#item"
pb-expect="html"
pb-swap="beforebegin">
Вставить перед элементом
</button>➡ Новый HTML будет вставлен перед #item, сам #item не изменится.
4. afterend
Вставляет HTML после элемента.
<div id="item">Пункт</div>
<button pb-trigger="click"
pb-get="/get/row"
pb-target="#item"
pb-expect="html"
pb-swap="afterend">
Вставить после элемента
</button>➡ Новый HTML будет вставлен сразу после #item.
5. beforeend
Добавляет HTML в конец содержимого элемента.
<ul id="list">
<li>Пункт 1</li>
</ul>
<button pb-trigger="click"
pb-get="/get/item"
pb-target="#list"
pb-expect="html"
pb-swap="beforeend">
Добавить элемент
</button>➡ Новый элемент добавится в конец списка (<li> после Пункт 1).
6. afterbegin
Добавляет HTML в начало содержимого элемента.
<ul id="list">
<li>Пункт 1</li>
</ul>
<button pb-trigger="click"
pb-get="/get/item"
pb-target="#list"
pb-expect="html"
pb-swap="afterbegin">
Добавить элемент
</button>➡ Новый элемент появится в начале списка, перед Пункт 1.
{ } JSON (pb-expect="json")
Если указано pb-expect="json", то:
- к запросу добавляются заголовки
Accept: application/jsonиX-Requested-With: XMLHttpRequest; - ответ разбирается через
response.json(); - дальше
pbFetchразбирает известные ему поля ответа — в строгом порядке.
Порядок обработки успешного JSON-ответа
data.redirect— если поле есть и не пустое, происходит переход и обработка обрывается.- Атрибут
pb-redirect—reloadперезагружает страницу, иначе переход по адресу. Обработка тоже обрывается. data.message— показывается черезpbMessage.success().pb-swapдляpb-target: особые значенияactive,inactive,delete,hide, а во всех остальных случаях — вставкаdata.html.
Весь этот блок пропускается, если у события pb:success вызвали preventDefault().
1. Динамические параметры в URL
<select id="country"
name="country"
class="form-select"
pb-get="/api/cities/{value}"
pb-trigger="change"
pb-expect="json"
pb-target="#city">
<option value="" selected disabled>Выберите страну</option>
</select>➡ При выборе страны со значением 2 отправится запрос /api/cities/2.
2. data.html — разметка внутри JSON-ответа
Если pb-swap не равен active / inactive / delete / hide, а в ответе есть непустая строка data.html, она вставляется в pb-target по обычным правилам вставки HTML.
<div id="city"></div>
<select name="country"
pb-get="/api/cities/{value}"
pb-trigger="change"
pb-expect="json"
pb-target="#city"
pb-swap="innerHTML">
<option value="2">Молдова</option>
</select>{
"success": true,
"message": "Города загружены",
"html": "<option value='5'>Кишинёв</option>"
}➡ В #city попадёт разметка из html, а message уйдёт в pbMessage. Так один ответ одновременно и обновляет блок, и сообщает результат — раньше для этого приходилось делать два запроса.
3. data.redirect — переход из ответа
{ "success": true, "redirect": "/profile" }➡ Браузер уйдёт на /profile. Ни сообщение, ни подмена разметки уже не выполняются.
4. pb-swap="delete"
Удаляет элемент из DOM после успешного ответа.
<button type="button"
pb-delete="/cargo/{id}"
pb-expect="json"
pb-target="#order-123"
pb-swap="delete"
pb-confirm="Удалить заказ?"
id="123"
class="btn-control">
Удалить заказ
</button>➡ Сначала показывается подтверждение, затем отправляется DELETE /cargo/123, при успехе — удаляется #order-123.
5. pb-swap="active"
Добавляет класс active к элементу.
<button type="button"
pb-post="/user/{id}/activate"
pb-expect="json"
pb-target="#user-123"
pb-swap="active"
id="123">
Активировать
</button>➡ После ответа JSON к #user-123 добавляется класс active.
6. pb-swap="inactive"
Удаляет класс active.
<button type="button"
pb-post="/user/{id}/deactivate"
pb-expect="json"
pb-target="#user-123"
pb-swap="inactive"
id="123">
Деактивировать
</button>➡ После ответа JSON у #user-123 убирается класс active.
7. pb-swap="hide"
Скрывает элемент (display: none).
<button type="button"
pb-post="/notifications/read/{id}"
pb-expect="json"
pb-target="#notif-55"
pb-swap="hide"
id="55">
Отметить прочитанным
</button>➡ После ответа JSON #notif-55 будет скрыт.
🧾 Что уходит на сервер
| Что | Когда |
|---|---|
Заголовок X-CONTEXT-KEY | Всегда. Значение — ключ контекста, переданный в init(). |
Заголовок X-CSRF-TOKEN | Для POST, PUT, PATCH, DELETE, если на странице есть <meta name="csrf-token">. |
Accept и X-Requested-With | При expect: 'json'. |
| Поля ближайшей формы | Для не-GET запросов элемент сам ищет closest('form'). |
Данные pb-vals и полей из pb-include | Всегда, если атрибуты заданы. |
При GET и HEAD форма не прикрепляется, а pb-vals, pb-include и поля формы, переданной вручную, дописываются в query-строку.
⏳ Индикатор загрузки
Элемент из pb-indicator показывается перед запросом (display: '') и скрывается после (display: 'none').
<span id="spinner" style="display:none">Загрузка…</span>
<button pb-get="/report"
pb-target="#report"
pb-indicator="#spinner">
Построить отчёт
</button>Если pb-indicator задан, события pb:progress:start / pb:progress:end не генерируются: индикация уже есть, глобальный прогресс-бар будет лишним.
⚙️ Использование в JavaScript
Методы
pbFetch.get(options)
pbFetch.post(options)
pbFetch.put(options)
pbFetch.patch(options)
pbFetch.delete(options)
pbFetch.ajax(options) // то же самое, метод задаётся опцией method
pbFetch.cancel(key) // отменить запрос по ключу; без аргумента — всеВсе они возвращают промис. В нём — разобранный ответ: строка при html, объект при json. Промис резолвится в undefined, если запрос отменили через pb:before, если случился редирект или если запрос оборвался сетевой ошибкой.
Примеры
// Простой GET-запрос
pbFetch.get({
url: '/news',
target: '#news-block',
swap: 'beforeend'
});
// POST-запрос с телом
pbFetch.post({
url: '/send',
body: new URLSearchParams({ name: 'John' }),
expect: 'json'
});
// PUT-запрос с JSON
pbFetch.put({
url: '/user/5',
body: JSON.stringify({ name: 'Alice' }),
headers: { 'Content-Type': 'application/json' },
expect: 'json'
});
// PATCH-запрос
pbFetch.patch({
url: '/profile',
body: new FormData(document.querySelector('#profile-form')),
expect: 'json',
success: (response, data) => console.log('Обновлено:', data),
after: (response, data) => console.log('Запрос завершён')
});
// DELETE-запрос
pbFetch.delete({
url: '/item/12',
expect: 'json',
success: (response, data) => alert(data.message),
error: (response, data) => alert('Ошибка удаления')
});
// Отправка формы
const form = document.querySelector('#form');
pbFetch.post({
url: form.action,
form,
expect: 'json'
});
// Отмена предыдущих запросов
pbFetch.get({
url: '/search?q=test',
cancelKey: '/search',
cancelPrevious: true
});
// Отменить вручную
pbFetch.cancel('/search');Поддерживаемые опции
| Опция | По умолчанию | Описание |
|---|---|---|
method | GET | HTTP-метод (GET, POST, PUT, PATCH, DELETE). |
url | '' | Адрес запроса. |
target | null | Селектор для вставки ответа. |
swap | innerHTML | Метод вставки (innerHTML, beforeend, и т.п.). |
expect | html | Формат ответа: html или json. |
body | null | Тело запроса (URLSearchParams, FormData, строка JSON). |
headers | {} | Кастомные заголовки. |
form | null | Элемент формы (HTMLFormElement). |
redirect | null | Куда перейти после успешного JSON-ответа; reload — перезагрузка. |
showProgress | true | Генерировать ли события pb:progress:start/end. |
cancelKey | URL без query-строки | Ключ запроса для отмены. |
cancelPrevious | true | Отменить предыдущий запрос с тем же ключом. |
before | (form) => {} | Колбэк перед запросом. Может быть async — его дождутся. |
success | (response, data) => {} | Колбэк при response.ok. |
error | (response, data) => {} | Колбэк при HTTP-ошибке. |
after | (response, data) => {} | Колбэк после завершения запроса (и при успехе, и при ошибке). |
Как отменить отправку
Возвращённое из before значение не проверяется — вернуть false недостаточно. Отправку отменяет только preventDefault() на событии pb:before:
document.addEventListener('pb:before', (e) => {
if (!window.confirm('Точно?')) e.preventDefault();
});Сетевая ошибка (сервер недоступен, запрос прерван) не доходит до error — она уходит в события pb:fail и pb:abort. Колбэк error вызывается только тогда, когда ответ пришёл, но с плохим статусом.
🗨️ События
Все события генерируются на document и являются cancelable.
| Событие | Когда срабатывает | detail |
|---|---|---|
pb:before | До отправки. preventDefault() отменяет запрос. | method, url, target, form |
pb:response | После получения ответа, до его обработки. | method, url, target, response, form |
pb:success | После успешного ответа. preventDefault() отключает обработку JSON: и pbMessage, и редирект, и подмену разметки. | method, url, target, response, data, form |
pb:error | После HTTP-ошибки. preventDefault() отменяет показ pbMessage. | method, url, target, response, data, form |
pb:progress:start | При старте загрузки (если showProgress). | url |
pb:progress:end | После завершения загрузки (если showProgress). | url |
pb:after | После завершения запроса (в любом случае). | method, url, target, response, data, form |
pb:abort | Если запрос отменён. | method, url, target |
pb:fail | При сетевой ошибке (например, нет связи с сервером). | method, url, target, error |
Пример
document.addEventListener('pb:success', (e) => {
console.log('Успех:', e.detail);
});
document.addEventListener('pb:error', (e) => {
console.log('Ошибка:', e.detail);
e.preventDefault(); // отменить автоматический показ pbMessage
});✅ Автоматический pbMessage
Сообщение показывается, когда сошлись три условия:
expect: 'json';- в ответе есть непустое
data.message; - событие
pb:success(илиpb:error) не отменено.
Контекст для поиска блока сообщения — форма элемента, а если её нет, то элемент из pb-target. Когда ни того ни другого нет, показывать сообщение некуда, и оно молча пропадает. Подробности — в pbMessage.
🏆 Преимущества
- Чистый JS-код без зависимостей.
- Простота — работает «из коробки».
- Поддержка загрузки по
load, множественных событий. - Контроль и гибкость через события и JS-интерфейс.
- Поддержка форм, индикаторов и отмены запросов.
🟢 Заключение
pbFetch — лёгкий и мощный способ внедрения асинхронных запросов в PageBlocks. Всё на чистом HTML и JavaScript. Без зависимостей, с полной управляемостью и расширяемостью.