Categories


Archives


Recent Posts


Categories


RequireJS Modules that Return Two Widgets

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

One thing I breezed by in my recent Modifying a jQuery Widget in Magento 2 article was the whole returns two widgets RequireJS module thing. We mentioned that modules like the mage/menu module will do this.

//File: vendor/magento/magento2-base/lib/web/mage/menu.js
define([
    "jquery",
    "matchMedia",
    "jquery/ui",
    "jquery/jquery.mobile.custom",
    "mage/translate"
], function ($, mediaCheck) {
    'use strict';

    $.widget(/*...*/);


    $.widget(/*...*/);

    return {
        menu: $.mage.menu,
        navigation: $.mage.navigation
    };
});

However, we never got into what it means for a RequireJS modules to return two widgets. We learned if a RequireJS module returns a single jQuery widget,

$.widget('pulsestorm.someWidget', /*...*/);

return $.pulsestorm.someWidget;

and if that RequireJS module is invoked via a data-mage-init script

<div id="foo" data-mage-init='{"Pulsestorm_Modulename/path/to/module":{/*...*/}}'></div>

that Magento will invoke the returned jQuery widget definition ($.pulsestorm.someWidget) as though the following were called

jQuery('#foo').someWidget({});

What’s unclear is how Magento handles a RequireJS module (like mage/menu) that returns two widgets. Does it invoke both widgets? Or are these modules intended as loaders only – i.e. they should not be invoked by data-mage-init attributes?

This is more than an esoteric architecture question – a working Magento developer needs to understand how their systems work if they’re going to make sense of them, or if they’re going to provide stable customizations.

Multiple Widgets, Aliases, and Invoked As

The answers turns out to be – a little complicated. Stated as plainly as I can

If a RequireJS module, invoked via data-mage-init, returns an object (containing a list of key/value pairs), Magento will invoke the widget stored at the key matching the name the RequireJS module was invoked as

That’s a mouthful, right? Let’s consider the mage/menu module again. This RequireJS module returns an object.

return {
    menu: $.mage.menu,
    navigation: $.mage.navigation
};

If this module was invoked like this

<div data-mage-init='{"menu":{...}}'></div>    

i.e. if its invoked using the RequireJS menu alias, then Magento will apply the $.mage.menu widget to the div.

This is a little weird, for a few reasons. First, it means if we invoked this particular modules via its real RequireJS module name (mage/menu)

<div data-mage-init='{"mage/menu":{...}}'></div>    

then Magento would throw a confusing javascript error

Uncaught TypeError: Cannot read property 'bind' of undefined
    at main.js:26
    at Object.execCb (require.js:1650)
    at Module.check (require.js:866)
    at Module.<anonymous> (require.js:1113)
    at require.js:132
    at require.js:1156
    at each (require.js:57)
    at Module.emit (require.js:1155)
    at Module.check (require.js:917)
    at Module.enable (require.js:1143)    

The reason for this error? Because there’s no mage/menu key in the returned object.

return {
    menu: $.mage.menu,
    navigation: $.mage.navigation,

    //not a thing
    //'mage/menu': $.mage.menu
};     

The other weird part? We’d like to say the following – that if the module was invoked like this

<div data-mage-init='{"navigation":{...}}'></div>    

then Magento would apply the $.mage.navigation widget. Except we can’t say that, because there’s no RequireJS alias that points navigation to mage/menu.

This sort pattern – where configurations in multiple, distant, files impacts what running code is meant to return – results in confusing, and fragile, systems.

If you’re creating widgets (or more likely, customizing Magento code that uses widgets), I’d stay away from this “returns two widgets” pattern. While I’m sure the core developers did this for a reason, the intent here is less than clear, and will only serve to confuse you and your teammates down the line.

Appendix: Cannot read property ‘bind’ of undefined

For the super curious – why did Magento throw such a confusing error instead of saying something like could not find key mage/menu? If we take a look around line 26 of main.js

#File: lib/web/mage/apply/main.js
function init(el, config, component) {
    require([component], function (fn) {

        if (typeof fn === 'object') {
            fn = fn[component].bind(fn);
        }

        if (_.isFunction(fn)) {
            fn(config, el);
        } else if ($(el)[component]) {
            $(el)[component](config);
        }
    });
}   

This is the code that fails. Putting on some x-ray debugging specs, the above code would look like this for the context we described

#File: vendor/magento/magento2-base/lib/web/mage/menu.js

function init(/*...*, /*...*/, 'mage/menu') {

    require(['mage/menu'], function ({menu: $.mage.menu,navigation: $.mage.navigation}) {

        if (typeof fn === 'object') {
            fn = {menu: $.mage.menu,navigation: $.mage.navigation}['mage/menu'].bind(fn);
        }

        /* ... */
    });
}   

So, as you can see, the module returns the menu, navigation keyed object, but then tries to access key named mage/menu. When it can’t, javascript pukes.

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 26th July 2017