Orderby - Better Product Sorting in VirtueMart – Cache-Free, Accurate via AJAX

Started by hazael, May 21, 2025, 16:39:49 PM

Previous topic - Next topic

hazael

The default product sorting mechanism in VirtueMart has several major flaws:
It doesn't accurately display which sorting method is currently applied – neither the default set in the VirtueMart configuration nor the one selected by the user.

When users change categories, their chosen sorting is preserved in the session, but the sorting dropdown misleadingly displays an incorrect value.
It's affected by page caching, which may lead to inconsistent UI and UX.

I created a workaround that solves all of these issues. Key advantages of my solution:
Sorting is handled via AJAX – fully independent of page cache, always reflecting the actual state.
Accurately shows the active sorting method – whether default or user-selected.
Stores the value in a cookie – which allows sorting preference to persist across categories without relying on session hacks.
SEO-friendly – since the AJAX-generated output isn't indexed by Google, it avoids duplicate content issues and preserves crawl budget.
Server-friendly – bots and crawlers won't overload the server with unnecessary requests since the mechanism works in the background.

If you're struggling with unreliable or misleading sorting behavior in VirtueMart, I highly recommend this approach. It's a lightweight plugin + template override that works reliably on Joomla 5 and VM 4.x.

Happy to share the code and logic if anyone's interested  :)

You should create a plugin in the folder

/plugins/ajax/vmordderby/vmorderby.php
<?php
defined
('_JEXEC') or die;
use 
Joomla\CMS\Factory;
use 
Joomla\CMS\Plugin\CMSPlugin;
use 
Joomla\CMS\Router\Route;
class 
PlgAjaxVmorderby extends CMSPlugin
{
    public function 
onAjaxVmorderby()
    {
        
// Download data from Request
        
$input \Joomla\CMS\Factory::getApplication()->input;
        
$virtuemart_category_id $input->getInt('virtuemart_category_id');
        
$virtuemart_manufacturer_id $input->getInt('virtuemart_manufacturer_id'0);
        
$itemid $input->getInt('Itemid');
        
// Load VM configuration
        
require_once JPATH_ADMINISTRATOR '/components/com_virtuemart/helpers/config.php';
        
VmConfig::loadConfig();
        
$lang Factory::getLanguage();
        
$lang->load('com_virtuemart'JPATH_SITEnulltrue);
        
$fields VmConfig::get('browse_orderby_fields');
        
$orderbyRaw $input->getString('orderby');
        
$orderby $orderbyRaw?: ($_COOKIE['vm_orderby'] ?? VmConfig::get('browse_orderby_field'));
        
$orderDir VmConfig::get ('prd_brws_orderby_dir');
        
$orderDirTxt vmText::('COM_VIRTUEMART_SEARCH_ORDER_'.$orderDir);
        
// If you have a new sorting by Get, you will overwrite the cookie
        
if ($orderbyRaw) {
            
$normalizedOrderby preg_replace('/^`?p`?\./'''strtolower($orderbyRaw));
            
$normalizedOrderby str_replace('`'''$normalizedOrderby);
            
setcookie('vm_orderby'$normalizedOrderbytime() + 300'/');
        }
        
$clean str_replace(['`p`.''p.''`''.'], ''strtolower($orderby));
        
$displayText vmText::_('COM_VIRTUEMART_SEARCH_ORDER_' strtoupper($clean));
        echo  
htmlspecialchars($displayText)
            . 
'<span style="display:none" id="orderby">' htmlspecialchars($clean) . '</span>'
            
'<span class="sort-icon text-muted ms-2">'.$orderDirTxt.' <i class="fa fa-sort-amount-up" aria-hidden="true"></i></span>';
        echo 
'<ul class="orderlist" style="display:none;">';
        foreach (
$fields as $field) {
            
$fieldWithoutPrefix strpos($field'.') !== false substr($fieldstrpos($field'.') + 1) : $field;
            
$text vmText::_('COM_VIRTUEMART_SEARCH_ORDER_' strtoupper(str_replace([',',' '], ['_',''], $fieldWithoutPrefix)));
            
$link 'index.php?option=com_virtuemart&view=category'
                
'&orderby=' $fieldWithoutPrefix
                
'&virtuemart_category_id=' $virtuemart_category_id
                
'&virtuemart_manufacturer_id=' $virtuemart_manufacturer_id
                
'&Itemid=' $itemid;
            
$link Route::_($linkfalse);
            echo 
'<li><a href="' htmlspecialchars($link) . '">' htmlspecialchars($text) . '</a></li>';
        }
        echo 
'</ul>';
    }
}

And this xml file
/plugins/ajax/vmordderby/vmorderby.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="ajax" method="upgrade" version="5.0">
    <name>plg_ajax_vmorderby</name>
    <author>Piekielko.com</author>
    <version>1.0.0</version>
    <description>AJAX loader VMorderby by Hazael</description>
    <files>
        <filename plugin="vmorderby">vmorderby.php</filename>
    </files>
</extension>

You can install the created plug from the ZIP package or after creating it in the indicated folder, detect it in the administrator panel
/administrator/index.php?option=com_installer&view=discover
- install it and publish it.


Then, in the Virtuemart template file
/templates/template-name/html/com_virtuemart/sublayouts/orderby.php
Replace the entire code with this

<?php
$categoryId 
vRequest::getInt('virtuemart_category_id'0);
$manufacturerId vRequest::getInt('virtuemart_manufacturer_id'0);
$itemId JFactory::getApplication()->input->getInt('Itemid'0);
$orderby $_GET['orderby'];
?>

<div class="lista"><span id="ajax-orderby" class="activeOrder livesearch d-flex align-items-center justify-content-between"></span></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
    const categoryId = <?php echo (int)$categoryId?>;
    const manufacturerId = <?php echo (int)$manufacturerId?>;
    const itemId = <?php echo (int)$itemId?>;

    const urlParams = new URLSearchParams(window.location.search);
    const orderbyParam = '<?php echo $orderby?>';

    let ajaxUrl = '/index.php?option=com_ajax&plugin=vmorderby&format=raw'
        + '&virtuemart_category_id=' + categoryId
        + '&virtuemart_manufacturer_id=' + manufacturerId
        + '&Itemid=' + itemId;

    if (orderbyParam) {
        ajaxUrl += '&orderby=' + orderbyParam;
    }
    fetch(ajaxUrl)
        .then(res => res.text())
        .then(html => {
            document.getElementById('ajax-orderby').innerHTML = html;

            const selectWrapper = document.querySelector(".activeOrder");
            const selectOptions = document.querySelector(".orderlist");

            document.body.addEventListener("click", function(event) {
                if (!event.target.closest(".lista")) {
                    selectOptions.style.display = "none";
                }
            });
            if (selectWrapper) {
                selectWrapper.addEventListener("click", function() {
                    selectOptions.style.display = (selectOptions.style.display === "block") ? "none" : "block";
                });
            }
        });
});
</script>

My solution is available on this page:
Example 1: https://szeregowe.pl/projekty-domow

After changing sorting, you can go to another category and the check box will display your currently selected sorting regardless of the web cache
Example 2:  https://szeregowe.pl/blizniaki

You cannot view this attachment.

loppan


hazael

THX! 8)

A small fix to the code  ensures that the product list is always generated according to the customer's selection, regardless of server-side page caching.

in orderby.php
<?php
defined
('_JEXEC') or die;
$categoryId vRequest::getInt('virtuemart_category_id'0);
$manufacturerId vRequest::getInt('virtuemart_manufacturer_id'0);
$itemId JFactory::getApplication()->input->getInt('Itemid'0);
$orderby $_GET['orderby'] ?? '';
?>

<div class="lista"><span id="ajax-orderby" class="activeOrder livesearch d-flex align-items-center justify-content-between"></span></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
    const categoryId = <?php echo (int)$categoryId?>;
    const manufacturerId = <?php echo (int)$manufacturerId?>;
    const itemId = <?php echo (int)$itemId?>;
    const orderbyParam = '<?php echo $orderby?>';

    let ajaxUrl = '/index.php?option=com_ajax&plugin=vmorderby&format=raw'
        + '&virtuemart_category_id=' + categoryId
        + '&virtuemart_manufacturer_id=' + manufacturerId
        + '&Itemid=' + itemId;
    if (orderbyParam) {
        ajaxUrl += '&orderby=' + orderbyParam;
    }
    fetch(ajaxUrl)
        .then(res => res.text())
        .then(html => {
            document.getElementById('ajax-orderby').innerHTML = html;

            // Dropdown
            const selectWrapper = document.querySelector(".activeOrder");
            const selectOptions = document.querySelector(".orderlist");
            document.body.addEventListener("click", function(event) {
                if (!event.target.closest(".lista")) {
                    selectOptions.style.display = "none";
                }
            });
            if (selectWrapper) {
                selectWrapper.addEventListener("click", function() {
                    selectOptions.style.display = (selectOptions.style.display === "block") ? "none" : "block";
                });
            }
        });
});
</script>


in vmorderby.php
<?php
defined
('_JEXEC') or die;

use 
Joomla\CMS\Factory;
use 
Joomla\CMS\Plugin\CMSPlugin;
use 
Joomla\CMS\Router\Route;

class 
PlgAjaxVmorderby extends CMSPlugin
{
    public function 
onAjaxVmorderby()
    {
        
$input Factory::getApplication()->input;
        
$user Factory::getUser();
        
$virtuemart_category_id $input->getInt('virtuemart_category_id');
        
$virtuemart_manufacturer_id $input->getInt('virtuemart_manufacturer_id'0);
        
$itemid $input->getInt('Itemid');
        
// Load VirtueMart config
        
require_once JPATH_ADMINISTRATOR '/components/com_virtuemart/helpers/config.php';
        
VmConfig::loadConfig();
        
$lang Factory::getLanguage();
        
$lang->load('com_virtuemart'JPATH_SITEnulltrue);
        
$fields VmConfig::get('browse_orderby_fields');
        
$defaultField VmConfig::get('browse_orderby_field');
        
$orderbyRaw $input->getString('orderby');
        
// Detect potential cache
        
$isLikelyCached = empty($orderbyRaw) && $user->guest;
        if (
$isLikelyCached) {
            
$orderby $defaultField;
            
// Remove the cookie, as the view is already from the cache anyway
            
if (isset($_COOKIE['vm_orderby'])) {
                
setcookie('vm_orderby'''time() - 3600'/');
                unset(
$_COOKIE['vm_orderby']);
            }
        } else {
            
$orderby $orderbyRaw ?: ($_COOKIE['vm_orderby'] ?? $defaultField);
        }
        
// Set the sorting method
        
$orderby $isLikelyCached
            
$defaultField
            
: ($orderbyRaw ?: ($_COOKIE['vm_orderby'] ?? $defaultField));
        
// Save cookie when sorting changes
        
if ($orderbyRaw) {
            
$normalizedOrderby preg_replace('/^`?p`?\./'''strtolower($orderbyRaw));
            
$normalizedOrderby str_replace('`'''$normalizedOrderby);
            
setcookie('vm_orderby'$normalizedOrderbytime() + 300'/');
        }
        
// Process to translation key
        
$clean str_replace(['`p`.''p.''`''.'], ''strtolower($orderby));
        
$translationKey $clean ?: str_replace(['`p`.''p.''`''.'], ''strtolower($defaultField));
        
$displayText vmText::_('COM_VIRTUEMART_SEARCH_ORDER_' strtoupper($translationKey));
        
// Sorting direction
        
$orderDir VmConfig::get('prd_brws_orderby_dir');
        
$orderDirTxt vmText::_('COM_VIRTUEMART_SEARCH_ORDER_' $orderDir);
        
// Link to active sorting
        
$activeLink 'index.php?option=com_virtuemart&view=category'
            
'&orderby=' $clean
            
'&virtuemart_category_id=' $virtuemart_category_id
            
'&virtuemart_manufacturer_id=' $virtuemart_manufacturer_id
            
'&Itemid=' $itemid;
        
$activeLink Route::_($activeLinkfalse);
        
// Main sorting field - You can transfer data from this area to JavaScript for correct pagination on the product list
        
echo htmlspecialchars($displayText)
            . 
'<span style="display:none" id="orderby" data-url="' htmlspecialchars($activeLink) . '">' htmlspecialchars($clean) . '</span>'
            
'<span class="sort-icon text-muted ms-2">' $orderDirTxt ' <i class="fa fa-sort-amount-up" aria-hidden="true"></i></span>';

        
// List of sorting options
        
echo '<ul class="orderlist" style="display:none;">';
        foreach (
$fields as $field) {
            
$fieldWithoutPrefix strpos($field'.') !== false
                
substr($fieldstrpos($field'.') + 1)
                : 
$field;
            
$text vmText::_('COM_VIRTUEMART_SEARCH_ORDER_' strtoupper(str_replace([','' '], ['_'''], $fieldWithoutPrefix)));
            
$link 'index.php?option=com_virtuemart&view=category'
                
'&orderby=' $fieldWithoutPrefix
                
'&virtuemart_category_id=' $virtuemart_category_id
                
'&virtuemart_manufacturer_id=' $virtuemart_manufacturer_id
                
'&Itemid=' $itemid;
            
$link Route::_($linktrue);

            echo 
'<li><a href="' htmlspecialchars($link) . '">' htmlspecialchars($text) . '</a></li>';
        }
        echo 
'</ul>';
    }
}


If the user is logged in, the sorting option displayed is based on the user's selected method. The system reads the orderby value from the URL or from the cookie (vm_orderby), so the displayed product list and the selected sorting option are consistent.

If the user is not logged in, the page is often served from server cache. In that case, if no sorting has been selected, the system always displays the default sorting option defined in VirtueMart settings. If the guest user changes the sorting option, a new URL is generated with the selected method, and the page reloads without cache, correctly reflecting the sorting choice.

Additionally, if a cached page is served and a cookie exists with a different sorting method, the cookie is ignored or deleted. This prevents inconsistencies between the visible product list and the selected sorting option.