Like its brethren plugin and module, the word widget has the unfortunate distinction of being a popular way to describe a bunch of computer code without a corresponding strict definition of what a widget is.
Magento 2 continues this tradition and adds their typical distinct spin. Like Magento 1, Magento 2 has a “CMS Widget” system that allows developers to create user interfaces for data entry of structured content blocks. While an interesting system, that’s not what we’re here to talk about today.
Why should you care about jQuery widgets?
Finally, as Magento 2 developers, it’s usually our job to change the behavior of the default system in some subtle way. While it’s possible to do this with jQuery widgets, it’s not always obvious how to do this in Magento 2.
Today we’re setting out with the end goal of developing a systematic approach for replacing a method defined on a jQuery widget in Magento 2. In order to do that we’ll need to briefly discuss what jQuery widgets are, how Magento typically defines them, and the extra systems Magento’s introduced for using them.
If you’re a very long time reader, you might remember my pre-Magento four part series on developing a jQuery plugin.
The jQuery widget system is an attempt to further formalize plugin development, help prevent namespace collisions in plugins, and give developers state management and full object lifecycle methods for their plugins. We’re not going to cover widgets in full — jQuery does a good job of that themselves. However, we are going to frame our discussion of widgets.
The above code would make a method named ourPluginMethod available for jQuery client programmers.
This namespace is not something that client programmers are normally exposed to — as we said, all they need to do is call the ourPluginMethod method. Instead, the namespace is there so jQuery has a key to store the widget definition object by. If you peek at the global jQuery object, you’ll find your widget definition object stored under your namespace.
So, that’s plain jQuery widgets without Magento. Magento 2 offers users a number of custom widgets built using the jQuery UI pattern. However, Magento 2’s reliance on the RequireJS module loader means using these widgets isn’t as straight forward as you may be used to.
Magento defines widgets inside RequireJS modules. For example, Magento’s core code defines the list widget in the mage/list module.
The above RequireJS based program has two dependencies. The first is the jQuery library itself, and the second is the list widget. You’ll notice we never actually use the listWidget variable in our program. We need to load the mage/list module so that the widget gets defined. However, once defined, we don’t have any need for the actual widget objects returned by the mage/list module. We access the list method directly via the jQuery object.
This is the general pattern Magento widgets follow. However — Magento being Magento — there are times where the core code strays from this simple “one widget, one RequireJS module” pattern. For example, the Menu and Navigation widgets are both defined in the mage/menu RequireJS module.
Most (but not all) of Magento’s jQuery-widget-defining-RequireJS-modules are aliased like this.
Finally, sometimes Magento defies all convention. Consider the calendar widget. So far, an astute reader might assume that the calendar widget is defined via a RequireJS module named mage/calendar. They’d be right so far in that there’s a lib/web/mage/calendar.js file that Magento invokes as a RequireJS module named mage/calendar. You can see an example of that here.
However, the calendar.js file is not actually a RequireJS module. Instead it’s an immediately invoked anonymous callback function that defines both the mage.dateRange and mage.calendar widget.
This data-mage-init attribute invokes the dropdownDialog RequireJS module. The dropdownDialog is actually an alias to the mage/dropdown RequireJS module.
One last important thing to note before we move on. In our example above, the jQuery widget’s full name is mage.dropdownDialog. This means the jQuery method name will be dropdownDialog. While the name of the RequireJS module alias is also dropdownDialog, there’s nothing in the system that formally connects this module name and the widget name. Magento could just as easily have aliased mage/dropdown as dropdownDialogLaDeDa or not aliased it all and things will still work. Keep in mind that not all Magento core widgets will have RequireJS aliases or module names that line up nicely with the widget names.
Since the earliest days, Magento development has always been about making slight, stable changes to the stock Magento system that will implement the new functionality you want without changing the behavior of other core systems. The stabler your changes, the more success you’d have. The less stable your changes, the greater the chance your system would fail/crash.
The features in Magento 2’s systems continue that tradition. The PHP backend has class preferences and plugins, and the front-end systems have RequireJS’s many aliasing techniques.
So long as you define the widget before it’s instantiated, you can add and redefine methods to it all day long. The widget system even offers you the ability to call parent methods via _super and _superApply methods (see the official docs for more information). So long as you redefine the widget before instantiating a widget instance, everything will work out great.
That, unfortunately, is a problem for Magento. You’ll recall that, via the data-mage-init attribute, Magento allows you to instantiate a widget inline.
In other words, the data-mage-init technique both defines and instantiates a widget in one go. This makes changing widget behavior less than straight forward.
There are, of course, inelegant/brute-force methods. As we learned earlier the dropdownDialog symbol is an alias for the mage/dropdown module, which lives in the file ./lib/web/mage/dropdown.js. We could edit this file directly, or replace this file in our custom theme with one that had our changes. Working Magento developers do this every day.
While this might implement the functionality we need, we risk our changes (both intentional changes and non-obvious changes) breaking the system in some other place. We also end up needing to manually merge (and remembering to manually merge) our changes with changes in each new point release of Magento.
When you’re changing the behavior of Magento, you want your changes to be slight. It’s better to write 5 lines of code and spend the rest of the day figuring out how to elegantly insert them than jamming in 50 new lines of code with 500 copy/pasted lines of code however you can.
Magento will replace that value by whatever our function returns.
Using mixins, we can listen for Magento’s loading of a RequireJS widget loading module. Then, we can redefine the jQuery widget. Finally, we can return our newly instantiated widget definition.
If that didn’t make sense, an example should clear things up. We’re going to redefine the open method on the dropdownDialog widget.
The first thing we’ll want to do is create a new Magento module with a requirejs-config.js file. This is Magento’s standard mechanism that lets developers add to the existing, standard, RequireJS configuration. We’re going to use pestle to do this, but feel free to use whatever module creating technique you prefer.
Once you’ve created your module, add the following requirejs-config.js file.
Then, create a Pulsestorm_Logdropdown/js/dropdown-mixin RequireJS module that matches the following.
This should automatically clear your cache as well. Once you’ve done that, load any page that uses the dropdownWidget widget (home page, catalog listing page, etc), and you should see an alert and a console.log message that says Our mixin is hooked up.
we’re telling Magento we want a mixin for the mage/dropdown RequireJS module, and that we’ve implemented the mixin in the Pulsestorm_Logdropdown/js/dropdown-mixin RequireJS module. This needs to be the real module name (mage/dropdown) and not the alias name (dropdownDialog).
Once you have your scaffolding up and running, we’ll be ready to actually change the widget definition.
Congratulations! You just extended a Magento 2 jQuery widget.
When used with three parameters, the jQuery.widget factory will create (or redefine) a widget named mage.dropdownDialog that uses the second argument as a parent widget definition (in this case, the same widget as stored on the global jQuery object), with the third parameter containing the new methods.
Our call to this._super(); is a part of the jQuery widget system — it calls the parent open method. Whether your method definition returns the original return value and/or puts your code before/after the this._super() method will depend on what you’re trying to do. Standard OOP principles still apply!
While there are other techniques that might work for replacing a jQuery widget’s method definition in Magento 2, Magento’s mixin-listeners offer the cleanest methods I’ve seen for doing so. Like most techniques in Magento 2, there’s no guarantees these will continue to work in future versions of Magento, or even work in all scenarios in current versions.
Like most real-world software problems, it’s best to understand how these techniques work so you can adapt and extend them yourself in the future. Despite opinions to the contrary, software is ultimately a logical, step by step progression through a set of rules. A working system is, ultimatly, knowable.