CGridView. Часть вторая. AJAX.

Владимир | | Ajax, PHP, Web разработка, Yii.

yii grid view

В этой части я хочу рассказать о некоторых особенностях реализации AJAX запросов в компоненте CGridView.

Предположим, у нас есть таблица, и мы создали для неё модель и скрипты для выполнения CRUD операций (с помощью встроенного генератора Yii).

Пусть таблица называется countries, содержит список стран с двумя полями (id, name).

В этом случае, страница управления записями будет доступна адресу

index.php?r=countries/admin

cgridview ajax

В нижней части страницы находится пагинатор (листалка). Клик по ссылке с номером страницы приведет к отправке ajax запроса. Но вот тут появляется одна из особенностей. Этот ajax запрос возвращает не только новую страницу с данными, но и всю страницу целиком. Начиная от doctype и заканчивая закрывающим тегом html.

Если на странице ничего, кроме таблицы нет, то это не проблема. Но если мы разместим на ней несколько виджетов, для формирования которых нам потребуется выполнить дополнительные запросы к БД, то смысла в такой ajax пагинации окажется немного.

В общем, нужно, чтобы при клике на номер страницы сервер отправлял только фрагмент разметки с таблицей.

Сделать это несложно. Для начала вынесем из представления views/countries/admin.php код, который выполняет формирование таблицы в отдельный файл — views/countries/admingrid.php.

Речь идет об этом фрагменте

<?php $this->widget('zii.widgets.grid.CGridView', array(
	'id'=>'countries-grid',
	'dataProvider'=>$model->search(),
	'filter'=>$model,
	'columns'=>array(
		'c_id',
		'c_name',
		array(
			'class'=>'CButtonColumn',
		),
	),
)); ?>

На его место в views/countries/admin.php добавляем следующий код.

<?php
$this->renderPartial('admingrid', array('model'=>$model));
?>

Идея в том, что если мы получим ajax запрос, то возвращать будем только содержимое admingrid.php, если запрос будет обычный – всю страницу.

Теперь переделываем метод actionAdmin контроллера.

public function actionAdmin()
{
	$model=new Countries('search');
	$model->unsetAttributes();  // clear any default values
	if(isset($_GET['Countries']))
		$model->attributes=$_GET['Countries'];
	
	if (isset($_GET['ajax'])) {
		$this->renderPartial('admingrid',array(
			'model'=>$model,
		));
	}
	else {
		$this->render('admin',array(
			'model'=>$model,
		));
	}
}

Здесь добавлена дополнительная проверка (строка 8). Класс CGridView имеет свойство ajaxVar, которое по-умолчанию равно ajax. Это свойство задает имя GET параметра, который указывает, что данный запрос является ajax запросом.

Если этот параметр установлен, мы используем метод renderPartial, который возвращает браузеру только содержимое представления admingrid. В противном случае, вызываем метод render, который формирует всю страницу целиком.

Внешне в работе страницы ничего не изменилось, но если вы посмотрите с помощью firebug’а ответы сервера на ajax запросы, то разница будет заметной.

Теперь сделаем просмотр записей с помощью AJAX.

По-умолчанию, если мы нажмем на кнопку просмотра (в последней колонке таблицы), то мы попадем на страницу соответствующей записи.

grid buttons

Мы изменим это поведение. Добавим под таблицей блок, в котором будут отображаться подробные сведения о выбранной записи, т.е. содержимое представления views/countries/admingrid.php.

Чтобы не менять стандартное поведение кнопок в таблице мы добавим к ней ещё один столбец со ссылками «Предпросмотр».

При клике по этой ссылке под таблицей появится подробная информация о данной записи.

Приступим.

Добавляем новую колонку в таблицу. Для этого изменим файл admingrid.php следующим образом.

<?php
$this->widget('zii.widgets.grid.CGridView', array(
	'id'=>'countries-grid',
	'dataProvider'=>$model->search(),
	'filter'=>$model,
	'afterAjaxUpdate'=>'function(id, data) {$.fn.setPreviewLinksHandler(id, data);}',
	'columns'=>array(
		'c_id',
		'c_name',
		array(
			'class'=>'CButtonColumn',
		),
		array(
			'type'=>'raw',
			'value'=>'CHtml::link("Предпросмотр", array("countries/view", "id"=>$data->c_id, "ajax"=>"preview"), array("name"=>"previewLink"))',
		),
	),
)); ?>

Здесь есть нюансы. Во-первых, при добавлении столбца необходимо указать, что он имеет тип raw, иначе по-умолчанию Yii применит фильтры к его содержимому.

Во-вторых, нам нужно назначить каждой ссылке обработчик события onClick. Причем, сделать это необходимо как при первоначальной загрузке страницы, так и при «перелистовании».

Для этого мы используем свойство afterAjaxUpdate (строка 6). Ему присваиваем JavaScript функцию, которая будет вызвана после обновления таблицы с помощью ajax метода.

Теперь напишем функцию setPreviewLinksHandler. Для этого создаём файл js/previewlinks.js.

$(function() {
	$.fn.setPreviewLinksHandler = function(id, data) {
		$("a[name=previewLink]").click(function() {
			var link = $(this);
			$.get(link.attr('href'), function(data) {
				$('#preview').html(data);
			});
			return false;
		});
	};
	$.fn.setPreviewLinksHandler();
});

Эта функция получает в качестве параметров id блока с таблицей и её содержимое (data). Но, т.к. содержимое таблицы мы менять не будем, то и эти параметры нам не нужны.

Наша функция setPreviewLinksHandler ищет все ссылки, у которых атрибут name равен previewLink и назначает им обработчик события onClick.

Этот обработчик формирует и отправляет ajax запрос, который возвращает подробную информацию о записи.

Тут обратите внимание, что при отправке ajax запроса используется атрибут href ссылки, таким образом, при отключенном JS клик по ссылке будет отправлять пользователя на страницу с подробной информацией о записи.

Рассмотрим метод actionView контроллера.

public function actionView()
{
	if (isset($_GET['ajax'])) {
		$this->renderPartial('viewrow',array(
			'model'=>$this->loadModel(),
		));
	}
	else {
		$this->render('view',array(
			'model'=>$this->loadModel(),
		));
	}
}

Здесь мы проверяем, есть ли в параметрах запроса атрибут ajax, и если он есть, показываем представление viewrow. При этом используется метод renderPartial. В противном случае, просто показываем представление view.

viewrow.php создаём по тому же принципу, что и admingrid.php. Перемещаем код создания виджета CDetailView в viewrow.php.

<?php $this->widget('zii.widgets.CDetailView', array(
	'data'=>$model,
	'attributes'=>array(
		'c_id',
		'c_name',
	),
)); ?>

А в view.php добавим

<?php $this->renderPartial('viewrow', array('model'=>$model)); ?>

В результате этих манипуляций подробные сведения о записи будут появляться под таблицей при клике по ссылке «Предпросмотр».

Я надеюсь, что никого не запутал своими объяснениями 😉 Чтобы вам удобнее было экспериментировать, выкладываю архив с исходниками этого примера и дампом базы (предварительно вам нужно будет создать приложение Yii).

Source

Если есть вопросы или замечания, пишите! Особенно интересуют альтернативные варианты решения таких задач 🙂

Интересно почитать

Компания Intelsib предлагает продвижение сайта в интернете, поисковиках, а также его аудит.

  • аякс то такое, а я вот вроде даже фильтры побил )

  • Andrey

    Мне кажется обработчик для ссылки проще добавить с помощью live(), а саму ссылку вынести в ButtonColumn, тем более о том как это сделать, у вас было написано в предыдущих постах. 🙂

  • Да, насчет live вы правы. По-идее, можно будет обойтись без afterAjaxUpdate.

    Ссылку я сделал отдельно только для того, чтобы сократить описание 😉

  • Digger

    Удобнее во всплывающем окне показывать, а не под таблицей.

  • Не спорю, но это тема другого поста 😉
    Тут была задача показать только получение данных с помощью ajax.

    • Иван

      А вы не могли бы натолкнуть на мысль каким именно образом показать содержимое во всплывающем окне, а то что-то пока ничего не выходит )

      • Разницы практически нет. Дело в том, что вы в любом случае создаёте html разметку, которую вставляете в страницу.
        Только для всплывающего окна нужно изменить CSS стили и добавить немного JS кода.
        Проще всего подключить какую-нибудь библиотеку, например, эту.

  • Интересно! Кажется, разобрался!

  • Alex

    А как возможно это использовать для комментариев в блоге(демо yii)?
    Там такая конструкция view:
    <div id=»comments»>
    <?php if($model->commentCount>=1): ?>

    <?php echo $model->commentCount . 'comment(s)'; ?>

    <?php $this->renderPartial('_comments',array(
    'post'=>$model,
    'comments'=>$model->comments,
    )); ?>
    <?php endif; ?>
    </div>
    Комментов на пост бывает ой как много.

  • Да, можно.
    Для этого добавьте метод контроллера, который будет получать id поста и возвращать список комментариев
    $this->renderPartial('_comments',array(
    'post'=>$model,
    'comments'=>$model->comments,
    ));

    Естественно, на страницу поста нужно будет добавить js код, который отправит ajax запрос.

  • Andrew

    Когда я вынес CGridView в отдельное представлен столкнулся с проблемой. Проблема заключается в том что AJAX запрос возвращает как и положено только код с результатом обработки CGridView, корневой див имеет id заданное в свойствах, но вместо его отображения пропадает грид вообще.

    Когда залез в плагин jquery то в обработке success если вывести переменную data то она содержит полный текст, а вот $(data).html() уже без div(а) с id заданым в свойствах, посему в конструкции

    $.each(settings.ajaxUpdate, function() {
    $('#'+this).html($(data).find('#'+this));
    });

    $(data).find('#'+this).html() возвращает null

    для решения проблемы я обернул CGridView еще одним дивом и все заработало.

    Но почему так происходит так и не понял, буду рад комментарию.

    Спасибо за статью!

  • Если я правильно понял ситуацию, происходит следующее.

    $(data) — создает объект jQuery, т.е. объект соответствующий корневому div'у.
    А метод html() возвращает содержимое этого элемента, т.е. содержимое div'а.

    Если верить документации, то именно так и должно быть 😉

    В моем примере элемент, в который вставляется ответ, создан заранее на странице. И вставка выполняется так
    $('#preview').html(data);
    Т.е. внутрь div#preview вставляется ответ сервера целиком.

  • Zvnman

    а как убрать кнопку например view, а остальные оставить?

    • С помощью параметра template

      array(
      'class'=>'CButtonColumn',
      'template' => '{postview} {preview}',

      • Grinderspro

        как настроить вывод например по 20 строк в таблице ? (по умолчанию 10)

        пишу так: 'pager'=>array('pageSize'=>20,), но все равно 10

        • Andrey Grachov

          Количество строк надо задавать в используемом DataProvider:
          'pagination' => array('pageSize'=>20)

  • Гость
  • Ololo

    спасибо за Ваш блог с такими качественными и подробными статьями!
    Обновляйтесь почаще : ) 

  • Отличная статья. Спасибо!

  • TempoMail

    вообще-то, эти виджеты довольно запутанная вещь, пока не разберешься со всем что разработчик туда вложил и как описывать элементарные вещи — ничего не получится. Даже с ооп опытом на пхп, лучше плюнуть и создать свои визуализации представлений со своими виджетами и не мучить мозги. По крайней мере будет все понятно.
    А то как добавить кнопку картинку в колонку надо выдумывать непонятно что.
    Надо начинать цикл статей «переделываем Yii в правдивое Yes it is!»

    Автору за статью спасибо.

    • Большинство виджетов предназначены для выполнения стандартных операций и рассчитаны на работу с другими компонентами фреймворка. Для каждого частного случая они не подойдут. Например, если вы ходите показать в виджете данные, полученные, например, с помощью двух CActiveDataProvider, то использовать CGridView будет проблематично и лучше написать свой виджет.

      С другой стороны, ваш виджет тоже вряд ли будет универсальным 😉

  • Антон

    после перехода по страницам перестают работать фильтры. может сталкивались с такой проблемой?

    • Не сталкивался. Но порядок поиска ошибки может быть примерно такой.
      1) Активируете лог запросов к базе http://www.yiiframework.com/doc/guide/1.1/ru/topics.logging
      2) Проверяете метод search() модели. В нем должен быть код вроде
      $criteria=new CDbCriteria;
      $criteria->compare('id',$this->id);
      $criteria->compare('title',$this->title,true);

      3) Смотрите разницу в запросах к базе, которые отправляются при просмотре первой страницы и последующих.

      Дальше — по ситуации.

      • Антон

        странно, думал, что это баг грида, т.к. у меня он во всех интерфейсах. дело в том что после перехода по страницам и выборе фильтра на сервер отправляется кривой GET-запрос вида controller/action/ajax/model-grid/Model[param_1]/OLD_VALUE?ajax=model-grid&Model[param_1]=NEW_VALUE&stock_page=1 и сервер цепляет первые (старые параметры)

  • Egor

    Столкнулся с такой проблемой, делаю все как у вас описано, не работает обновление и добавление новых записей, после нажатия кнопки создать/сохранить, обновляется текстфилд, и ничего не происходит? Может быть кто-то сталкивался? Ошибок  ПХП скрипты не возвращают, все ответы сервера норм. В чем может быть Проблема? Заранее спасибо за помощь!

    • Добавление записей в этой статье не рассматривалось. Используется код, сгенерированный с помощью yiic. Если вы видите, что запрос с корректными данными ушел и php код ошибок не вернул, значит запись должна появиться в БД. Это так, запись в базе есть?
      После этого, при обновлении страницы CGridView должен вывести список записей из базы.

      Приведите, пожалуйста, скриншот. Не совсем ясно, в какой момент «ничего не происходит».

  • Filsh

    А как можно обновлять поля через ajax?

    • Стандартный механизм, если не ошибаюсь, не предусмотрен. Т.е. придется создать форму, написать js обработчик, который отправит ajax запрос и написать соответствующий action контроллера.
      После получения ответа — обновляете grid.

  • bob

    Может вы с таким сталкивались. Есть вьюшка, которая выводит данные из таблицы User. Если применить фильтр, а затем перейти на вторую страницу результата (т.е. когда пользователей соответствующих этому фильтру больше чем 10 например), тогда все фильтры перестают работать. Т.е. если я делаю фильтрацию по имени, что бы мне выводились все пользователи у которых имя начинается на А. Их в базе находится например 100, мне формируется ответ из 10 страничек, перехожу на вторую(тут отображены пользователи с 11 по 20 ), так вот на этой странице если я захочу использовать еще дополнительный фильтр, например по дате, то он уже не сработает

    • 1) найдите метод контроллера, который обрабатывает отображение страниц с фильтрами
      2) в этом методе должно быть создание моделей (CActiveRecord или CDataProvider). Убедитесь, что при получении данных из этих моделей используются параметры фильтров и номер страницы.

  • Поэксперементировал, что то изменения в скорости загрузки страницы я не сильно заметил.

  • Rarog

    Сделал вывод CGridView
    $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$serviceDP,
    'template'=>'{items}',
    'columns'=>array(
    'name',
    array(
    'class'=>'CButtonColumn',
    'template'=>'{delete}',
    'deleteButtonUrl' => 'Yii::app()->controller->createUrl(«deleteService»,array(«sid»=>$data->id,»uid»=>'.$model->id.'))',
    'deleteConfirmation' => 'Вы уверены, что хотите удалить этот сервис?',
    ),
    ),
    ));
    Строчки удаляются и судя по ФайрБагу страничка возвращается. Но в браузере не перерисовывается. В чём может быть проблема?

    • Раз у вас установлен firebug, то посмотреть что происходит достаточно просто. Перерисовка в браузере выполняется с помощью JavaScript после получения ответа на AJAX запрос. В этой статье такая операция приведена в четвертом листинге (начиная со строки 4). Вам нужно найти ваш скрипт, который отправляет AJAX запрос и посмотреть что происходит после получения ответа.