Hey, We are rebranding 🥳 Sneak Peak!

Magento 2 Previous Next Product Navigation

This entry was posted on September 19, 2019 by Željko Ivanović, Magento/PHP Full Stack Web Developer.

Previous Next Cover

Previous Next navigation is a very useful extension that you can implement in your Magento 2 platform. In the following steps, we are going to explain how to build Previous Next product navigation for Magento 2, which is going to work with every case and category on your website. Many people try to make this extension without much trouble, but they end up hitting walls because of Magento 2 cache-in. In order to prevent this from happening, we have found a better way of using layouts and Ajax calls for rendering blocks. In this blog, you will find pieces of code that are responsible for the improved functionality of this extension that is different from the others.

This is how it should look like:

Magento 2 product page

1. Magento 2 product page

 

The structure of the module should be like in the image below. So, the first thing you should do is to make a structure like the one in the following image. The module path should be Vendor/PreviousNextNavigation.

 

Magento 2 Folder Structure

2. Magento 2 folder structure

First of all, your task is to create registration.php file. This file is a must for every module. In the registration.php file you should register your module which will be loaded in the following way:

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_PreviousNextNavigation',
    __DIR__
);

After that, you should create the PrevNextLink.php file in Vendor/PreviousNextNavigation/Block/Catalog/Product/. From this block, we can obtain information about the Previous and Next products with the use of their links.

Using getSortedArray() function defined below in the code you can get products with their positions from the admin backend. The positions are set by the admin in the admin panel. Magento will, by default settings on the category page (sort by position), do all the things needed in order for this to work well. The only thing you need to do is to fetch the already sorted array ($jsonSort = $block->getSortedArray([]), this piece of code with all cookie logic is found below), which you can get and write in the cookie while loading a category page.

        ...
            //if cookies are deleted, get product last category and format the output array
            if (is_null($decodedSortArray)) {
                if (!is_null($categoryIdsArray)) {
                    $lastCatId = end($categoryIdsArray);
                    $decodedSortArray = $this->getProductCollection($lastCatId);
                } 
                return false;
            }

You need to flip the keys of the array because you need them to be sorted in the right order, for example:

10934 = "0" to 0 = 10934

10931 = "1" to 1 = 10931

10938 = "2" to 2 = 10938

$decodedSortArrayFlipped = array_flip($decodedSortArray);

On many occasions, you need to check if the variable exists, and to be careful not to get undefined offset. Another possibility would be to wrap it in and try to catch the block.

In the code below, $decodedSortArray[$decodedSortArray[$prod_id]+1], you can get Next product from the $decodedSortArray variable which can be retrieved from the cookie. Then you form an url for that link by using setUrlDataObject function.

	...
            //get next id based on position, example current prod: 10931 position 1, and next position is 2, $productId is 10938
            $productId = $nextId = $decodedSortArrayFlipped[$decodedSortArray[$prodId] + 1];
            $product = $this->productRepository->getById($productId);
            $product->setCategoryId($category->getId());
            $urlData = $this->catalogUrl->getRewriteByProductStore([$product->getId() => $category->getStoreId()]);
            if (isset($urlData[$product->getId()])) {
                $product->setUrlDataObject(new \Magento\Framework\DataObject($urlData[$product->getId()]));
            }
	...
    //get previous product function
    public function getPreviousProduct($prodId, $catId, $decodedSortArray)
    {
        $categoryIdsArray = null;
        if ($catId != 0) {
            $category = $this->categoryRepository->get($catId);
        } else {
            //if category empty- cookie got deleted or some other reason, get first category
            $product = $this->productRepository->getById($prodId);
            $categoryIdsArray = $product->getCategoryIds();
            if (isset($categoryIdsArray[0])) {
                $category = $this->categoryRepository->get($categoryIdsArray[0]);
            } 
             return false;
        }
        if ($category) {
            //if cookies are deleted, get product last category and format the output array
            if (is_null($decodedSortArray)) {
                if (!is_null($categoryIdsArray)) {
                    $lastCatId = end($categoryIdsArray);
                    $decodedSortArray = $this->getProductCollection($lastCatId);
                } 
                return false;
            }

You need to flip the keys of the array again, for example from:

10934 = "0" to 0 = 10934

10931 = "1" to 1 = 10931

10938 = "2" to 2 = 10938


$decodedSortArrayFlipped = array_flip($decodedSortArray);

In the code below, $decodedSortArray[$decodedSortArray[$prod_id]-1], you can get Previous product from the $decodedSortArray variable which can be retrieved from the cookie. Then you form an url for that link by using setUrlDataObject function.


$productId = $prevId = $decodedSortArrayFlipped[$decodedSortArray[$prod_id] -1];

Get the product category ID from the cookie, or, in case the cookie gets deleted, get the first category. You need to get the category from the registry, provided it is set. If it is set, you will get the category ID and name. On many occasions, the registry will return empty data, and because of that, you need to make sure that you get some data. Otherwise, you will take the first category.

With $prodListDir and $prodListOrder you will get the array of data with products from the post. We already fetched this data.

     /**
     * get product category id
     *
     * @return array
     */
    public function getProdCategoryId()
    {
        $returnProdCatId = {};
        $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
        $category = $objectManager->get('Magento\Framework\Registry')->registry('current_category');//get current category
        $catId = 0;
        $catName = 0;
        if (isset($category)) {
            $catId = $category->getData("entity_id");
            $catName = $category->getData("name");
        }
        //get direction from post from cookie
        $prodListDir = $this->getRequest()->getParam("product_list_dir");
        if (is_null($prodListDir)) {
            $prodListDir = 'asc';
        }
        //get type of list order, position, price, name etc
        $prodListOrder = $this->getRequest()->getParam("product_list_order");
        if (is_null($prodListOrder)) {
            $prodListOrder = 'first_time';
        }
        $returnProdCatId["direction"] = $prodListDir;
        $returnProdCatId["order"] = $prodListOrder;
        $returnProdCatId["cat_id"] = $catId;
        $returnProdCatId["cat_name"] = $catName;

        return $returnProdCatId;
    }

Using the function getLoadedProductCollection you are getting Magento default collection of products which is loaded on the category page when you visit it. You go through it and add it to $sortArray array.

    /**
     * get sorted array based on preloaded product collection
     *
     * @param $productCollection
     * @return false|string|null
     */
    public function getSortedArray($productCollection)
    {
        $jsonSort = null;

        if ($this->getLayout()->getBlock('category.products.list')) {
            $productCollection = $this->getLayout()->getBlock('category.products.list')->getLoadedProductCollection();
            //get sort array previous next
            $sortArray = {};
            $countArr = 0;
            //format product collection
            foreach ($productCollection->getItems() as $keyColl => $singleProd) {
                $sortArray[$keyColl] = (string) $countArr;
                $countArr++;
            }
            $jsonSort = json_encode($sortArray);
            //end of get sort array previous next

        }
        return $jsonSort;
    }
}

Your next step is to create the file Index.php in Vendor/PreviousNextNavigation/Controller/Index/. This controller is crucial for rendering the previous/next block with Ajax on the product page.

You will need to get the data from Ajax, and this piece of code below is product and category id that is retrieved from Ajax response:

$prodId = $dataFromAjax["prod_id"] ?? 0;
$catId = $dataFromAjax["cat_id"] ?? 0;

When retrieving variables please use ?? operator.

Further exists the "??" (or null coalescing) operator, available as of PHP 7.

Example:

<?php
// Example usage for: Null Coalesce Operator
$action = $_POST['action'] ?? 'default';

// The above is identical to this if/else statement
if (isset($_POST['action'])) {
    $action = $_POST['action'];
} else {
    $action = 'default';
}
?> 

The expression (expr1) ?? (expr2) evaluates to expr2 if expr1 is NULL, and expr1 otherwise. In particular, this operator does not emit a notice if the left-hand side value does not exist, just like isset(). This is especially useful on array keys.

Note: Please note that the null coalescing operator is an expression and that it doesn't evaluate to a variable, but to the result of an expression. This is important to know if you want to return a variable by reference. The statement return $foo ?? $bar; in a return-by-reference function will therefore not work and a warning is issued.

With the line below you can get a decoded sort array.


$decodedSortArray = json_decode($json_sorting_array, true);

With the following code, you call previous and next function from the block and return it to $outputHtml to render it on the page.

            $nextProd = $this->vendornameblock->getNextProduct($prodId, $catId, $decodedSortArray);
            $prevProd = $this->vendornameblock->getPreviousProduct($prodId, $catId, $decodedSortArray);
            $outputHtml = '';
            if($prevProd) {
                $outputHtml .= "<a class='prev_url_a' href='" . $prevProd->getProductUrl() . "' title='" . $prevProd->getName() . "'>";
                $outputHtml .= "<div class='prev'>";

Font awesome tag fa-angle-left is responsible for displaying left arrow on the page. It is not necessary for the functionality, but it sure looks good.

                $outputHtml .= "<i class='fas fa-angle-left'></i>";
                $outputHtml .= "<div class='prev-content'>";
                $outputHtml .= "<span>" . $prevProd->getName() . "</span>";
                $outputHtml .= "</div>";
                $outputHtml .= "</div>";
                $outputHtml .= "</a>";
            }
            if($nextProd) {
                $outputHtml .= "<a class='next_url_a' href='" . $nextProd->getProductUrl() . "' title='" . $nextProd->getName() . "'>";
                $outputHtml .= "<div class='next'>";
                $outputHtml .= "<i class='fas fa-angle-right'></i>";
                $outputHtml .= "<div class='prev-content'>";
                $outputHtml .= "<span>".$nextProd->getName()."</span>";
                $outputHtml .= "</div>";
                $outputHtml .= "</div>";
                $outputHtml .= "</a>";
            }
            return $this->jsonResponse($outputHtml);

        } catch (\Magento\Framework\Exception\LocalizedException $e) {
            return $this->jsonResponse($e->getMessage());
        } catch (\Exception $e) {
            $this->_logger->critical($e);
            return $this->jsonResponse($e->getMessage());
        }
    }
 
}

It goes without saying that you have already created the file module.xml in Vendor/PreviousNextNavigation/etc/

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

The next thing to be done is to create routes.xml file in Vendor/PreviousNextNavigation/etc/frontend/. This file is required for a controller, in order to catch data sent with Ajax.

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
	<router id="standard">
		<route frontName="vendor_previousnextnavigation" id="vendor_previousnextnavigation">
			<module name="Vendor_PreviousNextNavigation"/>
		</route>
	</router>
</config>

In Vendor/PreviousNextNavigation/view/frontend/ you should create two folders. You should name the first one Layout and the second one Templates. When this is done, you can create two files in the Layout folder (Vendor/PreviousNextNavigation/view/frontend/layout/).

The first file catalog_category_view.xml with the content mentioned below. This will add content from phtml to the category page, so you can get it and set the category preloaded product collection, which is already sorted. It is better to render it after all of the elements with attribute after=”-”, at the end of the document body, so it will get all of the required data.

        <referenceContainer name="content">
            <block class="Vendor\PreviousNextNavigation\Block\Catalog\Product\PrevNextLink" name="category.products.list.custom" as="custom_name" template="Vendor_PreviousNextNavigation::catalog/product/list_custom.phtml" after="-" />
        </referenceContainer>

It will load list_custom.phtml at the end of the list.phtml no matter if it was extended by other modules or not. From this layout, you can get some crucial pieces of information crucial for loading and displaying Previous-Next products.

The second file is catalog_product_view.xml where you load product links in the product view page.

    <referenceContainer name="content">
        <block template="Vendor_PreviousNextNavigation::catalog/product/prev_next_link.phtml" class="Vendor\PreviousNextNavigation\Block\Catalog\Product\PrevNextLink" name="sr_product_prev_link" before="-">
        </block>
    </referenceContainer>

In templates folder you should create two files at the path:
Vendor/PreviousNextNavigation/view/frontend/templates/catalog/product/.

The first file is list_custom.phtml with its content below. Once this file is loaded at the end of list.phtml it will gather information about the product collection array and the product category ID. All of this data will be stored in the cookie, which is going to be used in the block.

<?php
//get sorted array from preloaded collection from list phtml
$jsonSort = $block->getSortedArray([]);
//get product category id from block
$catidBlockData = $block->getProdCategoryId();

?>
<script type="text/javascript">
    require([
        'jquery',
        'jquery/jquery.cookie'
    ], function(jQuery){
        var $j = jQuery.noConflict();
        $j(document).ready(function () {

Lines below are responsible for setting the cookie with the category name and ID, products order, sorted array of products' data, and product sorting direction. Cookies are playing an important part in storing and returning data. Cookie arguments are:

Name: The name of the cookie which will be used for collecting data

Value: The value of the cookie

Path: Define the path where the cookie is valid. By default, the path of the cookie is the path of the page where the cookie was created (standard browser behavior). If you want to make it available, for instance, across the entire domain use path: '/'. Default: the path of the page where the cookie was created.

            <?php if($catidBlockData["order"]) { ?>
            $j.cookie('product_list_order', '<?php echo $catidBlockData["order"]; ?>', { path: "/"}); // Set Cookie Value
            <?php } ?>
            <?php if($jsonSort) { ?>
            $j.cookie('sort_array', '<?php echo $jsonSort; ?>', { path: "/"}); // Set Cookie Value
            <?php } ?>
            $j.cookie('product_list_dir', '<?php echo $catidBlockData["direction"]; ?>', { path: "/"}); // Set Cookie Value
        });
    });
</script>

The second and the last file is prev_next_link.phtml. In this file, Ajax is called when the file is loaded, returning all data in order to display the links.

<?php
//get id of current product
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$currentProduct = $objectManager->get('Magento\Framework\Registry')->registry('current_product');//get current product information
$blockProdId = $currentProduct->getId();
?>

Send category and product data through the controller with Ajax. After rendering data, you need to append the response that you got from Ajax to element which is already displayed on the product view. Remember that you need to do styling for this element, so it will be in consistence with the current design.

<script type="text/javascript">
    require(["jquery"],function($) {
        $(document).ready(function() {
            var customurl = "<?php echo $this->getUrl().'vendor_previousnextnavigation/index/index'?>";
            var catIdCookie = getCookie('cat_id');
            var jsonSortArray = getCookie('sort_array');
            //send ajax to customurl above
            //get from cookie - category id, product id, and array of products - jsonSortArray
            $.ajax({
                url: customurl,
                type: 'POST',
                dataType: 'json',
                data: {
                    cat_id: catIdCookie,
                    prod_id: <?php echo $blockProdId; ?>,
                    json_sort_array: jsonSortArray
                },
                complete: function(response) {
                    var responseObj = response.responseJSON;
                    //append response to the .previous_next div
                    $(document).find(".previous_next").append(responseObj);
                },
                error: function (xhr, status, errorThrown) {
                    console.log('Error with previous/next ajax. Try again.');
                }
   });
        });

This is a simple function from which you can get the cookie based on the name:

        //get cookie by name function
        function getCookie(cname) {
            var name = cname + "=";
            var decodedCookie = decodeURIComponent(document.cookie);
            var ca = decodedCookie.split(';');
            for(var i = 0; i <ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') {
                    c = c.substring(1);
                }
                if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
            return "";
        }
    });
</script>

If you are sure that you have created all of the necessary files, run the following commands:

php bin/magento setup:upgrade
php bin/magento setup:static-content:deploy -f
php bin/magento cache:flush

Now it is time to refresh the page and see the results. This extension works every time, no matter in which category/subcategory the products are placed. Every frontend sorting settings, Magento default ones: position, price, name, etc. are going to be applied to the product’s array and displayed properly.

If you want to buy a fully functional module, please contact us at [email protected]

 

Reference:

https://github.com/carhartl/jquery-cookie#readme 

https://php.net/manual/en/language.operators.comparison.php

This entry was posted in Magento 2 and tagged Web Development, eCommerce platform, SyncIt Group, Magento 2, Web, Magento 2 Development, Magento 2 Extensions, Previous Next, Previous Next product navigation, eCommerce, eCommerce Development, Magento 2 Community on September 19, 2019 by Željko Ivanović, Magento/PHP Full Stack Web Developer .