Widgets

Magento builds most storefront behaviour on the jQuery UI widget factory: options, a _create hook, and one instance per element.

Syntax$.widget('vendor.widgetName', [$.mage.baseWidget,] { options: {}, _create: function () {} });

A widget is a stateful JavaScript object bound to a DOM element, built with the jQuery UI widget factory. Magento 2.4.9 bundles jQuery 3.7.1 and the widget factory from jQuery UI 1.14.1, and ships around thirty widgets of its own in lib/web/mage: collapsible, tabs, accordion, dropdown, menu, loader, tooltip, validation and others. The modal widget is an exception: it is registered as mage.modal but its file lives in Magento_Ui, at Magento_Ui/js/modal/modal. Most storefront interaction you see is one of these.

Defining one

Call $.widget() with a namespaced name, an object of defaults under options, and your methods. Return the constructor the factory created. Depend on jquery-ui-modules/widget, not jquery/ui, which in 2.4 is mapped to a compatibility shim.

The part of the name after the dot becomes the jQuery plugin: $.widget('vendor.stockAlert', ...) creates $.fn.stockAlert. Inside the widget, this.element is the jQuery element, this.options is the defaults deep-merged with the configuration from the init markup, and _create runs once per element. Bind handlers with this._on() so they are removed when the widget is destroyed.

Extending one

Pass a base widget as the second argument: $.widget('vendor.myCart', $.mage.catalogAddToCart, { ... }). Inside an overridden method this._super() calls the base version. This creates a new widget and leaves the original alone, so templates that name the original still get the original. When you want every caller to get your change, use a mixin instead.

A new widget file needs static content rebuilt: bin/magento cache:flush, then clear pub/static/frontend in developer mode.

Example

Example · javascript
// app/code/Vendor/Notice/view/frontend/web/js/stock-alert.js

define([
    'jquery',
    'mage/translate',
    'jquery-ui-modules/widget'
], function ($, $t) {
    'use strict';

    $.widget('vendor.stockAlert', {
        options: {
            threshold: 5,
            qty: 0,
            messageSelector: '[data-role="stock-message"]',
            lowClass: 'stock-low'
        },

        /**
         * Runs once per matched element.
         *
         * @private
         */
        _create: function () {
            this.message = this.element.find(this.options.messageSelector);
            this._render();
            this._on({
                'click [data-role="refresh"]': '_render'
            });
        },

        /**
         * @private
         */
        _render: function () {
            var low = this.options.qty <= this.options.threshold;

            this.message.text(low ? $t('Only a few left') : $t('In stock'));
            this.element.toggleClass(this.options.lowClass, low);
        }
    });

    return $.vendor.stockAlert;
});

// The plugin created here is $.fn.stockAlert. A template starts it with
// data-mage-init='{"Vendor_Notice/js/stock-alert": {"qty": 3}}' and the
// element gains the stock-low class and the message text.
// Apply: bin/magento cache:flush, then clear pub/static/frontend.

When to use it

  • Add behaviour to a block of markup that needs to keep state between clicks.
  • Change how the core add to cart form submits, for your own template only.
  • Expose a short component name so designers can start your widget from a template.
  • Tear a widget down cleanly when its container is replaced by an AJAX response.

More examples

Extending a core widget into a new one

The base widget is passed as the second argument and reached with this._super(). This produces a separate widget: core templates keep using mage.catalogAddToCart, so you must point your own template at Vendor_Cart/js/bulk-add-to-cart for it to run.

Example · javascript
// app/code/Vendor/Cart/view/frontend/web/js/bulk-add-to-cart.js

define([
    'jquery',
    'mage/translate',
    'Magento_Catalog/js/catalog-add-to-cart',
    'jquery-ui-modules/widget'
], function ($, $t) {
    'use strict';

    $.widget('vendor.bulkAddToCart', $.mage.catalogAddToCart, {
        options: {
            confirmOver: 10
        },

        /** @inheritdoc */
        submitForm: function (form) {
            var qty = parseInt(form.find('#qty').val(), 10) || 0;

            if (qty > this.options.confirmOver && !window.confirm($t('Add this many?'))) {
                return;
            }

            this._super(form);
        }
    });

    return $.vendor.bulkAddToCart;
});

Shorthand and longhand for calling a widget method

The string-dispatch form is what you will see in core and in templates. The instance form is easier in the console and when you need to inspect state, and it is the same object either way.

Example · javascript
// Both lines change the same running widget instance.

// Shorthand: the jQuery plugin takes a method name as its first argument.
$('#product-notice').stockAlert('option', 'threshold', 10);

// Longhand: fetch the widget instance, then call the method on it.
// 'instance' is provided by the widget factory and returns the object.
$('#product-notice').stockAlert('instance').option('threshold', 10);

// Destroying works the same way and removes handlers bound with _on.
$('#product-notice').stockAlert('destroy');

A map alias so templates can use a short name

This is how core exposes names like collapsible and tooltip, which are defined in Magento_Theme's requirejs-config.js. Add an alias when the component will be named in many templates, and skip it when it is used once.

Example · javascript
// app/code/Vendor/Notice/view/frontend/requirejs-config.js

var config = {
    map: {
        '*': {
            stockAlert: 'Vendor_Notice/js/stock-alert'
        }
    }
};

// With this alias in place, a template can write
//   data-mage-init='{"stockAlert": {"qty": 3}}'
// and Magento resolves the name through the alias, finds $.fn.stockAlert
// on the element, and calls $(element).stockAlert(config).
// Apply: bin/magento cache:flush, then rebuild static content.

Discussion

  • Be the first to comment on this lesson.