Adding Ajax Load More Button into Pagination

In this cookbook, we will add a paginated brand list, including an ajax "load more" button to a product list page. After finishing the guide, you will know how to use multiple paginations on one page.

Note

After this change, you will have paginated also /brands-list/ page (front_brand_list route).

Implementation of Brand Pagination

First, we need to extend Shopsys\FrameworkBundle\Model\Product\Brand\BrandRepository by creating BrandRepository.php in /src/Model/Product/Brand, and we add getPaginationResult method.

namespace App\Model\Product\Brand;

use Shopsys\FrameworkBundle\Component\Paginator\QueryPaginator;
use Shopsys\FrameworkBundle\Model\Product\Brand\BrandRepository as BaseBrandRepository;

class BrandRepository extends BaseBrandRepository
{
    /**
     * @param int $page
     * @param int $limit
     * @return \Shopsys\FrameworkBundle\Component\Paginator\PaginationResult
     */
    public function getPaginationResult(
        $page,
        $limit
    ) {
        $queryBuilder = $this->getBrandRepository()->createQueryBuilder('b');
        $queryBuilder->orderBy('b.name', 'asc');

        /** @var \Shopsys\FrameworkBundle\Component\Paginator\QueryPaginator $queryPaginator */
        $queryPaginator = new QueryPaginator($queryBuilder);

        return $queryPaginator->getResult($page, $limit);
    }
}

Then we will extend Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade and add getPaginatedResult method.

namespace App\Model\Product\Brand;

use Shopsys\FrameworkBundle\Component\Paginator\PaginationResult;
use Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade as BaseBrandFacade;

class BrandFacade extends BaseBrandFacade
{
    /**
     * @param int $page
     * @param int $limit
     * @return \Shopsys\FrameworkBundle\Component\Paginator\PaginationResult
     */
    public function getPaginatedResult(
        $page,
        $limit
    ) {
        $paginationResult = $this->brandRepository->getPaginationResult(
            $page,
            $limit
        );

        return new PaginationResult(
            $paginationResult->getPage(),
            $paginationResult->getPageSize(),
            $paginationResult->getTotalCount(),
            $paginationResult->getResults()
        );
    }
}

After we have extended BrandRepository and BrandFacade, we need to set them to be used instead of the framework classes in our application. This is done via configuration in services.yaml.

Shopsys\FrameworkBundle\Model\Product\Brand\BrandRepository: '@App\Model\Product\Brand\BrandRepository'

Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade: '@App\Model\Product\Brand\BrandFacade'

Next, we will modify the brand list twig template, replacing the whole content and creating a twig template for rendering paging controls and paginated items via ajax, where we move the original content from the brand list template. We will use paginator.loadMoreButton(paginationResult, url('front_brand_list'), pageQueryParameter) twig component that will asynchronously load the next page when the user clicks on its button. We will also define pageQueryParameter variable so it will have a unique name and it will not interfere with other paging components on the same page.

{# templates/Front/Content/Brand/list.html.twig #}
{% extends 'Front/Layout/layoutWithoutPanel.html.twig' %}

{% block title %}
    {{ 'Brand overview'|trans }}
{% endblock %}

{% block main_content %}
    <div>
        <h1>{{ 'Brand overview'|trans }}</h1>
        {% include 'Front/Content/Brand/ajaxList.html.twig' with {paginationResult: paginationResult} %}
    </div>
{% endblock %}

There are two important CSS classes that must be used.

  • js-list-with-paginator - element with this class encapsulates paging component
  • js-list - fragment from which new items are pulled during an asynchronous call
{# templates/Front/Content/Brand/ajaxList.html.twig #}
{% import 'Front/Inline/Paginator/paginator.html.twig' as paginator %}
{% set entityName = 'brands'|trans %}
{% set pageQueryParameter = 'brandPage' %}

<div>
    <div class="js-list-with-paginator">
        {{ paginator.paginatorNavigation(paginationResult, entityName, pageQueryParameter) }}
        <ul class='list-images js-list'>
            {% for brand in paginationResult.results %}
                <li class="list-images__item">
                    <a href="{{ url('front_brand_detail', { id: brand.id }) }}" class="list-images__item__block list-images__item__block--with-label">
                        {{ image(brand, { alt: brand.name }) }}
                        <span>{{ brand.name }}</span>
                    </a>
                </li>
            {% endfor %}
        </ul>
        <div class="text-center margin-bottom-20">
            {{ paginator.loadMoreButton(paginationResult, url('front_brand_list'), pageQueryParameter) }}
        </div>
        {{ paginator.paginatorNavigation(paginationResult, entityName, pageQueryParameter) }}
    </div>
</div>

After that, we will modify listAction method in BrandController so Brand list page will be paginated, and we will be able to integrate it into another list page with other paginated items. We will also implement constants for page query parameter const PAGE_QUERY_PARAMETER = 'brandPage' and for the count of items on one page const ITEMS_PER_PAGE = 5;.

To determine whether the request for the brand list is called from the template, we need to add the dependency on Symfony\Component\HttpFoundation\RequestStack into our BrandController.

const PAGE_QUERY_PARAMETER = 'brandPage';
const ITEMS_PER_PAGE = 5;

/**
 * @var \Symfony\Component\HttpFoundation\RequestStack
 */
private $requestStack;

/**
 * @param \Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade $brandFacade
 * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
 */
public function __construct(
    BrandFacade $brandFacade,
    RequestStack $requestStack
) {
    $this->brandFacade = $brandFacade;
    $this->requestStack = $requestStack;
}

/**
 * @param \Symfony\Component\HttpFoundation\Request $request
 */
public function listAction(Request $request)
{
    // check whether the request is called directly via route or via the Twig template
    $isMainRequest = $this->requestStack->getMainRequest() === $request;

    if ($request->isXmlHttpRequest() || !$isMainRequest) {
        $template = 'Front/Content/Brand/ajaxList.html.twig';
    } else {
        $template = 'Front/Content/Brand/list.html.twig';
    }

    $requestPage = $request->get(self::PAGE_QUERY_PARAMETER);
    $page = $requestPage === null ? 1 : (int)$requestPage;

    return $this->render($template, [
        'paginationResult' => $this->brandFacade->getPaginatedResult($page, self::ITEMS_PER_PAGE),
    ]);
}

Customizing the "load more" button text

By default, the "load more" button displays general text - "Load next X item(s)". The option buttonTextCallback is available for Shopsys.AjaxMoreLoader javascript component that you can use to customize the displayed text to fit your use case. You can see the usage of the option in productList.js.

Integration of Paginated Brand List

We have implemented a paginated Brand list page that can also be loaded from asynchronous calls. We can integrate it into another Product list page that is also paginated with the page query parameter page. We only need to modify the template for the Product page. We will add twig code into main_content block.

{# templates/Front/Content/Product/list.html.twig #}
{{ render(controller('App\\Controller\\Front\\BrandController::listAction')) }}

Conclusion

Customers can see 2 paginated lists with buttons for loading items from the next pages on each Product list page. Since there are unique page query parameters, paginated lists can display different page indexes after the browser is loaded with these page query parameters.