Mixins

A mixin changes a core JavaScript module in place, without replacing its module id, so upgrades and other extensions keep working.

Syntaxreturn function (target) { /* change it */ return target; };

A mixin is an AMD module that receives another module as its argument and returns a changed version of it. Magento applies mixins through a RequireJS loader plugin, lib/web/mage/requirejs/mixins.js, so every consumer of the target id gets the modified module. The target keeps its own id, nothing is forked, and several modules can mix into the same target.

Declaring one

Add the target id and your mixin id under config.mixins in requirejs-config.js. The config file and the mixin file must sit in the same area directory, and the mixin file must live under web/js.

Four shapes of target

What your function returns depends on what the target is.

  • A UI component: call target.extend(mixinObject) and return the result.
  • A jQuery widget: re-declare it with $.widget('mage.modal', targetWidget, mixinObject) and return $.mage.modal.
  • A plain object: wrap one of its methods with wrapper.wrapSuper() from mage/utils/wrapper and return the object.
  • A plain function: return wrapper.wrap(target, fn), where fn gets the original as its first argument.

In the first three shapes this._super() calls the original method.

What trips people up

Mixins apply in module load order and cannot be switched off. You can replace another module's mixin by setting it to false and declaring your own, but that module must be listed in a sequence tag in your etc/module.xml or the order is undefined. After any change here run bin/magento cache:flush and rebuild static content.

Example

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

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

    return function (targetWidget) {
        // Re-declared under the target's own alias, mage.catalogAddToCart,
        // so every template that already uses it picks this version up.
        $.widget('mage.catalogAddToCart', targetWidget, {
            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 $.mage.catalogAddToCart;
    };
});

// On the storefront: adding more than ten of anything now asks for
// confirmation on every add to cart form, including core templates.
// Apply: bin/magento cache:flush, then clear pub/static/frontend and
// var/view_preprocessed in developer mode.

When to use it

  • Add a confirmation step to the core add to cart form without copying its file.
  • Record an event every time the checkout moves to a new step.
  • Change a default option on a core widget for every template that uses it.
  • Replace another extension's mixin on the same core module, declaring that extension in a sequence tag.

More examples

The declaration that activates it

The mixin file alone does nothing. Keep this config in the same area directory as the mixin, and remember that the target key is a module id, not a file path, so it carries no .js extension.

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

var config = {
    config: {
        mixins: {
            'Magento_Catalog/js/catalog-add-to-cart': {
                'Vendor_Cart/js/add-to-cart-mixin': true
            },
            'Magento_Checkout/js/model/step-navigator': {
                'Vendor_Cart/js/model/step-navigator-mixin': true
            }
        }
    }
};

// Nothing happens without this file. The key is the target module id
// exactly as other modules require it; a typo there fails silently,
// because RequireJS simply never asks for a mixin on that id.

Wrapping a method on a plain object

Magento_Checkout/js/model/step-navigator returns a single shared object rather than a constructor, so there is no extend() to call. wrapSuper replaces one method and makes this._super() available inside the replacement.

Example · javascript
// app/code/Vendor/Cart/view/frontend/web/js/model/step-navigator-mixin.js

define([
    'mage/utils/wrapper'
], function (wrapper) {
    'use strict';

    return function (stepNavigator) {
        stepNavigator.setHash = wrapper.wrapSuper(
            stepNavigator.setHash,
            function (hash) {
                this._super(hash);
                // Extra work after the original, for example notifying
                // an analytics module that the checkout step changed.
            }
        );

        return stepNavigator;
    };
});

Wrapping a module that returns a function

Magento_Checkout/js/proceed-to-checkout returns a bare function, the shape described in the initialisation lesson. wrap hands you the original as the first argument, so you decide whether and when to call it.

Example · javascript
// app/code/Vendor/Cart/view/frontend/web/js/proceed-to-checkout-mixin.js

define([
    'mage/utils/wrapper'
], function (wrapper) {
    'use strict';

    return function (proceedToCheckout) {
        return wrapper.wrap(
            proceedToCheckout,
            function (original, config, element) {
                // The original is the first argument, then the component's
                // own (config, element) pair.
                original(config, element);
            }
        );
    };
});

Discussion

  • Be the first to comment on this lesson.