«Бесконечная» лента записей с ajax дозагрузкой на Yii

Лента новостей

На многих новых сайтах всё чаще встречается вывод списка новостей или других сущностей в виде бесконечно подгружающейся ленты. На некоторых сайтах подгрузка выполняется автоматически (на twitter.com или vk.com), на других – вручную, то есть в конце списка вместо стандартного переключателя страниц имеется кнопка «Показать ещё». Освежим в памяти работу с ClistView и попробуем реализовать подобный функционал на своём сайте.

Довольно часто такую бесконечную «стену» делают где ни попадя, не задумываясь, нужна она или нет. Но мы не будем касаться здесь этической стороны.

Итак, нам нужно подгружать записи используя Ajax. Сначала отвлечёмся на разбор работы Ajax обновлений стандартного списка ClistView.

Вывод списка записей с ClistView

В самом простом случае с использованием встроенных средств Yii Frmework мы можем выводить список постов с Ajax переходами по страницам так:

Контроллер controllers/PostController.php:

class PostController extends Controller
{
    public function actionIndex()
    {
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
            ),
        ));
 
        $this->render('index', array(
            'dataProvider'=>$dataProvider,
        ));            
    }
}

Представление views/post/index.php:

<?php
$this->pageTitle = 'Блог';
$this->breadcrumbs = array(
    'Блог',
);
?>
<h1>Блог</h1>
<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>true,
    'template'=>"{items}\n{pager}",
)); ?>

Элемент списка views/post/_view.php:

<?php
<article>
<h2><?php echo Chtml::link($data->getUrl(), $data->title); ?></h2>
<?php echo $data->short; ?>
</article>

Здесь у нас всего одно действие actionIndex() и одно представление index.php для него. Но в блоге могут быть вывод записей из категории, записей по тегу, по дате и т.д. Все они будут использовать свои шаблоны index.php, category.php, tag.php, date.php, но одинаковый общий список. Целесообразно по принципу шаблонов Wordpress вынести формирование списка в отдельный файл _loop.php.

Все шаблоны типа index.php теперь ссылаются на файл списка _loop.php:

views/post/index.php:

<?php
$this->pageTitle = 'Блог';
$this->breadcrumbs = array(
    'Блог',
);
?>
<h1>Блог</h1>
<?php $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>

views/post/_loop.php:

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>true,
    'template'=>"{items}\n{pager}",
)); ?>

Теперь все наши списки нормально выводятся и переход по страницам происходит по Ajax. Но в этом простейшем способе кроются некоторые подводные камни.

Снижение нагрузки при обновлении по ajax

Ajax запрос обращается по ссылкам, которые прописаны в ячейках списка номеров страниц, то есть фактически к тому же actionIndex(), который независимо от способа запроса рендерит полную страницу сайта в строке $this->render('index', array(...)). Обработчик Ajax ответа только выделяет из всего HTML-содержимого код своего списка. Эта тема уже поднималась на Habrahabr. Теперь мы можем решить эту проблему.

При Ajax запросе наш контроллер должен возвращать не всю страницу в шаблоне, а только код списка. Так как мы вынесли список в отдельный файл _loop.php, ничто не мешает генерировать только его вызывая $this->renderPartial('_loop', array(...)):

class PostController extends Controller
{
    public function actionIndex()
    {
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
            ),
        ));
 
        if (Yii::app()->request->isAjaxRequest){
            $this->renderPartial('_loop', array(
                'dataProvider'=>$dataProvider,
            ));
            Yii::app()->end();
        } else {
            $this->render('index', array(
                'dataProvider'=>$dataProvider,
            ));
        }
    }
}

Теперь при Ajax запросе будет возвращаться только список без генерации всего шаблона сайта. Этот способ подойдёт и для CGridView, где из файла admin.php таблицу можно вынести в файл _grid.php.

Добавляем подгружающуюся ленту

Действия нашего контроллера уже способны оптимизировать свою работу при Ajax запросах. Рассмотрим теперь непосредственно организацию ленты. Нам нужно:

  • Создать на странице блок <div id="listView"></div>;
  • В блоке вывести стандартный список первых 10 новостей со навигацией по страницам;
  • Если включен JavaScript и страниц больше одной, то скрыть навигатор и отобразить кнопку «Показать ещё»;
  • Запомнить в JavaScript номер текущей страницы
  • Навесить на эту кнопку обработчик, который бы по щелчку увеличивал номер текущей страницы на единицу и загружал новую порцию записей и добавлял в блок #listView;
  • Если записи закончились (page>=pageCount), то скрыть кнопку.

Если JavaScript у пользователя отключен, то наш скрипт не сработает и останется стандартный навигатор.

Также нам нужно добавить индикацию процесса загрузки (можно, например, показывать/скрывать анимированное изображение или дабавлять/удалять класс-примесь кнопке) и защиту от частых нажатий (выставлять флаг на всё время згрузки).

Представление views/post/_loop.php с учётом этого может быть примерно таким:

<div id="listView">
 
    <?php $this->widget('zii.widgets.CListView', array(
        'dataProvider'=>$dataProvider,
        'itemView'=>'_view',
        'ajaxUpdate'=>false,
        'template'=>"{items}\n{pager}",        
        'pager'=>array(
            'htmlOptions'=>array(
                'class'=>'paginator'
            )
        ),
    )); ?>
 
</div>
 
<?php if ($dataProvider->totalItemCount > $dataProvider->pagination->pageSize): ?>
 
    <p id="loading" style="display:none"><img src="<?php echo Yii::app()->request->baseUrl; ?>/images/loading.gif" alt="" /></p>
    <p id="showMore">Показать ещё</p>
 
    <script type="text/javascript">
    /*<![CDATA[*/
        (function($)
        {
            // скрываем стандартный навигатор
            $('.paginator').hide();
 
            // запоминаем текущую страницу и их максимальное количество
            var page = parseInt('<?php echo (int)Yii::app()->request->getParam('page', 1); ?>');
            var pageCount = parseInt('<?php echo (int)$dataProvider->pagination->pageCount; ?>');
 
            var loadingFlag = false;
 
            $('#showMore').click(function()
            {
                // защита от повторных нажатий
                if (!loadingFlag)
                {
                    // выставляем блокировку
                    loadingFlag = true;
 
                    // отображаем анимацию загрузки
                    $('#loading').show();
 
                    $.ajax({
                        type: 'post',
                        url: window.location.href,
                        data: {
                            // передаём номер нужной страницы методом POST
                            'page': page + 1,
                            '<?php echo Yii::app()->request->csrfTokenName; ?>': '<?php echo Yii::app()->request->csrfToken; ?>'
                        },
                        success: function(data)
                        {
                            // увеличиваем номер текущей страницы и снимаем блокировку
                            page++;                            
                            loadingFlag = false;                            
 
                            // прячем анимацию загрузки
                            $('#loading').hide();
 
                            // вставляем полученные записи после имеющихся в наш блок
                            $('#listView').append(data);
 
                            // если достигли максимальной страницы, то прячем кнопку
                            if (page >= pageCount)
                                $('#showMore').hide();
                        }
                    });
                }
                return false;
            })
        })(jQuery);
    /*]]>*/
    </script>
 
<?php endif; ?>

Здесь мы отключили за ненадобностью встроенную поддержку ajax, так как будем делать переходы в своём скрипте.

Это представление уже не надо возвращать по Ajax. Оно должно выводиться единожды и подгружать только голый список. Для этого добавим представление loopAjax.php с кодом подгружаемого списка без прочих лишних элементов:

views/post/_loopAjax.php

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>false,
    'template' => "{items}",
)); ?>

Кроме переименования _loop в _loopAjax в контроллере необходимо произвести ещё несколько изменений. Все они касаются передачи номера необходимой страницы контроллеру.

Можно заметить, что номер страницы из нашего скрипта передаётся посредством переменной page в POST запросе (а не в GET), а в классе CPagination, используемом CActiveDataProvider, для определения номера текущей страницы используется именно GET.

В зависимости от используемых роутов при различных построениях ЧПУ номер страницы в URL может находиться в любом месте, например

http://site.com/blog?page=2
http://site.com/blog/page-2
http://site.com/page/2
http://site.com/blog/2
http://site.com/page-2

Наш JavaScript код не может воспользоваться функцией Yii::app()->createUrl() для генерации ссылок, поэтому номер необходимой страницы проще передать в POST запросе, отправленном на текущий адрес window.location.href и в контроллере произвести подмену $_GET['page']=$_POST['page'].

Кроме того, нужно явно указать имя параметра page в поле pageVar, иначе по умолчанию он будет использовать параметр по имени нашей модели, то есть Post_page.

class PostController extends Controller
{
    public function actionIndex()
    {        
        $this->processPageRequest('page');
 
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
                'pageVar' =>'page',
            ),
        ));
 
        if (Yii::app()->request->isAjaxRequest){
            $this->renderPartial('_loopAjax', array(
                'dataProvider'=>$dataProvider,
            ));            
            Yii::app()->end();
        } else {
            $this->render('index', array(
                'dataProvider'=>$dataProvider,
            ));
        }
    }    
 
    protected function processPageRequest($param='page')
    {
        if (Yii::app()->request->isAjaxRequest && isset($_POST[$param]))
            $_GET[$param] = Yii::app()->request->getPost($param);
    }
}

Подмену значения $_GET['page'] мы производим в методе processPageRequest(). Его можно при желании либо поднять в базовый контроллер, либо (чтобы избавиться от лишней строки $this->processPageRequest('page');) вынести в отдельный фильтр.

После всех этих манипуляций мы получим «бесконечную» ленту, подгружающую записи и становящуюся всё длиннее и длиннее по щелчку мыщи. Для автоматической загрузки новых записей при прокрутке страницы нужно обработчик щелчка $('#showMore').click() заменить на обработчик прокрутки страницы $('html').scroll() с проверкой на появление нашего блока #showMore в видимой части окна.

Для большего удобства можно вынести код кнопки и обработчика в отдельный настраиваемый виджет, которому бы передавались, например, $dataProvider и CSS-идентификатор стандартного навигатора по страницам.

Другие статьи

При переделывании своего проекта на модульную структуру сразу же захотелось сделать модули минимально связанными. При этом многим приходится сталкиваться с организацией выноса настроек модулей из главного конфигурационного файла в сами модули. Рассмотрим несколько приёмов и подводных камней.

Раньше мы рассматривали возможность повторного использования стандартных CRUD операций путём выноса их в отдельные классы (наследники класса CAction в Yii). У этого способа есть альтернатива – наследование общих действий от базового контроллера. Рассмотрим этот способ подробнее, а также попробуем найти и решить некоторые его проблемы.

Одна из вечных тем, то и дело всплывающих в сети и касаемых Yii Framework – это спор относительно использования в своих проектах прямых SQL запросов посредством DAO с одной стороны против использования ActiveRecord с другой. Ведь при разрастании объёмов данных и связей между ними в высоконагруженных проектах многие разработчики переходят от удобной объектной модели ActiveRecord к низкоуровневой работе с прямыми SQL запросами и с простыми асcоциативными массивами. Посмотрим, как в некоторых случаях можно разогнать выборки ActiveRecord почти до скорости DAO.

Разговорились сегодня насчёт вывода списка чекбоксов в админке для выбора категорий к записи, то есть для связи MANY_MANY. Предоположим, что в нашем блоге есть записи и категории. Или товары в магазине и категории. При этом у каждой записи или у каждого товара можно выбрать несколько категорий. Как вывести этот список на странице редактирования статьи или товара?

Комментарии

 

A

Опечатка: $this->processCurrentPage('page');
Надо: $this->processPageRequest('page');

Для автоматической подгрузки:

$(window).scroll(function()
{
    if (!loadingFlag)
    {
        if ($(window).scrollTop() == $(document).height() - $(window).height())
        {
            loadingFlag = true;
                    ...

                    // если достигли максимальной страницы, то прячем кнопку
                    if (page >= pageCount)
                        loadingFlag = true;
                }
            });
        }
    }
    return false;
})

Хмм, не уверен, где разместить loadingFlag = true;, который вначале.

Ответить

 

Дмитрий Елисеев

Спасибо, исправил.

Ответить

 

Alexander

у вас все равно $this->processCurrentPage('page'); стоит, такого метода нет в контроллере.

Ответить

 

helloworld

Сдейлайте в виде екстеншена

Ответить

 

Дмитрий Елисеев

А какой формат расширения для этого лучше подойдёт?

Ответить

 

assd

Здравствуйте. Спасибо за отличный пример. А какой у Вас action для _view? Мне не совсем понятно с выборкой.

Ответить

 

Дмитрий Елисеев

Это представление _view.php выводит каждую запись в _loop.php:

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
)); ?>
Ответить

 

assd

Тогда во _view должно быть так?

<article>
<h2><?php echo Chtml::link($dataProvider->getUrl(), $dataProvider->title); ?></h2>
<?php echo $dataProvider->short; ?>
</article>
Ответить

 

Дмитрий Елисеев

Почти:

<article>
    <h2><?php echo Chtml::link(CHtml::encode($data->title), $data->getUrl()); ?></h2>
    <?php echo $data->short; ?>
</article>

Текущая модель передаётся в переменную $data, номер записи в $index, а this виджета CListView в $widget.

Ответить

 

assd

Спасибо, разобрался уже после того как спросил)

Ответить

 

Святослав

Здравствуйте. Прошу прощения, не слишком силен в yii. Допустим во время того, как пользователь осуществляет прокрутку ленты, добавляется несколько новых записей в базу. Будут ли при этом продублированы последние добавленные на страницу записи?

Ответить

 

Дмитрий Елисеев

Да, новые будут сдвигать старые вниз. Это и обычного постраничного отображения касается.

Ответить

 

Святослав

Подскажите пожалуйста, а как решить эту проблему? Может необходимо при первой загрузке страницы сохранить в javascript переменной время первой загрузки, и потом передавать его контроллеру, а из базы выбирать данные, у которых время добавления не позже переданного?

Ответить

 

Дмитрий Елисеев

Можно и так.

Ответить

 

Святослав

Спасибо)

Ответить

 

Ivan

Здравствуйте!
Спасибо за пример. Все сделал, работает. Данные подгружает, но Js не работает в подгруженных данных. Как побороть это?
У меня подгружаются картинки, с возможностью проголосовать за них, так вот это голосование не работает...

Ответить

 

Дмитрий Елисеев

Навешивайте голосования live-привязкой через on():

$(document).on('click', 'span.vote', function(){...});

вместо обычного click():

$('span.vote').click(function(){...});
Ответить

 

Ivan

Спасибо! Попробую, отпишусь потом.

Ответить

 

Владимир 108495791184666567385

в views/post/_loop.php используется и jQuery и $
думаю стоит писать все в одном стиле

Ответить

 

Дмитрий Елисеев

Исправил. Спасибо!

Ответить

 

Александр

Спасибо за статью. Вопрос, почему когда нажимаю первый раз на "Показать ещё", он загружает новые 10 статей и по идеи в базе больше нет статей, но кнопка не исчезает и если нажать ещё то появляются те, к-е уже выводились.

Ответить

 

Александр

Ещё показывает несколько раз "Показать ещё" после первого нажатия.

Ответить

 

Антон

Подскажите, пожалуйста, почему JavaScript в Вашем примере не может воспользоваться Yii::app()->createUrl()?
Спасибо.

Ответить

 

Дмитрий Елисеев

Yii::app()->createUrl() – это PHP. В JavaScript без ajax это сделать будет проблематично.

Ответить

 

Антон

из Вашего же примера

$.ajax({
    type: 'post',
    url: window.location.href,
    data: {
        // передаём номер нужной страницы методом POST
    'page': page + 1,
    '<?php echo Yii::app()->request->csrfTokenName; ?>': '<?php echo Yii::app()->request->csrfToken; ?>'
Ответить

 

Дмитрий Елисеев

Ну попробуйте.

Ответить

 

Александр

Свою проблему решил. Вместо "_loopAjax", написал "_loop", поэтому не работало, исправил, теперь работает.

Ответить

 

Дмитрий Елисеев

А я уж подумал, что я в $ переименовал не так.

Ответить

 

Александр

:)

Ответить

 

Дмитрий Ермола

Отличный пример. Вообще у Вас на сайте замечательный материал, уже нашел для себя много полезного и интересного. В случаях когда делают "бесконечную ленту" делают еще и догрузку новых записей в начало списка, естественно с помощью AJAX. Было бы здорово если бы Вы описали и этот нюанс.

Спасибо :)

Ответить

 

Дмитрий Елисеев

Например, элементы можно пометить:

<div class="item" data-id="<?php echo $data->id; ?>">...</div>

Теперь в JavaScript по setTimeout() через каждые несколько секунд получать крайний идентификатор записи на странице:

var last_id = $('.item:first-child').data('id');

и передавать его в Ajax запросе.

А контроллер должен найти записи с id больше last_id и сгенерировать ответ через renderPartial(). В success коллбэке Ajax запроса клиентский скрипт теперь получает этот ответ, добавляет элементы вверх через prepend() и запускает следующий setTimeout().

Ответить

 

Дмитрий Ермола

Пожалуй это будет оптимальный вариант. Еще был вариант посмотреть в сторону "WebSockets", но поскольку технология еще не всеми браузерами поддерживается (или не всеми корректно), решил пока не углубляться в них.

Спасибо за подсказку.

Ответить

 

Самат

Добрый день!
у меня вопрос возник, а как реализовать pagination так что бы он при переходе на следующий страницу или при нажатии на любую из страничек делалэто без перезагрузки страницы?

Ответить

 

Дмитрий Елисеев

У CListView или CGridView указать 'ajaxUpdate' => true.

Ответить

 

White Whale

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

<div class="posts" id="posts">
   <div id="yw0" class="list-view">
     <div class="items"></div>
   </div>
   <div id="yw0" class="list-view">
     <div class="items"></div>
   </div>
</div>
Ответить

 

Дмитрий Елисеев

Да, так и есть. Но если это критично, то можно сделать озвлечение элементов из data и вставлять прямо в items:

$('#listView .items').append(data);
Ответить

 

Дмитрий Поплавский

Может можете подсказать, как извлечь из data только div item?

Ответить

 

Дмитрий Елисеев

Достаточно как-то обозначить закрывающий блок в _view.php:

<div class="items">
...
</div><!--items-->

И на клиенте выбрать внутренности с помощью регулярного выражения вроде:

<div class="items">(.*?)</div><!--items-->
Ответить

 

Akzhol

Грузит последние два элемента с базы данных. Видимо нет никакой проверки на то что эти записи уже выводились. Помогите пожалуйста!

Ответить

 

Akzhol

Грузит последние два элемента с базы данных снова и снова при прокрутке вниз*. Видимо нет никакой проверки на то что эти записи уже выводились. Помогите пожалуйста!

Ответить

 

Akzhol

А все исправил.
if (page >= pageCount)
loadingFlag = true;
написал совсем в другом месте

Ответить

 

Евгений

Здравствуйте, сделал ленту, отлично работает и автору респект!
Столкнулся с тем что необходимо добавить пользовательский поиск, чтобы была возможность ввода параметров для поиска и вывод данных сохраняя при этом ленту, делал сначала форму путем добавления модели и формы, но вот по нажатию поиска столкнулся с проблемой как сделать вывод на основе атрибутов этого самого поиска? какие идеи есть по этому поводу?

Ответить

 

Евгений

Реализовал, все работает. Для примера из этой статьи
Перед $dataProvider в контроллере добавляем

$model=new Post;
$model->unsetAttributes();  // чистим атрибуты поиска
if(isset($_GET['Post']))
{
    $model->attributes=$_GET['Post'];
    $newDataProvider=$model->search();
    $criteria->mergeWith($newDataProvider->criteria);
}

Далее в контроллере меняем

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

на следующий код

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

и во вьюшке в данном случае views/post/index.php добавляем форму поиска через виджет CActiveForm

Ответить

 

Всеволод

Подскажите пожалуйста, а как сделать, чтобы вначале при загрузке страницы не показывало ни одного блока, а при нажатии на кнопку уже выводило по три блока (или столько, сколько указано в размере "page")?

Ответить

 

Дмитрий Елисеев

Со стороны сервера сразу не скажу, а на клиенте можно сразу убрать:

$('#listView .items').html('');

И тогда page надо считать не с единицы, а с нуля.

Ответить

 

Максим

Не работает. На странице не выводит ни одного элемента, при этом есть надпись "показать еще". При нажатии на нее подгружается лоад-гифка и всё.

Контроллер:

if (Yii::app()->request->isAjaxRequest){
    $this->renderPartial('_loopAjax', array(
        'dataProvider'=>$dataProvider,
    ));
    Yii::app()->end();
}

Представление:

<ul class="catalog">
    <?php $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>
</ul>
Ответить

 

Максим

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

$this->processPageRequest('page');
        
$this->settings = User::model()->findByPk(1);
$main = Pages::model()->findByPk(1);

$dataProvider = new CActiveDataProvider('Product', array(
    'criteria'=>array(),
    'pagination'=>array(
        'pageSize'=>9,
        'pageVar' =>'page',
     ),
));

if (Yii::app()->request->isAjaxRequest){
    $this->renderPartial('_loopAjax', array(
        'dataProvider'=>$dataProvider,
    ));
    Yii::app()->end();
} else {
    $this->layout ='//layouts/catalog';
    $this->render('catalog',
       array(
          'dataProvider'=>$dataProvider,
          'main'=>$main,
       )
    );
}
Ответить

 

Дмитрий Елисеев

А что внутри ответа с layout? Обычно он весь подгружется при выводе ошибки.

Ответить

 

Максим

С этим разобрался. Сейчас возникли проблемы связать это все еще с аякс-фильтром. Запутался уже) Буду очень благодарен небольшому примеру. Сейчас у меня так.
_loop.php:

<div id="listView">
<?
				$this->widget('zii.widgets.CListView', array(
				  'dataProvider'=>$dataProvider,
				  'ajaxUpdate'=>false,
				  'itemView'=>'item',
			    'template'=>'{items}{pager}',
			    'pager'=>array(
		            'htmlOptions'=>array(
	                'class'=>'paginator'
			            )
			        ),
			    'id'=>'ajaxListView',

				));
?>
</div>
<?php if ($dataProvider->totalItemCount > $dataProvider->pagination->pageSize && CHtml::normalizeUrl('')!=='/'): ?>
 
    <a id="showMore" class="more-catalog" href="">
                Показать еще
    </a>
    <script type="text/javascript">
        (function($)
        {

            $('.pager').hide();

            var page = parseInt('<?php echo (int)Yii::app()->request->getParam('page', 1); ?>');
            var pageCount = parseInt('<?php echo (int)$dataProvider->pagination->pageCount; ?>');
 
            var loadingFlag = false;
 $('.filter-check').change(function()
            {
                var inc = [];
                $('input[name="inc[]"]:checked').each(function() {inc.push($(this).attr('value'));});

                    $.ajax({
                        type: 'post',
                        url: '/catalog',
                        data: {
                            'inc': inc,
                            '<?php echo Yii::app()->request->csrfTokenName; ?>': '<?php echo Yii::app()->request->csrfToken; ?>'
                        },
                        success: function(data)
                        {
                            $('#listView').empty();
                            $('#listView').append(data);  
                        }
                    });
                return false;
            })
            $('.more-catalog').click(function()
            {
                var inc = [];
                $('input[name="inc[]"]:checked').each(function() {inc.push($(this).attr('value'));});
                if (!loadingFlag)
                {
                    loadingFlag = true;
                    $.ajax({
                        type: 'post',
                        url: '/catalog',
                        data: {
                            'page':page+1,
                           'inc':inc,
                            '<?php echo Yii::app()->request->csrfTokenName; ?>': '<?php echo Yii::app()->request->csrfToken; ?>'
                        },
                        success: function(data)
                        {
                            page++;                            
                            loadingFlag = false;                            
                            $('#listView').append(data);
                            if (page >= pageCount)
                                $('#showMore').hide();
                        }
                    });
                }
                return false;
            })


        })(jQuery);
    /*]]>*/
    </script>
 
<?php endif; ?>


_loopAjax:

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'item',
    'ajaxUpdate'=>false,
    'template' => "{items}",
)); ?>


контроллер:

  if (Yii::app()->request->isAjaxRequest){
            $dataProvider = $this->searchFilter();
            $this->renderPartial('_loopAjax', array(
                'dataProvider'=>$dataProvider,
            ));
            Yii::app()->end();
        }


Вроде выборка правильная, но проблема в том, что если кнопка "Показать еще" была нажата, то она больше не появится при фильтрации

Ответить

 

Максим

И кнопка появляется в начале всегда (даже если элемента 3, при нажатии добавляются они же)

Ответить

 

Дмитрий Елисеев

Просто не забудьте учесть, что при фильтрации нужно получить новый pageCount и сбросить page=1.

Ответить

 

dmitrii

Ваш метод инфинит скролла имеет баг

Conditions: limitPage = 10, countItems = 11

Вы передаете page=2, Вам по идеи должно вернуть 1 item, но за пару сек. до этого кто то постит в ленту несколько сообщений в итоге вам вернется 9, 10, 11[id]
9, 10 - вернулись в первом запросе, получаем дублирование контента, решать этот вопрос средствами frontend это костыль.
Эту проблему нужно решать на уровне backend, солюшен состоит в том что бы передавать id последнего item со страници

В кондишенах имеем

$criteria->compare('id', $lastIdPrevPage, false, '>');
Ответить

 

Игорь

Спасибо!
Сразу интегрировал !!!!!!!!!!!!!1

Ответить

 

Владимр

Тут у меня проблема возникла, как вывести сообщения в обратном порядке после выборки из бд.
Вот критерии:

$criteria = new CDbCriteria;
$criteria->condition = '...';
$criteria->params = array(...);
$criteria->order = 't.created_at DESC';

$dataProvider = new CActiveDataProvider('UserMessages', array(
    'criteria'=>$criteria,
    'pagination'=>array(
       'pageSize'=>5,
    ),
));
Ответить

 

Владимр

Как видно из критерии сортировка идёт по desc, а как мне при рендере отсортировать сообщения по ACS, что-бы новые сообщения отображались последними в списке?
Может быть есть что-либо из коробки?
Пока не придумал ничего умнее чем:

   <?php $dataProvider->setData(array_reverse($dataProvider->getData()));?>
   <?php $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>
Ответить

 

Дмитрий Елисеев

Ну если нужно именно после выборки отсортировать, то можно и так.

Ответить

 

Максим

Дмитрий, большое спасибо за пример. Всё отлично работает!
Подскажите, как организовать добавку /page/номер_страницы в url при нажатии кнопки "Показать ещё"?

Ответить

 

Дмитрий Елисеев

Используйте history API.

Ответить

 

Максим

Спасибо!

Ответить

 

Геннадий

Скажите, а на сколько увеличивается нагрузка на сервер при таком подходе? Может быть лучше передавать только данные о постах в JSON, а потом уже javascript-ом их обворачивать в нужные дивы и вставлять? У меня просто проект где нагрузка на сервер будет немаленькая и вот думаю, делать способом как у Вас или все же передавать только данные в JSON, а уже остальную их обработку делать на javascript-ом. По логике нагрузку на сервер я таким образом сделаю немного меньше, но получается нехороший код тогда. Нужно, например, помимо php шаблона вывода поста делать шаблон для вставки данных в js. При изменении одного из них - надо менять второй. Ну и скорее всего в абсолютном выражении такой подход будет медленнее, ибо придется делать много замен в цикле в js. Чтобы из конструкции а-ля

%post_title%

%post_date%

получилась конструкция с именем и датой поста, которые пришли в JSON с сервера. Как думаете, какие еще моменты я не учел для выбора? На что еще стоит обратить внимание? Чтобы Вы посоветовали?

Ответить

 

Дмитрий Елисеев

Так как в контроллере сейчас есть условие:

if (Yii::app()->request->isAjaxRequest){
    $this->renderPartial('_loop', array( 'dataProvider'=>$dataProvider));
    Yii::app()->end();
} else {
    $this->render('index', array('dataProvider'=>$dataProvider));
}

то кроме _loop ничего лишнего из шаблона сервере рендериться не будет. А если делать возврат через JSON, то помимо самих записей нужно будет возвращать связанные модели и генерируемые поля (URL и т.п.). Так что разница будет небольшая.

Ответить

 

Геннадий

Что ничего лишнего не рендерится это понятно. Видимо, мне нужно было сразу сказать, что выводятся у меня не точно такие же посты как у Вас, а совсем другие данные. Так вот в этих данных, если делать через renderPartial, больше html кода, чем самих данных. Один элемент(аналог Вашего поста) у меня занимает 23 строки(примерно одинакового размера). Из них только 3 строки это данные с сервера, а остальное верстка. Относительно нагрузки на сервер у меня для одного элемента получается выбор: либо в JSON-е отправить эти 3 строки и обернуть их в верстку js-ом, либо грузить все 23 строки сразу с сервера, подождав пока отработает рендеринг. Теперь понимаете, что я имею в виду?

Ответить

 

Дмитрий Елисеев

Теперь понятно.

Ответить

 

Andrey

Добрый день,

В документации указано. что можно создавать Action, указав actionId:

http://www.yiiframework.com/doc/api/1.1/CController#createAction-detail

Но тут ничего не сказано о том, как передать туда параметры...

У меня 3 вопроса:

1. Можно ли сделать то, что я описал выше?
2. Есть ли более лояльный способ ajax переключения с actionCreate на actionUpdate. Обязательна поддержка загрузки картинок. Дело в том, что некоторые блоки формы должные открываться только после сохранения основной модели.
3. Возможно это лучше провернуть подменяя сценарии как то ?

Пока у меня такой быдлокод, но место , где я в isAjaxRequest блоке создаю action, не работает:

public function actionCreate()
  {
    if (Yii::app()->request->isAjaxRequest ) 
    {
      $model = new Hotels();
      if(isset($_POST['Hotels']))
      {
        $model->setAttributes($_POST['Hotels'], true);
        $model->save();

            // Вызов чужого контроллера и action 
     $p = Yii::app()->createController('sadmin/hotels'); // /update
            $act = $p[0]->createAction('update', ['id'=>$model->id]);
            $act->run();
      Yii::app()->end();
    }
    else
    {
      $model = new Hotels();
      $this->render('create',
        [
          'model'=>$model,
        ]);
    }
Ответить

 

Andrey

Нашел:

Yii::app()->runController('sadmin/hotels/update/id/1416');


Только почему то не работает в isAjaxRequest, только в основном блоке экшена.

Почему-то данный метод отрабатывает в основном теле экшена, но при isAjaxRequest ничего не генерируется(

public function actionCreate()
  {
    if (Yii::app()->request->isAjaxRequest 
    {
                        // Тут не работает  = (((
      Yii::app()->runController('sadmin/hotels/update/id/1416');
      Yii::app()->end();
    }
    else
    {
      $model = new Hotels();

                        // тут работает
      Yii::app()->runController('sadmin/hotels/update/id/1416');

      $this->render('create', ['model'=>$model,]);
    }
  }
Ответить

 

Дмитрий Елисеев

Не пробовал, так что не знаю. Если так и не получится, то попробуйте вынести код из actionUpdate в обычный метод и вызывать его здесь.

Ответить

 

Andrey

Буду разбираться тогда, спасибо, что не прошли мимо!

Ответить

 

Дмитрий Елисеев

Дополнил комментарий.

Ответить

 

Andrey

Благодарю, пробую нечто подобное сделать, хочется fullAjax на старости лет)

Ответить

 

Andrey

А есть ли способ собрать action create и action update в один action без костылей вроде input type="hidden" и js-a , который подменяет значение этого поля?

Ответить

 

Andrey

ajaxSubmitButton на success ничего не возвращает:

echo CHtml::ajaxSubmitButton
            (
                $model->isNewRecord ? 'Добавить номера' : 'Обновить',

                Yii::app()->createUrl('sadmin/hotels/create'),
                [
                    'success'=>'js:function(data)
                        {
                            displayMessage(data);
                            $("#hotels-form").remove();
                            $("#crud-hotel").append(data);
                        }',
                    'error'=>'js:function()
                        {
                            alert("Плохо");
                        }',
                ],
                [
                    'id'=>'add-hotel',
                    'class'=>'btn btn-primary btn-lg center-block',
                ]
            );

Сама кнопка точно отрабатывает, если подкинуть в isAjaxRequest какие нибудь die($value), они возвращаются

Ответить

 

Роман

Добрый день. А как такое сделать на Yii2? Что-то не выходит никак сделать)

Ответить

 

Дмитрий Елисеев

В что именно не выходит?

Ответить

 

Shvarz

Вот опять Я, привет Дмитрий.
Вопрос такой , мне нужен пример добавление комментариев с ajax , не обязательно древовидный.
Или статьи на эту тему, искал , но нашел не рабочие.
Подскажи где посмотреть наглядный пример

Ответить

 

Дмитрий Елисеев

Пример jQuery.ajax?

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>