A great use for Require JS Mixins – The catalog “Add to Cart” trick

For a long time I have been using a little Javascript on Magento 2 sites to open up the Minicart when a customer clicks the “Add to Cart” button. But a few days back i took a second look at this little piece of Javascript and realized there was a better way to do this. A great excuse for a blog post about Require JS Mixing in Magento 2 and learning from my mistakes.

You are doomed to repeat it

If the history of being a developer has taught me anything it’s that I am constantly repeating myself. With each project there are tricks and techniques that I just use again and again. Returning to one of these tricks, I looked up a stack overflow post I had answered years ago to grab some code that i’ve used before (see that post here: Magento Stack Exchange). When I was looking at the post I realized that my answer was wrong. Well, not wrong per se. But there was a better way to get this functionality.

Digging up the past – THE WRONG WAY TO DO IT

If you were to ask any 10 developers how to achieve some functionality, you would get 11 answers. Such was the case with the aforementioned Magento Stack Exchange post. The functionality is really simple, and really all the answers here “work”. Magento is a very flexible framework and there is no “one way to do” any one thing. That said, there are advantages to using built in systems that are optimized for the task.

In my original answer i used this script:

define(
    'jquery'
], function ($) {
    return function () {
        $('[data-block="minicart"]').on('contentLoading', function () {
            $('[data-block="minicart"]').on('contentUpdated', function ()  {
                $('html, body').animate({ scrollTop: 0 }, 'slow');
                $('[data-block="minicart"]').find('[data-role="dropdownDialog"]').dropdownDialog("open");
            });
        });
    }
});

This method works. It’s some simple jQuery watching for the contentLoading event, scrolls to the top and then “opens” the Minicart. Nothing wrong with that. And in comparison to some of the other answer we are not falling into the trap of overriding a phtml file (something that should be done with great care). The top answer says to override minicart.js which is not only the wrong approach, it’s actually harmful to your site (overriding JS files leads to massive upgrade problems that are hard to debug).

So what is the right way to do this?

Looking to the future – THE RIGHT WAY TO DO IT

Magento 2 came with a RequireJS system for injecting custom code into existing scripts call Mixins (learn more about RequireJS here). Alan Storm wrote a great blog about this system with a lot more background then i will cover here. But the basic idea is pretty user friendly. Let me show an example and explain what it’s doing.

let config = {
    config: {
        mixins: {
            'Magento_Catalog/js/catalog-add-to-cart': {
                'Magento_Catalog/js/catalog-add-to-cart-mixin': true
            }
        }
    }
};

This is the local requirejs-config.js file in your theme or module. Inside we have a config object and inside that we have a node called mixin which takes the name of the file you are looking to inject into. From that node we have an object that has the name of the file where your code is going to go with a : true setting.

The name of the file you are injecting into can be the path to the file or the RequireJS name for it. In the case of catalog-add-to-cart.js we could have used catalogAddToCart as it is defined in the Magento core.

// File: vendor/magento/module-catalog/view/frontend/requirejs-config.js: catalogAddToCart: 'Magento_Catalog/js/catalog-add-to-cart'

Either works just fine and i picked the full path here just to be “more clear” about what i’m injecting into.

The file we create can also live anywhere, but it’s convention to match the path and the file name with mixin added to the end. But without thinking too hard about it, this just makes sense, you have an existing file and you want to use another file to inject into it. Simple. The picky part of this is the structure of the inject code, which might be the reason some developer shy away from this system.

// File: app/design/frontend/{{vendor_name}}/theme/Magento_Catalog/web/js/catalog-add-to-cart-mixin.js
define([
    'jquery'
], function($) {
    "use strict";
    return function (widget) {
        $.widget('mage.catalogAddToCart', widget, {
            /**
             * Handler for the form 'submit' event
             *
             * @param {Object} form
             */
            submitForm: function (form) {
                this.showCart();
                this.ajaxSubmit(form);
            },
            /**
             * Open minicart when Add To Cart has been clicked
             */
            showCart: function() {
                let self = this;
                $('.block-minicart').dropdownDialog('open');
                $(this.options.dropdownDialogSelector).addClass('minicart-loading');
                $(this.options.minicartSelector).on('contentUpdated', function () {
                    $(self.options.dropdownDialogSelector).removeClass('minicart-loading');
                });
            }
        });
        return $.mage.catalogAddToCart;
    }
});

So let’s go through this file slow. There are a lot of gotchas here that can trip you up. But trust me, it’s worth the effort.

We start like any other RequireJS AMD module with a define and all our dependencies. In this case we are injecting into a jQuery widget, so we need jQuery to do that. Magento 2’s use of the jQuery widget system is a deep topic that I hope to cover here soon. But for now just know that we are calling the original widget with this line return function (widget), creating a new widget $.widget('mage.catalogAddToCart', widget, {} and then returning that widget with return $.mage.catalogAddToCart;. This is not optional. While there is no requirement to use mage.catalogAddToCart as a name, all the rest is required for the script to work.

Inside the function body we have the meat and potatoes of our work. We are trying to trigger the Minicart to open when the customer clicks the “Add to Cart” button. That action triggers the submitForm() function. So we grab that from the original file. Since there is nothing happening in the function other than the triggering of this.ajaxSubmit(form); we grab the whole thing. This is not required though. You will often see $this._super(); in these files. This line will run the original function after our injection. The signature of the function needs to stay the same, so pass in any of the perams that are called out.

For us, we just needed to add in one line to submitForm()this.showCart();. This is our custom function that is going to open the Minicart for us. In here, we are using a lot of the same code that is used in the core file. But one thing to note, we are still able to access the same options of the core file. Things like this.options.minicartSelector are there for us to use. This improves the extensibility of our mixin. Say that at some point in the far future Magento changes the core '[data-block="minicart"]' to something else. It’s unlikey, but let’s just imagine they did. We wouldn’t have to change a line of code in our file.

And that is about it. Very straight forward and simple. And i hope you can see all the different things we can now do. Let’s say a client asks to have a modal pop up when adding to cart give the customer extra information about the checkout process. Or you need to track this action for some other bits of information. You can easily add in all that complexity without overriding a single core file. But before we wrap up, i want to go back over why this is such a better method.

Why Mixins matter

It’s easy when working on a Magento 2 site to just get things working and walk away. In the case of this feature and the Magento Stacks question that spawned this post, quite a few people do just that. But it’s import when learning Magento 2 to keep track what you don’t know. In the case of this little feature it’s close to impossible to know all the different scripts and files that are touching the “Add to Cart” button. And without knowing all those different pieces of the puzzle it’s best to just stick to “best practices”.

Mixins provide us a powerful way to inject our custom code while maintaining upgradeability and core functionality. We avoid messy overrides and impossible to find snippets of Javascript that can cause errors on the front end. And when the syntax is fleshed out, it’s actually simpler and cleaner then some of the hacks i have come across (and written).