Magento – Magento 2 – Layered Navigation for Custom Product Collection

collection;layered-navigationmagento-2.1magento2

I created a CMS page for products that contain a certain attribute (On Sale Items).
I extended the ListProduct class and added filters to the product collection to achieve the desired results. (modified the _getProductCollection method).

I was able to add the toolbar and pager (with accurate counts).

However, I cannot seem to get the layered navigation to work. I added the default layered navigation classes to my CMS page XML.

It shows the layered navigation for the entire catalog but I know that I have to extend the class or classes that render it to show my custom collection.

I have tried extending the product collection classes in layer.php and navigation.php with no luck (added collection filters).

I know that in Magento 1, you can extend the product list and layer classes(located in list.php, view.php, and layer.php if I remember correctly), add the desired filters, and render the custom classes in the CMS page.

I know that people have come across a situation in which they need to create a Clearance, On Sale, or any other CMS page where a custom product collection is used, and needs layered navigation.

Any ideas on how to achieve this in Magento 2?

Best Answer

I've been trying to achieve the same thing for a long time. I would really like if someone like Amit Bera could help us with this because there are many questions about "how to get layered navigation with custom collection" and not many usefull answers. Maybe this will help you a little and if you do succeed please share your answer.

I was able to get product collection by using layerResolver like this:

Vendor/Module/Controller/Index/Index.php

    public function __construct(
    \Magento\Framework\App\Action\Context $context,
    \Magento\Framework\View\Result\PageFactory $pageFactory,
    \Magento\Catalog\Model\Layer\Resolver $layerResolver
) {
    $this->layerResolver = $layerResolver;
    $this->pageFactory = $pageFactory;
    $this->context = $context;
    parent::__construct($context);
}

public function execute()
{        
    $date = new \Zend_Date();
    $result = $this->pageFactory->create();
    $this->layerResolver->create('search');

    $collection = $this->layerResolver->get()->getProductCollection();
    $collection->('special_price', ['gt'=>0],'left');
    $collection
         ->addAttributeToFilter(
            'special_from_date',
            [
                'or' => [
                    0 => [
                        'date' => true,
                        'to' => $date->get('YYYY-MM-dd').' 23:59:59'],
                    1 => [
                        'is' => new \Zend_Db_Expr('null')
                    ],
                ]
            ],
            'left'
        )->addAttributeToFilter(
            'special_to_date',
            [
                'or' => [
                    0 => [
                        'date' => true,
                        'from' =>  $date->get('YYYY-MM-dd').' 00:00:00'],
                    1 => [
                        'is' => new \Zend_Db_Expr('null')
                    ],
                ]
            ],
            'left'
        );
    $list = $result->getLayout()->getBlock('custom.products.list');
    $list->setProductCollection($collection);
    return $result;
}

To avoid problems with aggregations and buckets I use this:

Vendor/Module/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- To prepare the filterlist for our custom collection which would be passed to the left navigation we need below virtual types for our custom page navigation -->
    <virtualType name="customFilterList" type="Vendor\Module\Model\Layer\FilterList">
        <arguments>
            <argument name="filterableAttributes" xsi:type="object">Vendor\Module\Model\Layer\FilterableAttributeList</argument>
            <argument name="filters" xsi:type="array">
                <item name="attribute" xsi:type="string">Vendor\Module\Model\Layer\Filter\Attribute</item>
                <item name="category" xsi:type="string">Vendor\Module\Model\Layer\Filter\Category</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- once the filter list virtual type is ready we can pass the same to our navigation , I have prepared the virtual type of the core navigation for my custom module and have passed the custom filter list to it -->
    <virtualType name="Vendor\Module\Block\Navigation\Custnavigation" type="Magento\LayeredNavigation\Block\Navigation">
        <arguments>
            <argument name="filterList" xsi:type="object">customFilterList</argument>
        </arguments>
    </virtualType>
</config>

Vendor/Module/Model/Layer/FilterableAttributeList.php

namespace Vendor\Module\Model\Layer;
class FilterableAttributeList extends \Magento\Catalog\Model\Layer\Category\FilterableAttributeList
{
}

Vendor/Module/Model/Layer/FilterList.php

namespace Vendor\Module\Model\Layer;
class FilterList extends \Magento\Catalog\Model\Layer\FilterList
{
}

Vendor/Module/Model/Layer/Filter/Attribute.php

namespace Vendor\Module\Model\Layer\Filter;

class Attribute extends \Magento\Catalog\Model\Layer\Filter\Attribute
{
}

Vendor/Module/Model/Layer/Filter/Category.php

namespace Vendor\Module\Model\Layer\Filter;

class Category extends \Magento\CatalogSearch\Model\Layer\Filter\Category
{
}

Vendor/Module/view/frontend/layout/custompage_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left">
    <body>
        <attribute name="class" value="page-with-filter"/>
        <referenceContainer name="sidebar.main">
            <block class="Vendor\Module\Block\Navigation\Custnavigation" name="catalog.leftnav" before="-" template="Magento_LayeredNavigation::layer/view.phtml">
                <block class="Magento\LayeredNavigation\Block\Navigation\State" name="catalog.navigation.state" as="state" />
                <block class="Magento\LayeredNavigation\Block\Navigation\FilterRenderer" name="catalog.navigation.renderer" as="renderer" template="Magento_LayeredNavigation::layer/filter.phtml"/>
            </block>
        </referenceContainer>
        <referenceContainer name="content">
            <block class="Vendor\Module\Block\Product\CustomList" name="custom.products.list" as="product_list" template="Magento_Catalog::product/list.phtml">
                <container name="category.product.list.additional" as="additional" />
                <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.details.renderers" as="details.renderers">
                    <block class="Magento\Framework\View\Element\Template" as="default"/>
                </block>
                <block class="Magento\Catalog\Block\Product\ProductList\Toolbar" name="product_list_toolbar" template="Magento_Catalog::product/list/toolbar.phtml">
                    <block class="Magento\Theme\Block\Html\Pager" name="product_list_toolbar_pager"/>
                </block>
                <action method="setToolbarBlockName">
                    <argument name="name" xsi:type="string">product_list_toolbar</argument>
                </action>
            </block>
        </referenceContainer>
    </body>
</page>

Vendor/Module/Block/Product/CustomList.php

namespace Vendor\Module\Block\Product;

use Magento\Catalog\Block\Product\ListProduct;
use Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection;

class CustomList extends ListProduct
{
    public function getLoadedProductCollection()
    {
        return $this->_productCollection;
    }

    public function setProductCollection(AbstractCollection $collection)
    {
        $this->_productCollection = $collection;
    }
}

This will give you filtered collection on custompage via avaiable special price today. You will have attributes to filter but i still cant get category filter to work properly ( count number for products in collection and layered navigation doesnt match and not all categories are displayed ). If anyone can provide answer on how to make category filter to work with this or other solutions please help.

Most of this code i found here on stackexchange in questions with layered-navigation tag (believe me when i say i searched a lot).

Hope this will get you somewhere. :)