Обновление грида через ajax

в 10:53, , рубрики: ajax, cgridview, yii, метки: , , ,

Привет, читатели!

В этой теме я хочу обсудить наиболее правильное использование компонента CGridView в Yii.
Ниже я опишу 3 способа, которые вижу лично я, и буду рад услышать ваши идеи в комментариях.

Обновление грида через ajax

Итак, задача:
Требуется страница с несколькими блоками, в одном из которых должна быть таблица (грид).
Нужна возможность сортировки и пажинации грида через ajax.

Звучит несложно, не правда ли? Давайте посмотрим, что нам предлагает Yii.

Способ 1. Cтандартный CRUD Generatror

Сгенерированный код содержит 1 действие в контроллере и 1 вьюху:
Controller:

        public function actionAdmin()
        {
                $model=new Product('search');
                $model->unsetAttributes();  // clear any default values
                if(isset($_GET['Product']))
                        $model->attributes=$_GET['Product'];

                $this->render('index',array(
                        'model'=>$model,
                ));
        }

index.php:

...
<?php $this->widget('zii.widgets.grid.CGridView', array(
        'id'=>'product-grid',
        'dataProvider'=>$model->search(),
        'filter'=>$model,
...

В целом все работает, но каждый раз когда вы используете ajax сортировку или пажинацию грида, сервер возвращает полный html-код страницы! Из которого используется только маленький html-кусочек для обновления грида. Если страница содержит несколько элементов (например еще один грид), то все они на серверной стороне обрабатываются впустую. Не самое оптимальное решение на мой взгляд.

Способ 2. Все через Ajax

Чтобы решить проблему способа 1, создадим отдельный action и view для формирования грида. Они будут работать только через Ajax и возвращать контент через метод renderPartial(), который оставит только необходимый html для грида:
Controller:

        public function actionGrid()
        {
             if(!Yii::app()->request->isAjaxRequest) throw new CHttpException('Url should be requested via ajax only');
                $model=new Product('search');
                $model->unsetAttributes();  // clear any default values
                if(isset($_GET['Product']))
                        $model->attributes=$_GET['Product'];

                $this->renderPartial('_grid',array(
                        'model'=>$model,
                ));
        }

_grid.php содержит только код виджета:

...
<?php $this->widget('zii.widgets.grid.CGridView', array(
        'id'=>'product-grid',
        'dataProvider'=>$model->search(),
        'filter'=>$model,
...

и index.php подтягивает грид при первой загрузке страницы:

  <div id="grid-container"></div>

  yii::app()->clientScript->registerScript('load-grid', ' 
      $("#grid-container").load("product/grid");
  ');  

Выглядит аккуратно, но не работает :) Метод renderPartial() не возвращает необходимые гриду JS и CSS файлы! Только Html.
Но стоп! У renderPartial() есть дополнительный параметр, называемый «process output», и который как раз таки позволяет вернуть js и css.
Меняем в контроллере:

        public function actionGrid()
        {
             if(!Yii::app()->request->isAjaxRequest) throw new CHttpException('Url should be requested via ajax only');
                $model=new Product('search');
                $model->unsetAttributes();  // clear any default values
                if(isset($_GET['Product']))
                        $model->attributes=$_GET['Product'];

                $this->renderPartial('_grid',array(
                        'model'=>$model,
                ), false, true);
        }

Загружаем страницу и снова видим бяку: jquery.js и остальные скрипты начинают грузиться каждый раз, когда мы обновляем грид через ajax!
Разумеется, они должны прогрузиться один раз, но renderPartial() исправно возвращает их при каждом запросе.
Как же отключить повторную загрузку скриптов? Для этого можно поставить отдельный extension, который при ajax-запросах вырезает скрипты, уже загруженные на странице.
Вуаля, все работает! Но лично я ожидаю от Yii решения этой задачи стандартными средствами, без расширений…

Способ 3. Все через Ajax, кроме первого раза

А что если в первый раз загрузить грид обычным запросом, а потом обновлять через ajax?
В контроллере все остается почти без изменений, только убираем проверку isAjaxRequest и в renderPartial без дополнительных параметров:

        public function actionGrid()
        {
             if(!Yii::app()->request->isAjaxRequest) throw new CHttpException('Url should be requested via ajax only');
                $model=new Product('search');
                $model->unsetAttributes();  // clear any default values
                if(isset($_GET['Product']))
                        $model->attributes=$_GET['Product'];

                $this->renderPartial('_grid',array(
                        'model'=>$model,
                ));
        }

В основной вьюхе index.php напрямую вызываем actionGrid():

  <div id="grid-container"><?php $this->actionGrid(); ?></div>

а в _grid.php устанавливаем параметр ajaxUrl для будущего обновления грида. Иначе ajaxUrl возмется из текущего url (например product/index):

   $this->widget('zii.widgets.grid.CGridView', array(
        'id'=>'product-grid',
        'dataProvider'=>$model->search() ,
        'filter'=>$model,
        'ajaxUrl' => array('product/grid'),
...

Смотрим результат: изначально грид загружается корректно, но пажинация не работает!
Ссылки у страниц продолжают указывать на product/index вместо product/grid. Изучение кода yii показало, что ссылки в pagination никак не связаны с параметром ajaxUrl и берутся из текущего запроса. Который при первой загрузке совсем не тот, т.к. мы из одного действия вызываем другое. На мой взгляд такой вызов даже идеологически не совсем корректен.
Но деваться некуда, исправляем установкой параметра route в объекте pagination дата-провайдера:
_grid.php:

<?php 
   $dataProvider = $model->search();
   $dataProvider->pagination->route = 'product/grid';

   $this->widget('zii.widgets.grid.CGridView', array(
        'id'=>'product-grid',
        'dataProvider'=>$dataProvider ,
        'filter'=>$model,
        'ajaxUrl' => array($dataProvider->pagination->route),
...

Теперь почти все как надо. Грид загружается, сортировка и навигация работают.
Но осталась ложечка дегтя:
если грид как-то отсортирован и находится не на первой странице, и мы вызываем его обновление через js:

$("#product-grid").yiiGridView("update");

то он сбрасывается на первую страницу со стандартной сортировкой. Обидно, не правда ли? Это происходит потому, что явно указан ajaxUrl и update всегда отправляет запрос по нему. А вот если не указывать ajaxUrl то при обновлении грид сохраняет текущую страницу и сортировку! Потому что внутри грида сохраняется url, по которому он был получен (конкретно в атрибуте title дива с ключами). Но не указывать ajaxUrl тоже нельзя, т.к. тогда запросы будут идти на исходный url грида, т.е. product/index.

Единственное решение, которое пришло мне в голову, это поменять тот самый title при первом (не-ajax) формировании грида. А ajaxUrl не указывать совсем.
итоговый _grid.php выглядит так:

<?php 
   $dataProvider = $model->search();
   $dataProvider->pagination->route = 'product/grid';

   $this->widget('zii.widgets.grid.CGridView', array(
        'id'=>'product-grid',
        'dataProvider'=>$dataProvider ,
        'filter'=>$model,
  //   'ajaxUrl' => array($dataProvider->pagination->route),
...

  if(!yii::app()->request->isAjaxRequest) {
      yii::app()->clientScript->registerScript('grid-first-load', ' 
          $("#product-grid").children(".keys").attr("title", "'.$this->createUrl($dataProvider->pagination->route).'");
      ');
  }

Вот теперь все работает как надо! Только смущает наличие вышеперечисленных костылей.

Заключение

Хотелось бы услышать ваши идеи/комментарии по способам выше и по использованию гридов в ваших yii-проектах.
Спасибо за внимание!

Автор: vitalets


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js