16 марта 2012 в 14:53

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

Yii*
Привет, хабрасообщество!

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



Итак, задача:
Требуется страница с несколькими блоками, в одном из которых должна быть таблица (грид).
Нужна возможность сортировки и постраничной навигации грида через 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-проектах.
Спасибо за внимание!
+12
19609
117
vitalets 20,5

Комментарии (35)

+1
mark_ablov, #
> и пажинации
Жесть :)
+1
vitalets, #
Хотел написать «навигации», но она у меня ассоциируется с кнопками назад-вперед в браузере)
+4
mark_ablov, #
Просто устоявшийся термин это «пагинация» ну или «постраничная навигация».
Довольно сильно резануло.
+2
SamDark, #
«пагинация» тоже сильно режет ухо. Звучит как нечто из гинекологии :)
0
zhulan0v, #
не возникало подобных ассоциаций =)
0
SamDark, #
Теперь будут :)
0
Nigrimmist, #
И пЭйджинг
+1
SamDark, #
А это про недомобильную связь 90-х.
+2
vitalets, #
«постраничная навигация» мне нравится, сейчас поправлю.
спасибо!
0
DiZeee, #
А если в контроллере проверять на ajax и если он, то выдавать renderPartial('_grid'), а если нет, то render('index'), в котором уже renderPartial('_grid')?
0
Gronpipmaster, #
Только хотел написать. Такой способ будет более логичен и не только для гридов, а так-же для всего остального ajax.
0
vitalets, #
Для одного грида подойдет.
Но если actionIndex создает, к примеру, два грида? Придется наставить if'ов.
0
DiZeee, #
ну зачем же сразу ифов, что мешает называть файлы с гридами в соответствии с id этих гридов, которые передаются в гет-параметре ajax?
0
kosenka, #
Зачем использовать «if»?
Можно сделать так:
    public function actionIndex()
    {
        $formsModel=new FormsMain('search');
        $fieldsModel=new FormsFields('search');

        switch(Yii::app()->getRequest()->getQuery('ajax'))
        {
            case 'formsListGridID'   : {
                            $formsModel->attributes=Yii::app()->getRequest()->getQuery('FormsMain');
                            $this->renderPartial('_formsListGrid',array('formsModel'=>$formsModel));
                            break;
                           }
            case 'fieldsListGridID'   : {
                            $fieldsModel->attributes=Yii::app()->getRequest()->getQuery('FormsFields');
                            $this->renderPartial('_fieldsListGrid',array('fieldsModel'=>$fieldsModel));
                            break;
                           }
            default: $this->render('main',array('formsModel'=>$formsModel,'fieldsModel'=>$fieldsModel)); break;
        }
    }

* This source code was highlighted with Source Code Highlighter.


Представление main.php с двумя CGridView:
    <table width="100%" cellpadding="0" cellspacing="1" class="contentList">
        <tr class="odd">
            <td>Forms</td>
            <td>Fields</td>
        </tr>
        <tr class="even">
            <td width="50%">
                            <? $this->renderPartial('_formsListGrid',array('formsModel'=>$formsModel)); ?>
            </td>
            <td width="50%">
                            <? $this->renderPartial('_fieldsListGrid',array('fieldsModel'=>$fieldsModel),false,false); ?>
            </td>
        </tr>
    </table>

* This source code was highlighted with Source Code Highlighter.
0
vitalets, #
В целом я согласен с таким подходом, он работает.
Просто у меня страница с 5-ю блоками ))
0
kosenka, #
Да хоть 10 ))
Главное, чтобы не тормозило и не огорчало того, кто этим будет пользоваться ))
0
vitalets, #
Можно назвать это делом вкуса.
Мне вместо свича на 10 значений приятней иметь компактные action'ы, где в каждом решается конкретная задача.
Например, такие:
    public function actionGrid()
    {
        $dp = new CActiveDataProvider('Product', array(
        'sort' => array(
            'defaultOrder' => 'name asc',
        ),
        'pagination' => array(
            'pageSize' => 10,
        )
        ));

        $this->renderPartial('_grid', compact('dp'));
    }

0
vitalets, #
Меня смущает что серверная логика всех компонентов собрана в одной функции.
А что если я захочу еще в другом месте сайта вывести тот же грид с пользователями?
0
kosenka, #
Сделайте виджет и вызывайте его где угодно.
0
DiZeee, #
проверил, работает отлично
0
vitalets, #
вероятно получилось как-то так:
if(Yii::app()->request->isAjaxRequest)
{
$this->renderPartial(Yii::app()->request->getParam('ajax'), array('dataProvider' => $dp));
}
else
{
$this->controller->render('index', array('dataProvider' => $dp));
}


но как вы выбираете нужный dataProvider?
0
DiZeee, #
Да как угодно, можно вообще передавать все подряд, а можно использовать массив типа $models['first_model'] = $first_model; $models['second_model'] = $second_model; тогда

if(Yii::app()->request->isAjaxRequest)

{

$grid_name = Yii::app()->request->getParam('ajax')

$this->renderPartial($grid_name, array('model' => $models[$grid_name]));

}

else

{

$this->controller->render('index', $models);

}
+1
DiZeee, #
Вообще у нас грид используется как правило в админках, где нет большого количества запросов, так что такого рода оптимизациями можно пренебречь. Но идея интересная.
0
kosenka, #
Прочитал, но не понимаю, почему у меня работает по другому, а не так как Вы описали в Способ 2. Все через Ajax

Вот action, который выводит список пользователей:
<?
class UsersList extends CAction
{
    public function run()
    {
    $model=new User('search');
    if(Yii::app()->getRequest()->getQuery('User')) $model->attributes=$_GET['User'];

        if(Yii::app()->getRequest()->getQuery('ajax'))//проверка "а есть ли в запросе призрак ajax'a"
        {
            $this->controller->renderPartial('usersListGrid',array('model'=>$model));
        }
        else
        {
       $this->controller->render('usersList',array('model'=>$model));
        }
  }
}


* This source code was highlighted with Source Code Highlighter.


Вышеописанный action рендерит представление usersList.php, которое в свою очередь вызывает usersListGrid.php.

Если же потом мы будем переходит по страницам, фильтровать, сортировать, то CGridView подставит в запрос ajax и вышеописанный action будет отдавать только представление usersListGrid.php (чистый html-код, без всяких вызовов jquery.js и остальных скриптов), так как они уже были загружены при первом вызове.

И все будет работать.

Представление: usersList.php
<table width="100%" cellpadding="0" cellspacing="1">
    <tr>
        <td><?=$this->renderPartial('usersListGrid',array('model'=>$model),true,false); ?></td>
    </tr>
</table>

* This source code was highlighted with Source Code Highlighter.

Представление: usersListGrid.php
<table width="100%" cellpadding="0" cellspacing="0">
<tr class="even1">
    <td>
        <?=CHtml::beginForm($this->createUrl("admin/itemsSelected"),'post',array('enctype'=>'multipart/form-data')); ?>
        <? $this->widget('zii.widgets.grid.CGridView', array(
          'id'=>'usersListGrid',
          'dataProvider'=>$model->search(),
          'selectableRows'=>3,
          'template'=>"{summary}<br />{pager}<br />{items}<br />{pager}<br />",
          'pager'=>array(
                  'class'=>'CLinkPager',
                'header'=>'',
                'firstPageLabel'=>'<<',
                'prevPageLabel'=>'<',
                'nextPageLabel'=>'>',
                'lastPageLabel'=>'>>',
                 ),
          'filter'=>$model,
          'columns'=>array(
            array(
              'class'=>'CCheckBoxColumn',
              'id'=>'itemsSelected',
            ),
            array(
              'name'=>'id',
              'value'=>'$data->id',
              //'filter'=>'',
              'htmlOptions' => array('style' => 'text-align:center;width:40px;'),
            ),
            array(
              'name'=>'datreg',
              'type'=>'raw',
              'value'=>'date("d/m/Y H:i:s",$data->datreg)',
              'filter'=>'',
              'htmlOptions' => array('style' => 'text-align:center;width:130px;'),
            ),
            array(
              'class'=>'CButtonColumn',
              'template'=>'{myupdate} {mydelete}',
              'htmlOptions' => array('style' => 'width:30px;'),
              'buttons'=>array(
                'myupdate'=>array(
                  'label'=>'Редактировать',
                  'url'=>'array("admin/usersEdit","id"=>$data->id)',
                  'imageUrl'=>Yii::app()->theme->baseUrl.'/img/pencil.png',
                ),
                'mydelete'=>array(
                  'label'=>'Удалить',
                  'url'=>'array("admin/usersDelete","id"=>$data->id)',
                  'imageUrl'=>Yii::app()->theme->baseUrl.'/img/minus.png',
                  'click'=>'function(){return confirm("'.Yii::t('lan','Удалить ?').'");}',
                  'visible'=>'$data->role!=User::ROLE_ADMIN',
                ),
              ),
            ),

          ),
        ));
        ?>
        С отмеченными: <?//=CHtml::DropDownList('workWithItemsSelected',null,$model->WorkItemsSelected,array('empty' =>'--')); ?>
        <?=CHtml::submitButton('Выполнить',array('onclick'=>"return confirm('?');")); ?>
        <?=CHtml::endForm(); ?>
    </td>
</tr>
</table>


* This source code was highlighted with Source Code Highlighter.
0
vitalets, #
Все правильно, это скорее способ 3, т.к. первый раз у вас грид загружается не через ajax.
Но что если на этой же странице вам потребуется второй грид, например, со списком прав пользователя?
0
kosenka, #
Ответил здесь
+1
PQR, #
@vitalets, увидел статью и уже хотел тебе ссылку отправить… но, дочитав до середины, понял, что где-то я уже всё это слышал :)
0
vitalets, #
Да, решил разобраться уже с этим вопросом. При встрече обсудим)
+1
Ekstazi, #
Спасибо. Всегда когда надо делаю 3-м способом, но, адаптированным. В демоблоге вроде хороший пример был грида + ajax...(или в gii, точнее не смогу сказать).
0
vitalets, #
А как поступаете с route'ом для сортировки и навигации? как в примерах выше или, может, более красиво?
0
Ekstazi, #
Обычно все стандартно делаю. В редких случаях jquery.bbq.js применяю для обработок ссылок вперед-назад и хранения инфы в адресной строке о порядке сортировки и текущей странице.
+1
tnz, #
Как же отключить повторную загрузку скриптов? Для этого можно поставить отдельный extension, который при ajax-запросах вырезает скрипты, уже загруженные на странице… Но лично я ожидаю от Yii решения этой задачи стандартными средствами, без расширений…


Есть вот такая интересная штука, правда ручками надо все скрипты отключить. Можно использовать маски.

        /** @var $cs CClientScript */
        $cs = Yii::app()->clientScript;

        $cs->scriptMap['jquery.js'] = false;
        $cs->scriptMap['jquery.min.js'] = false;
        $cs->scriptMap['jquery.qtip-1.0.0-rc3.min.js'] = false;
        $cs->scriptMap['cart.js'] = false;

        $cs->scriptMap['main.css'] = false;


	$this->controller->renderPartial('_supergrid', array(
            'bla_bla' => $bla_bla
	), false, true);
+1
tnz, #
if(!Yii::app()->request->isAjaxRequest) throw new CHttpException('Url should be requested via ajax only');


Вот для этого довольно удобно использовать фильтр ajaxOnly
0
vitalets, #
Не знал, спасибо!
0
Ekstazi, #
NlsClientScript Поддерживает только одиночную загрузку скриптов.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.