Skip to content

🚀 pbFetch: асинхронные запросы через HTML-атрибуты

pbFetch — это нативный JavaScript-класс для работы с AJAX-запросами, вдохновлённый подходом HTMX, но с собственным неймспейсом pb-. Реализует отправку GET, POST, PUT и DELETE-запросов без перезагрузки страницы и позволяет динамически обновлять контент через декларативные атрибуты.

🔗 Подключение

  1. Включите настройку pageblocks_load_scripts, чтобы скрипт подключился автоматически.
  2. Или подключите вручную:
html
<script src="/assets/components/pageblocks/js/web/pb.fetch.v300.js"></script>
<script>
  pbFetch.init();
</script>

init() принимает ключ контекста MODX, который уйдёт в заголовке X-CONTEXT-KEY:

js
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-targetCSS-селектор элемента, куда будет вставлен ответ.
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-valsJSON-строка с данными для тела запроса.
pb-includeCSS-селектор полей формы, которые нужно включить в запрос.
pb-indicatorCSS-селектор индикатора загрузки.
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 (по умолчанию)

Заменяет содержимое целевого элемента.

html
<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

Полностью заменяет сам элемент вместе с его тегом.

html
<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 перед элементом.

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 после элемента.

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 в конец содержимого элемента.

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 в начало содержимого элемента.

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-ответа

  1. data.redirect — если поле есть и не пустое, происходит переход и обработка обрывается.
  2. Атрибут pb-redirectreload перезагружает страницу, иначе переход по адресу. Обработка тоже обрывается.
  3. data.message — показывается через pbMessage.success().
  4. pb-swap для pb-target: особые значения active, inactive, delete, hide, а во всех остальных случаях — вставка data.html.

Весь этот блок пропускается, если у события pb:success вызвали preventDefault().

1. Динамические параметры в URL

html
<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.

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>
json
{
  "success": true,
  "message": "Города загружены",
  "html": "<option value='5'>Кишинёв</option>"
}

➡ В #city попадёт разметка из html, а message уйдёт в pbMessage. Так один ответ одновременно и обновляет блок, и сообщает результат — раньше для этого приходилось делать два запроса.

3. data.redirect — переход из ответа

json
{ "success": true, "redirect": "/profile" }

➡ Браузер уйдёт на /profile. Ни сообщение, ни подмена разметки уже не выполняются.

4. pb-swap="delete"

Удаляет элемент из DOM после успешного ответа.

html
<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 к элементу.

html
<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.

html
<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).

html
<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').

html
<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

Методы

js
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, если случился редирект или если запрос оборвался сетевой ошибкой.

Примеры

js
// Простой 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');

Поддерживаемые опции

ОпцияПо умолчаниюОписание
methodGETHTTP-метод (GET, POST, PUT, PATCH, DELETE).
url''Адрес запроса.
targetnullСелектор для вставки ответа.
swapinnerHTMLМетод вставки (innerHTML, beforeend, и т.п.).
expecthtmlФормат ответа: html или json.
bodynullТело запроса (URLSearchParams, FormData, строка JSON).
headers{}Кастомные заголовки.
formnullЭлемент формы (HTMLFormElement).
redirectnullКуда перейти после успешного JSON-ответа; reload — перезагрузка.
showProgresstrueГенерировать ли события pb:progress:start/end.
cancelKeyURL без query-строкиКлюч запроса для отмены.
cancelPrevioustrueОтменить предыдущий запрос с тем же ключом.
before(form) => {}Колбэк перед запросом. Может быть async — его дождутся.
success(response, data) => {}Колбэк при response.ok.
error(response, data) => {}Колбэк при HTTP-ошибке.
after(response, data) => {}Колбэк после завершения запроса (и при успехе, и при ошибке).

Как отменить отправку

Возвращённое из before значение не проверяется — вернуть false недостаточно. Отправку отменяет только preventDefault() на событии pb:before:

js
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

Пример

js
document.addEventListener('pb:success', (e) => {
  console.log('Успех:', e.detail);
});

document.addEventListener('pb:error', (e) => {
  console.log('Ошибка:', e.detail);
  e.preventDefault(); // отменить автоматический показ pbMessage
});

✅ Автоматический pbMessage

Сообщение показывается, когда сошлись три условия:

  1. expect: 'json';
  2. в ответе есть непустое data.message;
  3. событие pb:success (или pb:error) не отменено.

Контекст для поиска блока сообщения — форма элемента, а если её нет, то элемент из pb-target. Когда ни того ни другого нет, показывать сообщение некуда, и оно молча пропадает. Подробности — в pbMessage.

🏆 Преимущества

  • Чистый JS-код без зависимостей.
  • Простота — работает «из коробки».
  • Поддержка загрузки по load, множественных событий.
  • Контроль и гибкость через события и JS-интерфейс.
  • Поддержка форм, индикаторов и отмены запросов.

🟢 Заключение

pbFetch — лёгкий и мощный способ внедрения асинхронных запросов в PageBlocks. Всё на чистом HTML и JavaScript. Без зависимостей, с полной управляемостью и расширяемостью.

© PageBlocks 2019-present