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.

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

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.

Instead, we’re going to talk about Magento’s use of jQuery Widgets. The jQuery widget system is a part of jQuery UI. It’s marketed as a way to develop your own user interface elements for jQuery UI, but long time readers of this website will recognize it for what it is: An object system built on top of javascript.

Why should you care about jQuery widgets?

A good chunk of Magento’s default user interface is built-out using custom jQuery widgets — so much so that earlier evangelism efforts often confused jQuery widgets as Magento Widgets. Also, a good chunk of Magento’s underlying javascript systems were built with jQuery widgets in mind, but proved useful enough that other systems (UI Components, etc) piggy backed on them — creating more confusion. Also also, because all Magento javascript is (or should be) bootstrapped through RequireJS, it’s not always clear how/where widgets are defined, or how we can use them as Magento 2 developers.

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.

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 widget system is, on one level, just another javascript object system. In jQuery, you create a widget definition with code that looks something like this

When we call jQuery.widget — we’re creating a widget definition. This is similar to creating a class definition file in a traditional object system. When a developer says jQuery('.some-node').ourPluginMethod, this is similar to a developer instantiating an object using a class definition file. The jQuery widget system even allows you to call through to widget methods via a (slightly weird) API

One of the more confusing things about widgets are the namespace — ournamespace below

jQuery.widget('ournamespace.ourPluginMethod',

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.

console.log(jQuery.ournamespace.ourPluginMethod)

Widgets are a powerful and complex system — if you’re going to customize how the default Magento theme(s) behave you’ll want to learn them inside and out. However, for today, the most important thing to understand about widgets is they’re just another javascript object system.

As you can see, the mage/list module defines a widget in the mage namespace, with a name of list. This means if you want to use the list widgets in your jQuery programs, you need to do something like this

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.

The mage/menu module also offers another example of something to watch out for. Magento often aliases its jQuery-widget-defining-RequireJS-modules. For example, you can see mage/menu aliased as menu here

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.

This callback style allows a developer to use the lib/web/mage/calendar.js file as both a RequireJS module or as a bog-standard <script src=""></script> javascript include. This comes at the cost of some confusion for developers coming along later (i.e. us).

Instantiating Widgets with Magento 2

As we previously mentioned — when a developer calls the jQuery.widget method

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

they’re creating a widget’s definition — similar to a PHP/Java/C# developer defining a class. When a developer uses the widget

$(function(){
/* ... */
$('#someNode').someWidget(/*...*/);
});

they’re telling jQuery to use the foo.someWidget definition to create or instantiate the widget, similar to how a PHP/Java/C# developer might instantiate an object from a class

$object = new Object;

While it’s possible to use these Magento 2 defined widgets in the same way

Magento 2 offers two new ways of instantiating widget objects — and that’s the data-mage-init attributes, and the x-magento-init script tags. We covered both in our Javascript Init Scripts article. It turns out that bothdata-mage-init and the x-magento-init form with a DOM node (not the * form) are widget compatible. That is, you can say

and the data-mage-init and x-magento-init techniques expect a RequireJS module that returns a function with the same signature as a jQuery widget callback. In fact, it’s probably safe to say that both data-mage-init and x-magento-init were designed to work with widgets initially, and it was only later that they were adopted (by the UI Component system, for one) as a way of invoking javascript with server side rendered JSON objects.

Magento’s default themes (and the third party themes that use them as a base) use these data-mage-init widgets all over the place. Here’s one example from the home page

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.

Replacing Widget Methods

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.

What’s tricky with Magento’s jQuery widgets is, they’re an object system within another object system. What we mean is, jQuery widgets by themselves have a simple and elegant method for replacing methods — you just redefine the widget using the jQuery widget’s inheritance system

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.

The Magento core code that implements data-mage-init uses the returned widget object to instantiate a widget

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.

We’ll change this method to write a line of output to the javascript console, and then call its parent open method. i.e. We’ll make the widget do one extra thing, and then do whatever it was originally going to do. While our example is a little silly, this pattern is at the heart of any stable Magento customization.

Creating our Mixin

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.

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.

So far, all we’ve done is setup the scaffolding for our mixin. In the requirejs-config.js file

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).

We’ve imported the jquery module because we’ll need it later. The RequireJS module returning a function is how Magento’s javascript mixins are implemented. The originalWidget parameter holds the original return value of mage/dropdown. All we’ve done above is return the originalWidget variable after running our debugging statements. This means the system should behave identically as before.

Once you have your scaffolding up and running, we’ll be ready to actually change the widget definition.

Changing a Widget

OK, one last bit of code to go! We’re going to replace our module definition from above with the following

With the above in place, clear your browser cache and reload the page. Then, click on any dropdown widget on the page — the currency/store-views are good candidates

After clicking on these menus and confirming everything still works, take a look at your javascript console — you should see the I opened a dropdown! text successfully logged.

Congratulations! You just extended a Magento 2 jQuery widget.

As a reminder, our module needed to do two things: Redefine the jQuery widget, and then return the newly defined jQuery widget definition . We redefined the widget with the following call (comments stripped)

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.

As for the new methods themselves

{
open:function(){
//our new code here
console.log("I opened a dropdown!");
//call parent open for original functionality
return this._super();
}
}

Here we’ve redefined the open method, originally defined on the mage.dropdown widget

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!

Wrap Up

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.