Reading list Switch to dark mode

    How to replace magento2 default search suggestion logic

    Updated 5 March 2024

    In my previous article, I have explained how we can replace magento2 default MySQL search engine with any other engines like elastic, solr etc. Now we will see how we can change magento2 search suggestion logic. By default magento2 shows search suggestion by saving each searched term and then on as user types it starts showing those saved search terms according to their popularity, popularity means how many times the term has been searched. So if you have successfully replaced the search engine you will definitely want to use some advanced logic in search suggestions also. So let’s see how we can replace it.

    first, you will like to replace the phtml template that is used to show autocomplete results, create a default.xml file in your modules directory structure app/code/CompanyName/ModuleName/view/frontend/layout/default.xml and add the below code:

    <?xml version="1.0"?>
    <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance dc" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
        <referenceBlock name="top.search">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">CompanyName_ModuleName::form.mini.phtml</argument>
            </action>
        </referenceBlock>
    </page>

    Now you need to create form.mini.phtml in your module path app/code/CompanyName/ModuleName/view/frontend/templates/form.mini.phtml

    the original form.mini.phtml looks like this:

    <?php
    /**
     * Copyright © Magento, Inc. All rights reserved.
     * See COPYING.txt for license details.
     */
    
    // @codingStandardsIgnoreFile
    ?>
    <?php
    /** @var $block \Magento\Framework\View\Element\Template */
    /** @var $helper \Magento\Search\Helper\Data */
    $helper = $this->helper('Magento\Search\Helper\Data');
    ?>
    <div class="block block-search">
        <div class="block block-title"><strong><?= /* @escapeNotVerified */ __('Search') ?></strong></div>
        <div class="block block-content">
            <form class="form minisearch" id="search_mini_form" action="<?= /* @escapeNotVerified */ $helper->getResultUrl() ?>" method="get">
                <div class="field search">
                    <label class="label" for="search" data-role="minisearch-label">
                        <span><?= /* @escapeNotVerified */ __('Search') ?></span>
                    </label>
                    <div class="control">
                        <input id="search"
                               data-mage-init='{"quickSearch":{
                                    "formSelector":"#search_mini_form",
                                    "url":"<?= /* @escapeNotVerified */ $block->getUrl('search/ajax/suggest', ['_secure' => $block->getRequest()->isSecure()]) ?>",
                                    "destinationSelector":"#search_autocomplete"}
                               }'
                               type="text"
                               name="<?= /* @escapeNotVerified */ $helper->getQueryParamName() ?>"
                               value="<?= /* @escapeNotVerified */ $helper->getEscapedQueryText() ?>"
                               placeholder="<?= /* @escapeNotVerified */ __('Search entire store here...') ?>"
                               class="input-text"
                               maxlength="<?= /* @escapeNotVerified */ $helper->getMaxQueryLength() ?>"
                               role="combobox"
                               aria-haspopup="false"
                               aria-autocomplete="both"
                               autocomplete="off"/>
                        <div id="search_autocomplete" class="search-autocomplete"></div>
                        <?= $block->getChildHtml() ?>
                    </div>
                </div>
                <div class="actions">
                    <button type="submit"
                            title="<?= $block->escapeHtml(__('Search')) ?>"
                            class="action search">
                        <span><?= /* @escapeNotVerified */ __('Search') ?></span>
                    </button>
                </div>
            </form>
        </div>
    </div>

    in the above code you can see that it uses a javascript component “quickSearch” to initialize the search autocomplete by providing it some data “formSelector”, “url”, “destination” selector, the url key is used to fetch the autocomplete search results, magento2 uses “search/ajax/suggest”, you can replace it by your own controller path and apply your own logic.

    Searching for an experienced
    Magento 2 Company ?
    Find out More

    Now after replacing the template you must  want to replace, the “quickSearch” component to show more descriptive results or whatever changes you want to do, for this you need to create requirejs-config.js file in your module at this path “app/code/CompanyName/ModuleName/view/frontend/requirejs-config.js”:

    var config = {
        map: {
            '*': {
                quickSearch: 'CompanyName_ModuleName/form-mini'
            }
        }
    };

    magento uses require js and above is require js configuration files using this file we map js components to a key, and that key is used on the frontend to call the component although you can use the file path as well but using file path directly will restrict the component replace from other modules.

    Now doing the above step you need to create the js file in your module path “app/code/CompanyName/ModeulName/view/frontend/web/form-mini.js” the original file looks like this:

    /**
     * Copyright © Magento, Inc. All rights reserved.
     * See COPYING.txt for license details.
     */
    
    /**
     * @api
     */
    define([
        'jquery',
        'underscore',
        'mage/template',
        'matchMedia',
        'jquery/ui',
        'mage/translate'
    ], function ($, _, mageTemplate, mediaCheck) {
        'use strict';
    
        /**
         * Check whether the incoming string is not empty or if doesn't consist of spaces.
         *
         * @param {String} value - Value to check.
         * @returns {Boolean}
         */
        function isEmpty(value) {
            return value.length === 0 || value == null || /^\s+$/.test(value);
        }
    
        $.widget('mage.quickSearch', {
            options: {
                autocomplete: 'off',
                minSearchLength: 2,
                responseFieldElements: 'ul li',
                selectClass: 'selected',
                template:
                    '<li class="<%- data.row_class %>" id="qs-option-<%- data.index %>" role="option">' +
                        '<span class="qs-option-name">' +
                           ' <%- data.title %>' +
                        '</span>' +
                        '<span aria-hidden="true" class="amount">' +
                            '<%- data.num_results %>' +
                        '</span>' +
                    '</li>',
                submitBtn: 'button[type="submit"]',
                searchLabel: '[data-role=minisearch-label]',
                isExpandable: null
            },
    
            /** @inheritdoc */
            _create: function () {
                this.responseList = {
                    indexList: null,
                    selected: null
                };
                this.autoComplete = $(this.options.destinationSelector);
                this.searchForm = $(this.options.formSelector);
                this.submitBtn = this.searchForm.find(this.options.submitBtn)[0];
                this.searchLabel = $(this.options.searchLabel);
                this.isExpandable = this.options.isExpandable;
    
                _.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit');
    
                this.submitBtn.disabled = true;
    
                this.element.attr('autocomplete', this.options.autocomplete);
    
                mediaCheck({
                    media: '(max-width: 768px)',
                    entry: function () {
                        this.isExpandable = true;
                    }.bind(this),
                    exit: function () {
                        this.isExpandable = false;
                        this.element.removeAttr('aria-expanded');
                    }.bind(this)
                });
    
                this.searchLabel.on('click', function (e) {
                    // allow input to lose its' focus when clicking on label
                    if (this.isExpandable && this.isActive()) {
                        e.preventDefault();
                    }
                }.bind(this));
    
                this.element.on('blur', $.proxy(function () {
                    if (!this.searchLabel.hasClass('active')) {
                        return;
                    }
    
                    setTimeout($.proxy(function () {
                        if (this.autoComplete.is(':hidden')) {
                            this.setActiveState(false);
                        } else {
                            this.element.trigger('focus');
                        }
                        this.autoComplete.hide();
                        this._updateAriaHasPopup(false);
                    }, this), 250);
                }, this));
    
                this.element.trigger('blur');
    
                this.element.on('focus', this.setActiveState.bind(this, true));
                this.element.on('keydown', this._onKeyDown);
                this.element.on('input propertychange', this._onPropertyChange);
    
                this.searchForm.on('submit', $.proxy(function () {
                    this._onSubmit();
                    this._updateAriaHasPopup(false);
                }, this));
            },
    
            /**
             * Checks if search field is active.
             *
             * @returns {Boolean}
             */
            isActive: function () {
                return this.searchLabel.hasClass('active');
            },
    
            /**
             * Sets state of the search field to provided value.
             *
             * @param {Boolean} isActive
             */
            setActiveState: function (isActive) {
                this.searchForm.toggleClass('active', isActive);
                this.searchLabel.toggleClass('active', isActive);
    
                if (this.isExpandable) {
                    this.element.attr('aria-expanded', isActive);
                }
            },
    
            /**
             * @private
             * @return {Element} The first element in the suggestion list.
             */
            _getFirstVisibleElement: function () {
                return this.responseList.indexList ? this.responseList.indexList.first() : false;
            },
    
            /**
             * @private
             * @return {Element} The last element in the suggestion list.
             */
            _getLastElement: function () {
                return this.responseList.indexList ? this.responseList.indexList.last() : false;
            },
    
            /**
             * @private
             * @param {Boolean} show - Set attribute aria-haspopup to "true/false" for element.
             */
            _updateAriaHasPopup: function (show) {
                if (show) {
                    this.element.attr('aria-haspopup', 'true');
                } else {
                    this.element.attr('aria-haspopup', 'false');
                }
            },
    
            /**
             * Clears the item selected from the suggestion list and resets the suggestion list.
             * @private
             * @param {Boolean} all - Controls whether to clear the suggestion list.
             */
            _resetResponseList: function (all) {
                this.responseList.selected = null;
    
                if (all === true) {
                    this.responseList.indexList = null;
                }
            },
    
            /**
             * Executes when the search box is submitted. Sets the search input field to the
             * value of the selected item.
             * @private
             * @param {Event} e - The submit event
             */
            _onSubmit: function (e) {
                var value = this.element.val();
    
                if (isEmpty(value)) {
                    e.preventDefault();
                }
    
                if (this.responseList.selected) {
                    this.element.val(this.responseList.selected.find('.qs-option-name').text());
                }
            },
    
            /**
             * Executes when keys are pressed in the search input field. Performs specific actions
             * depending on which keys are pressed.
             * @private
             * @param {Event} e - The key down event
             * @return {Boolean} Default return type for any unhandled keys
             */
            _onKeyDown: function (e) {
                var keyCode = e.keyCode || e.which;
    
                switch (keyCode) {
                    case $.ui.keyCode.HOME:
                        this._getFirstVisibleElement().addClass(this.options.selectClass);
                        this.responseList.selected = this._getFirstVisibleElement();
                        break;
    
                    case $.ui.keyCode.END:
                        this._getLastElement().addClass(this.options.selectClass);
                        this.responseList.selected = this._getLastElement();
                        break;
    
                    case $.ui.keyCode.ESCAPE:
                        this._resetResponseList(true);
                        this.autoComplete.hide();
                        break;
    
                    case $.ui.keyCode.ENTER:
                        this.searchForm.trigger('submit');
                        break;
    
                    case $.ui.keyCode.DOWN:
                        if (this.responseList.indexList) {
                            if (!this.responseList.selected) {  //eslint-disable-line max-depth
                                this._getFirstVisibleElement().addClass(this.options.selectClass);
                                this.responseList.selected = this._getFirstVisibleElement();
                            } else if (!this._getLastElement().hasClass(this.options.selectClass)) {
                                this.responseList.selected = this.responseList.selected
                                    .removeClass(this.options.selectClass).next().addClass(this.options.selectClass);
                            } else {
                                this.responseList.selected.removeClass(this.options.selectClass);
                                this._getFirstVisibleElement().addClass(this.options.selectClass);
                                this.responseList.selected = this._getFirstVisibleElement();
                            }
                            this.element.val(this.responseList.selected.find('.qs-option-name').text());
                            this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
                        }
                        break;
    
                    case $.ui.keyCode.UP:
                        if (this.responseList.indexList !== null) {
                            if (!this._getFirstVisibleElement().hasClass(this.options.selectClass)) {
                                this.responseList.selected = this.responseList.selected
                                    .removeClass(this.options.selectClass).prev().addClass(this.options.selectClass);
    
                            } else {
                                this.responseList.selected.removeClass(this.options.selectClass);
                                this._getLastElement().addClass(this.options.selectClass);
                                this.responseList.selected = this._getLastElement();
                            }
                            this.element.val(this.responseList.selected.find('.qs-option-name').text());
                            this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
                        }
                        break;
                    default:
                        return true;
                }
            },
    
            /**
             * Executes when the value of the search input field changes. Executes a GET request
             * to populate a suggestion list based on entered text. Handles click (select), hover,
             * and mouseout events on the populated suggestion list dropdown.
             * @private
             */
            _onPropertyChange: function () {
                var searchField = this.element,
                    clonePosition = {
                        position: 'absolute',
                        // Removed to fix display issues
                        // left: searchField.offset().left,
                        // top: searchField.offset().top + searchField.outerHeight(),
                        width: searchField.outerWidth()
                    },
                    source = this.options.template,
                    template = mageTemplate(source),
                    dropdown = $('<ul role="listbox"></ul>'),
                    value = this.element.val();
    
                this.submitBtn.disabled = isEmpty(value);
    
                if (value.length >= parseInt(this.options.minSearchLength, 10)) {
                    $.getJSON(this.options.url, {
                        q: value
                    }, $.proxy(function (data) {
                        if (data.length) {
                            $.each(data, function (index, element) {
                                var html;
    
                                element.index = index;
                                html = template({
                                    data: element
                                });
                                dropdown.append(html);
                            });
    
                            this.responseList.indexList = this.autoComplete.html(dropdown)
                                .css(clonePosition)
                                .show()
                                .find(this.options.responseFieldElements + ':visible');
    
                            this._resetResponseList(false);
                            this.element.removeAttr('aria-activedescendant');
    
                            if (this.responseList.indexList.length) {
                                this._updateAriaHasPopup(true);
                            } else {
                                this._updateAriaHasPopup(false);
                            }
    
                            this.responseList.indexList
                                .on('click', function (e) {
                                    this.responseList.selected = $(e.currentTarget);
                                    this.searchForm.trigger('submit');
                                }.bind(this))
                                .on('mouseenter mouseleave', function (e) {
                                    this.responseList.indexList.removeClass(this.options.selectClass);
                                    $(e.target).addClass(this.options.selectClass);
                                    this.responseList.selected = $(e.target);
                                    this.element.attr('aria-activedescendant', $(e.target).attr('id'));
                                }.bind(this))
                                .on('mouseout', function (e) {
                                    if (!this._getLastElement() &&
                                        this._getLastElement().hasClass(this.options.selectClass)) {
                                        $(e.target).removeClass(this.options.selectClass);
                                        this._resetResponseList(false);
                                    }
                                }.bind(this));
                        }
                    }, this));
                } else {
                    this._resetResponseList(true);
                    this.autoComplete.hide();
                    this._updateAriaHasPopup(false);
                    this.element.removeAttr('aria-activedescendant');
                }
            }
        });
    
        return $.mage.quickSearch;
    });

    Here you can modify autocomplete template, change events or change the whole logic by your own.

    Hope this will help you in your projects, please ask questions if you have any doubt by commenting below.

    Thanks 🙂 .

    . . .

    Leave a Comment

    Your email address will not be published. Required fields are marked*


    4 comments

  • R
    • ashutosh srivastava (Moderator)
  • Rajesh
    • ashutosh srivastava (Moderator)
  • Back to Top

    Message Sent!

    If you have more details or questions, you can reply to the received confirmation email.

    Back to Home