Question

I have a small menu problem. The active tabs do not match the active class. If I disable the static_block cache it works again, so it actually looks like the menu is cached and continues to display the content that is cached regardless of the tab selected.
I tried to put cacheable=false on the block of the topmenu concerned in the default.xml layout but this didn't change anything.
if someone has already had this problem or has a lead...

Edit :
I fixed it quickly, by removing the topmenu from the cache, (if I understood what I did correctly) but as you can see it's not terrible, but it works very well now

<?php

namespace MyNamespace\Indo\Block\Html;

use Magento\Framework\Data\Tree\NodeFactory;
use Magento\Framework\Data\TreeFactory;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\LayoutFactory;
use TemplateMonster\Megamenu\Helper\Data;

class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{


    public $_helper;

    public $_layoutFactory;

    public function __construct(
        Template\Context $context,
        NodeFactory $nodeFactory,
        TreeFactory $treeFactory,
        Data $helper,
        LayoutFactory $layoutFactory,
        array $data = []
    )
    {
        parent::__construct($context, $nodeFactory, $treeFactory, $data);
        $this->_helper        = $helper;
        $this->_layoutFactory = $layoutFactory;
    }

    /**
     * Get block cache life time
     *
     * @return int
     * @since 100.1.0
     */
    protected function getCacheLifetime()
    {
        return 0;
    }
}
Was it helpful?

Solution

From my understanding, there is no perfect solution to this issue. Whatever fix you decide to implement will have its drawbacks.

I think there may also be 2 other techniques that you can use which are ESI (Edge Side Includes) or hole punching. Although I don't have any experience or knowledge about these, so if these are valid techniques for this purpose. I'll let someone else with a better understanding of them mention explain them.

TLDR

If you want the nav to always show the correct active state and don't mind taking a small performance hit, then disable caching on the navigation block and try to keep it as close to the root block as possible.

If you want maximum performance, then disable server-side rendering and hope the visitor has JS enabled.


Overview

In core Magento, there is both server side and client side logic to set the active state on nav items. And by default, both server and client side logic is active for some reason.

Server Side Logic

The issue with using server-side logic for this is that you cannot cache the block since you are going to be caching that active state as well. When you disable caching for the navigation menu, best case scenario is that you only invalidate full page cache, worse case is you invalidate some other blocks such as the header, etc depending on how your theme is set up.

The server side logic can be found in Magento\Theme\Block\Html\Topmenu::_getMenuItemClasses() It uses the is_active and has_active properties on the passed tree node attribute, which are set in Magento\Catalog\Plugin\Block\Topmenu::getCategoryAsArray

Client Side Logic

The downside to this method is, there is no active state marked until the JS loads. And if the client has JS disabled then they will have no active indicator at all.

The function that handles the client-side logic is lib/web/mage/menu.js::_setActiveMenu() the way this function works is that it searches the href of the nav items checking if one matches the current URL.


Solutions

Disable caching

You can disable caching on the navigation block, this will also disable caching on the parent blocks. So if your navigation block is within your header block, your header won't be cached either.

You can do this, by setting the TTL on the catalog.topnav xml node to 0 which is the same as overwriting the block and overwriting the cache lifetime as you have done is your edit.

<reference name="catalog.topnav" ttl="0"/>

Use the client side logic

The other method you have is to disable the active state being rendered on the server side and rely on the client side logic.

Since the Magento\Theme\Block\Html\Topmenu::_getMenuItemClasses() method is private, we cannot use a plugin to modify the result of it. You could overwrite the class using a preference and either

  • call the parent method and remove active and has-active from the final array
  • redeclare the method excluding the logic where it adds the active class.

OTHER TIPS

@arno solution in the original question is working perfectly for me but I wanted to expand a bit on how to implement the code. As well as simplifying the unneeded code.

Create a custom module for the changes

app/code/VendorName/ModuleName/registration.php

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

app/code/VendorName/ModuleName/etc/module.xml

<?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="VendorName_ModuleName" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Theme"/>
        </sequence>
    </module>
</config>

app/code/VendorName/ModuleName/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">
    <preference for="Magento\Theme\Block\Html\Topmenu" type="VendorName\ModuleName\Block\Html\Topmenu" />
</config>

app/code/VendorName/ModuleName/Block/Html/Topmenu.php

<?php

namespace VendorName\ModuleName\Block\Html;

class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{
    /**
     * Get block cache life time
     *
     * @return int
     * @since 100.1.0
     */
    protected function getCacheLifetime()
    {
        return 0;
    }
}

Your solution looks good, but as per my knowledge if you are set CacheLifetime zero then it will disable cache for that page it's working same as like cacheable=false

So I would like to recommend to remove cache programmatically instead of set CacheLifetime zero.

You can clear cache programmatically using below way.

Define constructor – pass Magento\Framework\App\Cache\TypeListInterface and Magento\Framework\App\Cache\Frontend\Pool to your file’s constructor as defined below.

public function __construct(
    Context $context,
    \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList,
    \Magento\Framework\App\Cache\Frontend\Pool $cacheFrontendPool
) {
    parent::__construct($context);
    $this->_cacheTypeList = $cacheTypeList;
    $this->_cacheFrontendPool = $cacheFrontendPool;
}

Now add the following code to the method where you want clear/flush cache

$types = array('config','layout','block_html','collections','reflection','db_ddl','eav','config_integration','config_integration_api','full_page','translate','config_webservice');
foreach ($types as $type) {
    $this->_cacheTypeList->cleanType($type);
}
foreach ($this->_cacheFrontendPool as $cacheFrontend) {
    $cacheFrontend->getBackend()->clean();
}

So in this way you can clean and flush the cache.

Note: In the above code $types= i defined all types of cache. you can define cache type as per your requirement.

There is one another method to clear cache if you don't want to write cache type hardcoded

public function __construct(
    \Magento\Framework\App\Cache\Manager $cacheManager
) {
    $this->cacheManager = $cacheManager;
}

private function whereYouNeedToCleanCache()
{
    $this->cacheManager->flush($this->cacheManager->getAvailableTypes());

    // or this
    $this->cacheManager->clean($this->cacheManager->getAvailableTypes());
}

I hope it helps!

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top