Не секрет, что львиную долю функционала корпоративных (и не только) приложений представляют собой CRUD операции с простыми валидацией и бизнес-логикой. Для многих наборов данных, при работе с ними, удобно использовать табличное представление, для этого могут использоваться довольно мощные инструменты, такие как jqGrid, DataTables, Handsontable, или более простые вроде включённого в Yii2 виджета GridView. Он хорошо справляется с поставленной задачей управления небольшими наборами данных, не требующими сложных способов поиска и сортировки, соответствует принципам RAD, но имеет несколько недостатков и явные моменты напрашивающиеся на улучшение. Рассмотрим несколько таких проблем и способов их решения.
1. При любых действиях с данными, вызывающих изменение состояния таблицы, происходит полная перезагрузка всей страницы. Это решается способом из wiki с помощью Pjax, который интегрирован в Yii2. Достаточно обернуть GridView в
и процедуры удаления, фильтрации, сортировки, перехода по страницам будут происходить без полного обновления страницы, в сам GridView уже интегрирована поддержка Pjax в необходимом объёме.
Однако, часто в программировании приходится идти на компромисс между удобством (использования, поддержки, отладки, и т. д.) и сложностью, также и в данном случае, использовать Pjax просто и удобно, но при обновлении части страницы с его помощью сервер всё равно формирует полную версию страницы, из которой Pjax на стороне клиента вырезает только нужное. Но это легко исправить добавив в контроллер что-то вроде
2. Не будет работать лишь обновление без перезагрузки страницы после сохранения формы, в этом случае можно использовать упомянутый способ из wiki разместив форму на одной странице с GridView, и обернув её также а Pjax. Или воспользоваться модальными окнами, о последних речь дальше и пойдёт. Раз уж так получилось, что Yii2 тесно связан с Bootstrap, и он используется во многих проектах на этом фреймворке, то можно применить модальные окна из состава Bootstrap. Для этих целей можно использовать расширение yii2-bootstrap, а из него виджет Modal.
Добавляем в исходный код страницы виджет Modal, например, так:
При клике по кнопке добавления новой записи или редактирования отображаем форму в модальном окне:
Далее устанавливаем перехватчик события формы beforeSubmit (не забыв в конце вернуть false), в котором отправляем содержимое формы с помощью AJAX на сервер, если нас всё устраивает в полученном от сервера ответе, то скрываем форму и перезагружаем Pjax, если нет, то выводим возвращённые сервером данные в это же модальное окно, или выполняем какие-либо другие действия:
При описанном подходе, если id загруженной в модальное окно формы не будет в пределах страницы уникальным, то могут возникнуть проблемы с валидацией и вообще корректным функционирование формы. А такое вполне может произойти, т. к. автоматически устанавливаемые фреймворком id виджетов автоматически нумеруются по порядку, и при AJAX запросе к серверу эта нумерация начнётся с начала, с нуля. Для решения проблемы можно всегда устанавливать id формы вручную, или реализовать в перехватчике события onClick кнопки отправки формы что-то вроде
3. Аппетит приходит во время еды, и теперь ещё хотелось бы отправить пользователю некое сообщение (уже скрыв форму и закрыв модальное окно), файл, а может вопреки всем принципам AJAX всё же обновить страницу или сделать переход на другую. Отвязавшись от стандартной процедуры отправки формы в предыдущем пункте, это легко можно реализовать в обработчике success метода $.ajax, описанном выше.
Также можно задать пользователю какой-либо вопрос, запросить подтверждение действий, и в зависимости от ответа отправлять запрос на сервер или нет.
Всё это (и даже больше) было реализовано в виде расширения yii2-gridview-ajaxed-widget, в котором модифицированный виджет GridView (наследуется от GridView фреймворка) может быть напрямую использован, как замена стандартному. В данном модифицированном виджете появляются некоторые не обязательные параметры, связанные с дополнительным функционалом, например, addButtons, где можно указать одну или больше кнопок, открывающих форму добавления нового элемента, каждая может быть добавлена со своим route, также можно указать собственные route для встроенных кнопок update, view, delete, но чаще всего в этом нет необходимости, виджет сам определяет нужный route и имена ключевых полей модели и формирует нужные GET параметры. Также можно указать свои функции обратного вызова для обработки ошибок и вывода сообщения пользователю, и немного всякой разной другой кастомизации, необходимость в которой возникала в ходе работы над различными проектами.
Если сам виджет можно использовать без изменений, как стандартный, то в контроллер нужно будет внести некоторые изменения, например actions процедур обновления и удаления должны содержать примерно такой код:
Delete, может вообще ничего не возвращать, или возвращать что-то вроде «#alert Запись невозможно удалить!», тогда пользователю выведется соответствующее сообщение. Как видите create.php и update.php становятся вообще не нужны, код приложения упрощается, а возможности несколько расширяются.
Также появляется возможность использовать несколько независимых GridView на одной странице, например, как вложенные элементы формы, независимые между собой, без зависимостей формы от них, но зависимые от самой формы:
Некоторые моменты в представленном коде можно упростить, если форма небольшая и особая оптимизация не требуется.
Предвосхищая вопросы по большому числу параметров виджета (вроде readOnly, actionColumnLeft) или содержащийся в нём фикс для Select2, или поддержку spinner из Font Awesome. Могу сказать, что это особо не мешает, учитывая небольшой объём кода и отсутствие непредвиденных эффектов; указанные библиотеки широко используется во многих проектах на Yii2; это виджет для RAD разработки. А параметры вроде actionColumnTemplate нужны по причине добавления в виджет важного для выполнения возложенной миссии функционала, никто не мешает установить actionColumnEnabled в false и добавить свой собственный ActionColumn изучив соответствующую часть кода виджета. А также добавить свои кнопки с различными действиями, вроде скачивания файла и отправки AJAX запроса на сервер без изменения состояния GridView.
Также, известно, что Pjax будет вынесен из ядра Yii в третьей версии фреймворка, но пока он там и его удобно использовать. С выходом Yii3 код виджета вполне можно будет переписать в реалиях новой версии фреймворка или вообще отказаться от Pjax и написать нужный функционал с использованием других библиотек или на plain JS, тоже можно сказать про jQuery и Bootstrap.
Как обычно, приветствуется любая конструктивная критика, вопросы и предложения.
1. При любых действиях с данными, вызывающих изменение состояния таблицы, происходит полная перезагрузка всей страницы. Это решается способом из wiki с помощью Pjax, который интегрирован в Yii2. Достаточно обернуть GridView в
<?php Pjax::begin(['id' => 'grid-pjax']); ?>
<?php Pjax::end(); ?>
и процедуры удаления, фильтрации, сортировки, перехода по страницам будут происходить без полного обновления страницы, в сам GridView уже интегрирована поддержка Pjax в необходимом объёме.
Однако, часто в программировании приходится идти на компромисс между удобством (использования, поддержки, отладки, и т. д.) и сложностью, также и в данном случае, использовать Pjax просто и удобно, но при обновлении части страницы с его помощью сервер всё равно формирует полную версию страницы, из которой Pjax на стороне клиента вырезает только нужное. Но это легко исправить добавив в контроллер что-то вроде
if (Yii::$app->request->isPjax) {
return $this->renderPartial('index', [ // если index.php содержит не только GridView, то вместо index.php можно использовать файл содержащий только код виджетов Pjax и GridView
'dataProvider' => $dataProvider,
]);
}
2. Не будет работать лишь обновление без перезагрузки страницы после сохранения формы, в этом случае можно использовать упомянутый способ из wiki разместив форму на одной странице с GridView, и обернув её также а Pjax. Или воспользоваться модальными окнами, о последних речь дальше и пойдёт. Раз уж так получилось, что Yii2 тесно связан с Bootstrap, и он используется во многих проектах на этом фреймворке, то можно применить модальные окна из состава Bootstrap. Для этих целей можно использовать расширение yii2-bootstrap, а из него виджет Modal.
Добавляем в исходный код страницы виджет Modal, например, так:
<?php Modal::begin(['id' => 'form-modal']); ?>
<?php Modal::end(); ?>
При клике по кнопке добавления новой записи или редактирования отображаем форму в модальном окне:
const modal = $("form-modal");
$("div.modal-body", modal).load("url_для_загрузки_формы");
modal.modal("show");
Далее устанавливаем перехватчик события формы beforeSubmit (не забыв в конце вернуть false), в котором отправляем содержимое формы с помощью AJAX на сервер, если нас всё устраивает в полученном от сервера ответе, то скрываем форму и перезагружаем Pjax, если нет, то выводим возвращённые сервером данные в это же модальное окно, или выполняем какие-либо другие действия:
modal.on("beforeSubmit", "form", function () {
const form = $(this);
$.ajax({
type: "POST",
url: form.attr("action"),
data: new FormData(form[0]),
processData: false,
contentType: false,
success: function (message) {
if (message) {
$(".modal-body", modal).html(message);
return;
}
modal.modal("hide");
$.pjax.reload({container: "#grid-pjax"});
}
});
return false;
});
При описанном подходе, если id загруженной в модальное окно формы не будет в пределах страницы уникальным, то могут возникнуть проблемы с валидацией и вообще корректным функционирование формы. А такое вполне может произойти, т. к. автоматически устанавливаемые фреймворком id виджетов автоматически нумеруются по порядку, и при AJAX запросе к серверу эта нумерация начнётся с начала, с нуля. Для решения проблемы можно всегда устанавливать id формы вручную, или реализовать в перехватчике события onClick кнопки отправки формы что-то вроде
const form = $("form", modal);
if (form.attr("id") === "w0") {
form.attr("id", widgetId + "-modal-form");
form.yiiActiveForm();
}
3. Аппетит приходит во время еды, и теперь ещё хотелось бы отправить пользователю некое сообщение (уже скрыв форму и закрыв модальное окно), файл, а может вопреки всем принципам AJAX всё же обновить страницу или сделать переход на другую. Отвязавшись от стандартной процедуры отправки формы в предыдущем пункте, это легко можно реализовать в обработчике success метода $.ajax, описанном выше.
if (message === "#reload") {
window.location.href = window.location.href.replace("#", "");
} else if (message.substring(0, 10) === "#redirect ") {
window.location.href = message.substring(10);
} // и т. д.
Также можно задать пользователю какой-либо вопрос, запросить подтверждение действий, и в зависимости от ответа отправлять запрос на сервер или нет.
Всё это (и даже больше) было реализовано в виде расширения yii2-gridview-ajaxed-widget, в котором модифицированный виджет GridView (наследуется от GridView фреймворка) может быть напрямую использован, как замена стандартному. В данном модифицированном виджете появляются некоторые не обязательные параметры, связанные с дополнительным функционалом, например, addButtons, где можно указать одну или больше кнопок, открывающих форму добавления нового элемента, каждая может быть добавлена со своим route, также можно указать собственные route для встроенных кнопок update, view, delete, но чаще всего в этом нет необходимости, виджет сам определяет нужный route и имена ключевых полей модели и формирует нужные GET параметры. Также можно указать свои функции обратного вызова для обработки ошибок и вывода сообщения пользователю, и немного всякой разной другой кастомизации, необходимость в которой возникала в ходе работы над различными проектами.
Если сам виджет можно использовать без изменений, как стандартный, то в контроллер нужно будет внести некоторые изменения, например actions процедур обновления и удаления должны содержать примерно такой код:
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return null;
}
return $this->renderAjax('_form', [
'model' => $model,
]);
Delete, может вообще ничего не возвращать, или возвращать что-то вроде «#alert Запись невозможно удалить!», тогда пользователю выведется соответствующее сообщение. Как видите create.php и update.php становятся вообще не нужны, код приложения упрощается, а возможности несколько расширяются.
Также появляется возможность использовать несколько независимых GridView на одной странице, например, как вложенные элементы формы, независимые между собой, без зависимостей формы от них, но зависимые от самой формы:
// в контроллере
if (Yii::$app->request->get('_pjax')) {
return $this->renderPartial('_pjax', [
'model' => $model,
]);
}
// в форме
<?= $this->render('_grid1', [
'model' => $model,
]) ?>
// …
<?= $this->render('_grid2', [
'model' => $model,
]) ?>
// в _pjax.php
<?php if (Yii::$app->request->get('_pjax') === '#grid1-pjax'): ?>
<?= $this->render('_grid1', [
'model' => $model,
]) ?>
<?php elseif (Yii::$app->request->get('_pjax') === '#grid2-pjax'): ?>
<?= $this->render('_grid2', [
'model' => $model,
]) ?>
<?php endif; ?>
Некоторые моменты в представленном коде можно упростить, если форма небольшая и особая оптимизация не требуется.
Предвосхищая вопросы по большому числу параметров виджета (вроде readOnly, actionColumnLeft) или содержащийся в нём фикс для Select2, или поддержку spinner из Font Awesome. Могу сказать, что это особо не мешает, учитывая небольшой объём кода и отсутствие непредвиденных эффектов; указанные библиотеки широко используется во многих проектах на Yii2; это виджет для RAD разработки. А параметры вроде actionColumnTemplate нужны по причине добавления в виджет важного для выполнения возложенной миссии функционала, никто не мешает установить actionColumnEnabled в false и добавить свой собственный ActionColumn изучив соответствующую часть кода виджета. А также добавить свои кнопки с различными действиями, вроде скачивания файла и отправки AJAX запроса на сервер без изменения состояния GridView.
Также, известно, что Pjax будет вынесен из ядра Yii в третьей версии фреймворка, но пока он там и его удобно использовать. С выходом Yii3 код виджета вполне можно будет переписать в реалиях новой версии фреймворка или вообще отказаться от Pjax и написать нужный функционал с использованием других библиотек или на plain JS, тоже можно сказать про jQuery и Bootstrap.
Как обычно, приветствуется любая конструктивная критика, вопросы и предложения.