Работа с AJAX
Albireo CMS предлагает сразу несколько готовых решений при работе с AJAX-запросами.
Важно! Albireo CMS использует AlpineJS как основную js-библиотеку, поэтому примеры ниже требуют хотя бы начального знания основ AlpineJS.
Что такое AJAX
С помощью AJAX можно управлять html-элементами через post-запросы к серверу без перезагрузки страниц. Наиболее распространено использование AJAX совместно с формами. Когда форма отправлена, то страница без перезагрузки отобразит результат отправки.
Структура AJAX
Сам по себе AJAX — это обычный post-запрос к серверу. Но вместо отправки через submit-формы, используется http-запрос через fetch(). Таким образом использование AJAX подразумевает, что
- есть html, где хранятся данные,
- есть url, по которому форма отправляет данные,
- есть php-обработчик, который принимает данные и отдаёт результат.
Есть несколько разных вариантов организации AJAX в Albireo CMS. Ниже приведены примеры для каждого из них.
Простой POST («классика»)
Вначале рассмотрим обычный post-запрос, который реализуется в Albireo CMS.
Самый простой вариант, это использование двух страниц, одна из них будет содержать форму, а другая выступать в роли обработчика POST-отправки.
Первая страница содержит форму — это произвольная страница, которая содержит форму (произвольную), например:
<form method="POST">
<div class="mar10-b">Email</div>
<input class="w100" type="email" name="email" value="">
<div class="mar10-b mar20-t">Password</div>
<input class="w100" type="password" name="password" value="">
<div class="mar10-b mar20-t">Info</div>
<input class="w100" type="text" name="info" value="">
<div><button class="mar20-tb" type="submit">Submit</button></div>
</form>
У формы обычно есть атрибут action, где указывается адрес, на который будет отправлен запрос. Если action не указан, то запрос будет отправлен на эту же страницу. Например адрес страницы с формой «сайт/form», тогда на этот же адрес придёт post-запрос с данными формы.
Поэтому в Albireo CMS достаточно сделать новую страницу (например form_handler.php) с полями:
slug: form method: POST layout: empty.php
Таким образом Albireo CMS автоматически подключит эту страницу как только будет адрес «сайт/form», но для post-запроса. В данном примере уже сама страница будет обработчиком входящего запроса.
Этот механизм — фактически стандартный для html-форм. В отличие от AJAX, страница будет перезагружена.
Простой AJAX («классика»)
Для того, чтобы сделать «чистый» AJAX в Albireo CMS, можно модернизировать форму, добавив в неё скрытое поле _method:
<form method="POST" onSubmit="...">
<input type="hidden" name="_method" value="AJAX">
... дальше форма
Поле _method указывает, что вместо «post», будет применяться «ajax». Поэтому в странице-обработчике нужно указывать поля так:
slug: form method: AJAX layout: empty.php
Отправка формы происходит любым js-методом, например через fetch().
Это «классический» вариант AJAX, который может быть реализован на чистом JS.
Единая точка входа для AJAX
Очевидно, что две страницы — это хоть и простой подход, но не очень удобный. Как правило обработчик AJAX-запроса (также как и POST) пишется на чистом PHP и в ответ выдаёт либо готовый HTML/TEXT, либо JSON данные.
Второе неудобство это необходим контроль за адресами страницы-обработчика. Если нужно сделать несколько форм, то для каждой нужно делать отдельные обработчики в виде страниц.
Поэтому в Albireo CMS можно использовать единую точку входа для всех AJAX-запросов. В этом случае остается только решить где именно разместить обработчик AJAX. Он может быть в трех вариантах:
- это может быть обычный php-файл (не страница!),
- это может быть отдельная функция,
- это может быть класс и его метод.
AJAX с php-файлом (для форм)
Albireo CMS использует AlpineJS как основную библиотеку для работы с JavaScript. В комплекте системы уже есть модуль для работы с формами через AlpineJS. Он автоматически подключается для каждой страницы, поэтому вам не нужно об этом заботится.
В простом виде можно организовать отправку произвольной формы так:
<div x-data="{...albireoForm(), agreement: true}">
<p x-show="!result || hasErrors">Заполните поля и отправьте форму.</p>
<form method="post" x-show="!result || hasErrors" x-ref="form" @submit.prevent="submitForm('<?= REQUEST_AJAX ?>')">
<?= METHOD_AJAX ?>
<?= CSRF ?>
<?= formHandler(__DIR__ . '/form_handler.php') >
<input type="text" name="form[name]" placeholder="your name..." required>
<input type="email" name="form[email]" placeholder="your email...">
<textarea name="form[message]" required></textarea>
<button type="submit" :disabled="!agreement">Submit</button>
<label><input type="checkbox" x-model="agreement"> I agree</label>
</form>
<div x-ref="result"></div>
</div>
Здесь albireoForm() «магический» компонент AlpineJS, внутри него уже есть переменные result и hasErrors, а также метод submitForm(), который принимает единый адрес AJAX. Для этого служит php-константа REQUEST_AJAX.
Для корректной работы с единым AJAX-адресом, необходимо отправлять защитные токены и скрытое поле _method. Чтобы не вводить это вручную используются уже готовые php-константы METHOD_AJAX и CSRF.
Для того, чтобы система, принимая AJAX-запрос, понимала какой нужен обработчик (handler), используется функция formHandler(). В данном примере это обычный php-файл form_handler.php в том же каталоге, где и текущая страница.
Обратите внимание, что form_handler.php — это обычный php-файл, а не pages-страница. В нём размещается итоговый html-код, который будет выведен в блоке <div x-ref="result"></div>.
Код обработчика может быть произвольным, примерно такого вида:
if (isset($_POST['form'])) {
// работаем с данными
// например отладочный вывод данных
pr($_POST);
}
Для корректной работы, также необходимо указывать имя формы x-ref="form". После отправки форма скрывается, за это отвечают инструкции x-show="!result || hasErrors".
Такой вариант отлично подходит для отправки любых форм, поскольку submitForm() берёт на себя обслуживание не только отправки запроса, но и обработку ответа. Это особенно актуально при передаче html вместе с js-кодом.
В submitForm() также реализован небольшой «хак», позволяющий управлять состоянием переменной hasErrors. Для этого в тексте ответа необходимо в произвольном месте вставить фразу error-form, например:
...
// $rules — это правила валидации для arrayValidate()
$result = arrayValidate($_POST['form'], $rules);
if ($result['errors']) {
// нужно указать фразу error-form, которая указывает, что этот текст сообщение об ошибках
echo arrayToStrHTML($result['errors'], '<ul error-form class="t-red100 bg-red600">', '</ul>', '<li>', '</li>');
} else {
... нет ошибок ...
}
Когда встретится error-form, то переменная hasErrors будет равна true, что позволяет использовать её для разных случаем обработки ошибок. Например, если есть ошибка в заполнении формы, то hasErrors позволит не скрывать форму для повторной отправки.
Обработчик в виде функции
В некоторых случаях, может быть удобней использовать не отдельный php-файл, а отдельную php-функцию. В Albireo CMS для размещения своих автоматически подключаемых php-файлов следует использовать каталог service/functions. Например мы можем разместить в нём myform_handler.php. Вот тестовый пример:
function myform_handler()
{
// тестовый код для отладки JSON и TEXT
$type = $_POST['ajax_return_type'] ?? 'text';
if ($type == 'json') {
$response = [
'received_at' => date('H:i:s'),
'post_data' => $_POST,
];
header('Content-Type: application/json');
echo json_encode($response);
} elseif ($type == 'text') {
pr('date('H:i:s'), $_POST);
}
}
После этого в форме укажем функцию в форме:
...
<?= formHandler('myform_handler') ?>
...
Albireo CMS сама поймёт, что это именно php-функция, поскольку у неё нет окончания .php.
ВАЖНО! Обратите внимание, что имя функции, так же как и имя файла для AJAX-обработчика должен завершаться на _handler. Если его не будет, то система отклонит такой запрос.Функцию-обработчик не нужно завершать по exit(), поскольку Albireo CMS сделает это самостоятельно.Обработчик в виде php-класса
Для своих php-классов лучше использовать каталог service/psr4. Albireo CMS поддерживает автоматическое подключение файлов по стандарту PSR4, поэтому вам не нужно заботится о дополнительных действиях.
Например файл service/psr4/Demo/Ajax_demo.php:
namespace Demo;
class Ajax_demo
{
public function my_handler()
{
$type = $_POST['ajax_return_type'] ?? 'text';
if ($type === 'json') {
$response = [
'received_at' => date('H:i:s'),
'post_data' => $_POST,
];
header('Content-Type: application/json');
echo json_encode($response);
} elseif ($type === 'text') {
pr(date('H:i:s'), $_POST);
}
}
}
Соответственно, в форме пишем:
...
<?= formHandler('\Demo\Ajax_demo@my_handler') ?>
...
Здесь символ @ является разделителем класс/метод, а также служит указателем, что обработчиком будет именно класс/метод. Метод также должен заканчиваться на «_handler» из соображений безопасности.
Метод в классе должен быть публичным (public).
Отправка AJAX вне форм
Если нужно отправить данные без использования формы, то применяется немного другой подход. Поскольку AJAX требует отправки токенов, то вместо формы используется специально сформированный AJAX-адрес. Для этого применяется php-функция ajaxURL().
Для отправки произвольных данных через JS используются функции $sendDataHtml и $sendDataJson(), которые являются «магическими» для AlpineJS. Функция $sendDataHtml возвращает ответ в виде HTML/Text, а $sendDataJson() в виде JSON.
<div x-data="{ result: ''}" >
<button @click="result = await $sendDataHtml('<?= ajaxURL('ajaxTest_handler') ?>', {name: 'Admin'})">Отправить</button>
<div x-html="result"></div>
</div>
Функция sendDataHtml() первым аргументом принимает адрес, вторым — произвольные данные, которые будут доступны в обработчике как обычный $_POST.
Ответ сервера будет примерно таким:
15:18:21 TRUE Array ( [name] => Admin [ajax_return_type] => html )
Для получения ответа в виде JSON можно использовать такой подход:
<div x-data="{ result: ''}" >
<button @click="result = await $sendDataJson('<?= ajaxURL('ajaxTest_handler') ?>', {name: 'Admin'})">Отправить</button>
<template x-if="result">
<pre x-html="JSON.stringify(result, null, 2)"></pre>
</template>
</div>
</div>
Ответ сервера будет примерно таким:
{
"received_at": "15:17:05",
"success": true,
"post": {
"name": "Admin",
"ajax_return_type": "json"
}
}
Обратите внимание, что используется обработчик ajaxTest_handler (см. ниже), который входит в комплект Albireo CMS и служит для тестирования AJAX-запросов.
Задание аргументов для функции-обработчика
Если в качестве AJAX-обработчика выступает php-функция, то есть возможность сразу передать в неё произвольные аргументы (до 10 штук) из post-запроса.
Простой пример, демонстрирующий эту возможность:
<div x-data="{result: '' }">
<button @click="result = await $sendDataHtml('<?= ajaxURL('ajax_demo_args_handler') ?>', { foo: 'bar', 'type': 'text', '_handler_func_arg1': 'my1', '_handler_func_arg2': 'my2', '_handler_func_arg3': 'my3' })">
Проверить text
</button>
<template x-if="result">
<div x-html="result"></div>
</template>
</div>
Обратите внимание на ключи _handler_func_argX (где X от 1 до 10). Такие данные будут автоматически обработаны Albireo CMS и сразу переданы в исполняемую php-функцию:
function ajax_demo_args_handler($a1, $a2, $a3)
{
pr($a1, $a2, $a3);
}
В этом примере будет выведено:
my1 my2 my3
Тоже самое будет работать и с обычной формой:
<form ... >
...
<?= formHandler('ajax_demo_args_handler') ?>
...
<!-- поля произвольного типа -->
<input type="text" name="_handler_func_arg1" value="my1">
<input type="text" name="_handler_func_arg2" value="my2">
<input type="text" name="_handler_func_arg3" value="my3">
...
Ключи _handler_func_argX должны указываться последовательно, начиная с 1 (_handler_func_arg1). Если пропустить следующий номер (например, если указаны _handler_func_arg1 и _handler_func_arg3), то система оставит только аргументы до пропущенного номера (будет передано только _handler_func_arg1, поскольку пропущен _handler_func_arg2). В самой форме _handler_func_argX могут указываться в произвольном порядке, главное, чтобы они попали в post-запрос.
Отправка формы через ajaxURL()
Форму также можно отправить через ajaxURL(), а не formHandler(). В этом случае нет необходимости указывать служебные hidden-поля формы. В остальном подход тот же самый.
<div x-data="{...albireoForm()}">
<form method="post" x-ref="form" @submit.prevent="submitForm('<?= ajaxURL('form_demo2_handler') ?>')">
<div class="">
<label class="flex flex-vcenter flex-wrap">
<div class="w20 w100-tablet">Your name</div>
<input class="w80 w100-tablet form-input" type="text" name="form[name]" value="<?= htmlspecialchars(sessionOld('form-name')) ?>" placeholder="your name...">
</label>
</div>
<div class="mar20-t">
<label class="flex flex-vcenter flex-wrap">
<div class="w20 w100-tablet">Your email</div>
<div class="w80 w100-tablet">
<input class="w100 form-input" type="email" name="form[email]" value="<?= htmlspecialchars(sessionOld('form-email')) ?>" placeholder="your email...">
</div>
</label>
</div>
<div class="mar20-t">
<div class="flex flex-wrap">
<div class="w20 w100-tablet"></div>
<div class="w80 w100-tablet">
<button class="button w100-tablet mar10-b" type="submit"><i class="bi-check"></i>Submit form</button>
</div>
</div>
</div>
</form>
<div x-ref="result"></div>
</div>
function form_demo2_handler() {
pr($_POST);
}
Функция sessionOld() сохраняет предыдущее состояние в php-сессии.
Демо-примеры в комплекте Albireo CMS
В качестве примеров, обратитесь к каталогу pages/specific, где размещены не только рабочие файлы, но и несколько демо-примеров по работе с AJAX.
Валидация данных
В php-обработчике желательно проверять принимаемые данные. Для этого можно использовать функцию arrayValidate().
Где лучше разместить обработчик?
Если у вас одиночная форма, то проще всего использовать обработчик в виде php-файла, который будет располагаться рядом с файлом страницы. Таким образом, если страница поменяет адрес и расположение, то достаточно будет переместить и файл обработчика, без его изменений.
Если у вас несколько форм, можно разместить обработчики в виде php-функций в service/functions. Таким образом можно отделить исходный html-код формы от его php-обработчика.
Если вы используете AJAX не для форм, то удобней разместить обработчики также в виде php-функций. Но, если у вас несколько html-компонентов, то удобней использовать php-класс с разными методами.
Демо-пример «Счетчик на сервере»
<div x-data="{ count: 10 }">
<p>Текущее значение: <strong x-text="count"></strong></p>
<button @click="
let res = await $sendDataJson('<?= ajaxURL('counter_handler') ?>', { current: count });
if (res) count = res.new_count;
">
Увеличить на сервере
</button>
</div>
function counter_handler()
{
$current = (int)($_POST['current'] ?? 0);
header('Content-Type: application/json');
echo json_encode(['new_count' => $current + 1]);
}
Демо-пример «Lazy Loading с задержкой»
<div x-data="{ result: '', loading: true }"
x-init="setTimeout(async () => {
result = await $sendDataHtml('<?= ajaxURL('lazy_load_handler') ?>');
loading = false;
}, 500)">
<div x-show="loading" class="t-gray500 t90 animation-bounce">Загрузка данных через полсекунды...</div>
<template x-if="result">
<div x-html="result" class="animation-fade"></div>
</template>
</div>
function lazy_load_handler() {
echo '<ul>
<li>Новость 1: Albireo CMS обновилась</li>
<li>Новость 2: AJAX стал еще проще</li>
</ul>';
}
Если вы используете AJAX-запрос сразу после того, как загрузился HTML (как в этом примере), то делайте это с небольшой задержкой, иначе Albireo CMS выдаст ошибку «No timeout. Please refresh the page (F5)!». Так работает защита от автоматических роботов.
Демо-пример «Live Search»
<div x-data="{ result: '', query: '' }">
<input type="text"
x-model="query"
@input.debounce.500ms="result = await $sendDataHtml('<?= ajaxURL('search_handler') ?>', { s: query })"
placeholder="Начните искать...">
<div class="mar10-tb bordered pad10" x-show="result">
<div x-html="result"></div>
</div>
</div>
function search_handler() {
$s = $_POST['s'] ?? '';
if (empty($s)) exit('Введите запрос...');
// Имитация поиска по базе или файлам
$data = ['Apple', 'Banana', 'Orange', 'Cherry'];
$filtered = array_filter($data, fn($item) => str_contains(strtolower($item), strtolower($s)));
if ($filtered) {
foreach ($filtered as $item) echo "<div>Found: $item</div>";
} else {
echo "Ничего не найдено";
}
}
«Событийная модель» AJAX
Часто стоит задача получить ответ от сервера в виде множества данных. Например если форма не прошла валидацию, то нужно вывести какое-то особое сообщение. Для удобства следует использовать дополнительный http-заголовок «X-Result», который будет содержать имя события и его детализацию.
Событие — это custom events в JS, которые обрабатываются через «магический» объект $event в AlpineJS.
С помощью событий можно настроить разное поведение для разных ответов сервера.
Вы можете использовать встроенную в Albireo CMS функцию ajaxTest_handler для тестирования AJAX. Следующие примеры показывают основные возможности такого подхода.
<div x-data="{ result: '', message: '', success: false }"
@success="message = $event.detail.msg || 'Ок'"
@error="message = $event.detail.msg || 'Error'">
<div>
<input type="checkbox" x-model="success">
<button @click="result = await $sendDataHtml('<?= ajaxURL('ajaxTest_handler') ?>', {name: 'Admin', 'success': success})">Отправить</button>
<div x-html="result"></div>
</div>
<div x-html="message"></div>
</div>
<div x-data="{ result: '', message: '', success: false }"
@success="message = $event.detail.msg || 'Ок'"
@error="message = $event.detail.msg || 'Error'">
<div>
<input type="checkbox" x-model="success">
<button @click="result = await $sendDataJson('<?= ajaxURL('ajaxTest_handler') ?>', {name: 'Admin', 'success': success})">Отправить</button>
<template x-if="result">
<pre x-html="JSON.stringify(result, null, 2)"></pre>
</template>
</div>
<div x-html="message"></div>
</div>
Обратите внимание, что если на странице используется парсер TextSimple, то он может исказить данные примеры. Поэтому при тестировании лучше отключить парсер или обрамить код в nosimple.Ответ сервера формируется примерно так:
header('X-Result: ' . json_encode([
// обязательное поле event - к нему цепляется событие после отправки
'event' => $success ? 'success' : 'error',
'msg' => $success ? 'Ок!' : 'Error!'
]));
...
Поле event в json-ответе указывает на имя события. Для AlpineJS это выглядит так:
<div x-data="{ result: '', message: '', success: false }"
@success="message = $event.detail.msg || 'Ок'"
@error="message = $event.detail.msg || 'Error'">
...
Где @success — это событие, которое получено из поля X-Result. Соответственно, если в ответе event будет равно error, то сработает событие @error.
Если вX-Resultне передавать ключevent, то по умолчанию оно будет равно событию@ajax_success.
В ajaxTest_handler используется post-поле success, которое для тестирования применяет «переключатель ошибки/удачи» формы отправки в виде чекбокса формы. В реальной задаче можно немного упростить:
# handler-обработчик
... какая-то обработка post-данных, например через arrayValidate()
$result = arrayValidate($_POST['form'], $rules);
if ($result['errors']) {
// отправляем http-заголовок с ошибкой и сообщением
header('X-Result: ' . json_encode([
// событие @error
'event' => 'error',
// сообщение в этом заголовке будет содержать готовый html-код ошибок
'msg' => json_encode(arrayToStrHTML($result['errors'], '<ul class="t-red100 bg-red600">', '</ul>', '<li>', '</li>')),
]));
echo 'можно вывести ещё что-то';
} else {
... нет ошибок ...
header('X-Result: ' . json_encode([
'event' => 'success',
'msg' => 'Сообщение отправлено!',
]));
echo 'можно вывести ещё что-то';
}
Тогда html-код для формы будет таким:
<div x-data="{result: '', message: '' }"
@success="message = $event.detail.msg || 'Ок';"
@error="message = $event.detail.msg || 'Error';" >
<form @submit.prevent="result = await $sendDataHtml('обработчик_handler', $el)">
... поля формы ...
<button type="submit">Отправить</button>
</form>
<div x-text="message"></div>
<div x-html="result"></div>
</div>
Если мы используем $sendDataHtml, то получение всей формы возможно в виде $el, который указывает на текущий элемент, а все данные формы будут автоматически преобразованы в $_POST.
Если же использовать $sendDataJson, то в handler-обработчике можно отправить не простой html/текст, а что-то более сложное в JSON.
...
//тело ответа после X-Result, которое отправляется через echo
header('Content-Type: application/json');
echo json_encode([
'received_at' => date('H:i:s'),
'redirect' => 'url-to-redirect',
'other_data' => 'любые другие данные',
]);
Тогда в html можно получить более сложный объект result:
<span x-text="result.received_at"></span> <span x-text="result.redirect"></span> ... и т.д.
Отправка через $sendDataHtml и $sendDataJson автоматически добавляет в $_POST поле ajax_return_type, который будет равен 'html' или 'json' и указывает в каком виде сервер должен вернуть ответ.
# handler-обработчик
$type = $_POST['ajax_return_type'] ?? 'json';
if ($type == 'json') {
... ответ в json
$response = [...];
header('Content-Type: application/json');
echo json_encode($response);
} else {
... ответ в html
$response = '...';
echo $response;
}
Такой подход к построению AJAX позволяет:
- получить более простой html-код форм и произвольных html-элементов за счет «магии» AlpineJS,
- передавать из обработчика не только основной текст ответа, но и произвольное событие,
- использовать как HTML, так и JSON ответ.
Лучше использовать именно «событийную модель» AJAX, даже если нет необходимости в получении X-Result: если в будущем он понадобится, то это потребует минимального изменения кода.
«Живая пагинация» для главной
На основе AJAX в Albireo CMS организован «живая пагинация», которая добавляет кнопку «Загрузить ещё» для главной страницы сайта. По нажатию на эту кнопку, происходит получение следующей страницы пагинации без перезагрузки самой страницы. Записи добавляются к уже существующим. При этом корректно работают ссылки пагинации, история и панель адреса браузера.
Для включения такой пагинации следует в config.php указать:
'homeOutputModule' => 'home/home-live1.php',
Файл home-live1.php размещается в каталоге шаблона modules/home и при необходимости может быть использован как (измененная) копия в website/service/my/modules/home.
В качестве handler-обработчика выступает функция ajaxLivePagination_handler(), которая размещена в website/service/function. При необходимости можно его поправить (сделав копию, чтобы не затирать при обновлении системы).
Вся логика и алгоритм работы реализован на «событийной модели» AJAX.
«Магический» метод livePaginationHandler для AlpineJS уже входит в состав Albireo CMS и вряд ли потребует изменений.Используйте эти файлы как пример для организации «живой пагинации» для других целей.