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.