AJAX Pagination

There are two styles. Method one is pretty JS heavy, while the other relies on more server-side processing.

Either way, you'll need to enable the client module. We'll be creating or updating the clients SectionInterceptor beforeRender method.

Method One

This method has the advantage of being mostly limited to the template - making it pretty clear what it's doing.

SectionInterceptor.php:

use Ceo\Http\Response;

class SectionInterceptor extends \Ceo\Compat\Interceptors\SectionInterceptor
{
    public function beforeRender($params = [])
    {
        $params = parent::beforeRender($params);

        // looking for 'ajax' query param, load the dynamic_load.tpl partial
        if ($this->request->getQuery('ajax')) {
            $template = 'gryphon/section/_dynamic_load.tpl';
            $partial = $this->getDI()->getTwigPartial();
            $content = $partial->render($template, $params);

            $resp = new Response;
            $resp->setContent($content);

            return $resp;
        }

        return $params;
    }
}

And in your section template:

{# display articles #}
{% include 'gryphon/section/_dynamic_load.tpl' with {'articles':articles} %}

{# build pagination #}
{% set allPages     = pagination.getPages() %}
{% set maxInt       = allPages|length - 1%}
{% set nextPage     = pagination.getNext().url() %}
{% set nextInt      = pagination.getNext().parameters().page %}

{% set prevPage     = pagination.getPrevious().url() %}
{% set prevInt      = pagination.getPrevious().parameters().page %}

{% set activeInt    = nextInt - 1 %}
{% set urlArr       = nextPage|split('?') %}
{% set baseUrl      = urlArr|length ? urlArr|first : request.urlString()|url %}
<div class="clearfix visible-xs"></div>
<a class ="more" data-trigger="load-more" href="{{ baseUrl }}" data-page="{{nextInt}}" data-max="{{maxInt}}">See More {{ section.name }}</a>

...

{# handle loading #}
<script type="text/javascript">
    $(document).on('click', '[data-trigger="load-more"]:not(.loading)', function(event) {
        event.preventDefault();
        var loadTarget  = $('#article-load'),
            loadingEle  = $('<div class="load-bar-md"></div>')
            nextPage    = parseInt($(this).attr('data-page')),
            activePage  = nextPage - 1,
            prevPage    = activePage > 0 ? (activePage - 1) : 0,
            urlRaw      = $(this).attr('href'),
            queryData   = {'page':nextPage, 'ajax':1},
            queryStr    = $.param(queryData),
            loadUrl     = urlRaw + '/.html' + '?' + queryStr,
            loadData    = {'page':nextPage, 'ajax':1},
            loadDest    = $('<div></div>').appendTo('body'),
            $this       = $(this);
        // Add the loading class to the load trigger to prevent loading of more
        // articles prior to the response being sent & returned
        $this.addClass('loading');
        $.post(loadUrl, loadData, function(data) {
            // console.log(data);
            // Increment the load 'page' value by 1
            $this.attr('data-page', nextPage + 1);
            // Append the content to the article list
            loadTarget.append(data);
            // Remove the loading class to allow
            $this.removeClass('loading');
            if ($('.article-loader-complete').length) {
                $this.replaceWith($('.article-loader-complete'));
            }
        });
    });
</script>

Method Two

This method's advantage is that it uses the actual pagination as its placeholder, making it work even if the javascript doesn't fire. This makes it a great fallback method. The downside is that it's a bit more opaque than the previous method.

SectionInterceptor.php:

use Ceo\Http\Response;

class SectionInterceptor extends \Ceo\Compat\Interceptors\SectionInterceptor
{
    public function beforeRender($params = [])
    {
        $params = parent::beforeRender($params);

        // if it has the '_il' (for infinite loader) parameter
        if ($this->request->hasQuery('_il')) {
            $template = 'gryphon/section/dynamic-load.tpl';

            $twig = $this->getDI()->getTwigPartial();
            $content = $twig->render($template, $params);

            $hasMore = false;
            $max = 20;

            if ($params['articles']->length() >= $max) {
                $hasMore = true;
            }

            $data = array(
                'content' => base64_encode($content),
                'hasMore' => $hasMore
            );

            $resp = new Response;
            $resp->asJson();
            $resp->setContent(json_encode($data));
            return $resp;
        }

        return $params;
    }
}

Then, in your template:

<div class="row pagination-container has-infinite-loader">
    <div class="col-sm-8 col-sm-offset-2">
        <nav class="justified">
            <ul class="pagination">
                {% for page in pagination %}
                    <li><a href="{{ page.url }}">{{ page.label }}</a></li>
                {% endfor %}
            </ul>
        </nav>
    </div>
</div>

Finally, add the following to the master Javascript file:

$(function() {
    if ($('.has-infinite-loader').length === 0) {
        return;
    }

    $.fn.isInViewport = function() {
        var elementTop = $(this).offset().top;
        var elementBottom = elementTop + $(this).outerHeight();
        var viewportTop = $(window).scrollTop();
        var viewportBottom = viewportTop + $(window).height();
        return elementBottom > viewportTop && elementTop < viewportBottom;
    };

    // initialize vars
    var isFetching = false;
    var page = 1;

    var indicator = $('<div class="spinner pagination-loader"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>');
    // replace the pagination links with a loading indicator
    $('.pagination-container .pagination').replaceWith(indicator.clone());

    $(window).on('resize scroll', function() {
        if ($('.pagination-container .pagination-loader').isInViewport() && !isFetching) {
            isFetching = true;
            var loc = window.location.origin + window.location.pathname;
            page = page + 1;

            $.getJSON(loc + '.json', {page: page, _il: 1}, function(resp) {
                var data = atob(resp.content);

                // inject the content before the pagination
                $(data).insertBefore('.pagination-container');

                // is there more?
                if (!resp.hasMore) {
                    $('.pagination-loader').remove();
                }

                // turn the flag off so we can load more
                isFetching = false;
            });
        }
    });
});