Easily Add Custom JavaScript to Magento 2

Easily Add Custom JavaScript to Magento 2

How to easily add your own JavaScript into Magento.

Scroll Down for more info

In this article, we discussed the various tools available for our use in Magento 2. There is a large front end framework in Magento 2, and one of the 3rd-party frameworks it relies on is KnockoutJS. It uses a concept that is different from the way it has been done for a long time, and one that is becoming common with other frameworks such as Vue.js. That approach focuses on data within the Javascript files and uses declarative attributes to bind actions or data to specific elements.

Next, we will explore an incredibly simple approach in which we can leverage Magento 2's Javascript framework to add our own custom Javascript. This does not use KnockoutJS, but could be considered an "in-between" step, while keeping boilerplate code to nearly nothing.

In this and a future article, I will detail two effective approaches to including custom Javascript in the front end of Magento 2:

  1. Include a simple, custom Javascript module (this article).
  2. Use KnockoutJS by bootstrapping a minimal UI Component (coming soon).

Include a simple, custom Javascript module:

This technique does not use Knockout and still uses the concept of selecting elements based off of their attributes. However, for including a Javascript module in Magento 2, this is very lightweight and ideal for reasonably small sets of functionality. It also allows for the targeted content to remain visible to search engines and other crawlers. I personally use it frequently.


The bootstrap JSON:

The bootstrap is some JSON that is included in a .phtml template. That JSON is included within a <script> tag with a type attribute of script/x-magento-init. Script tags that the browser does not recognize are not parsed by the browser like ones with type="text/javascript". Magento finds them and uses the content within them for configuration. This is how it looks:

<script type="script/x-magento-init">
{
    "iframe[src*='//www.youtube.com']": { // selects YouTube videos
        "SwiftOtter_Youtube/js/responsive": {}
    }
}
</script>

In just a few lines of code, we (1) targeted a set of elements, (2) loaded a module, and (3) passed every instance of this element into the Javascript module. Magento takes the first key, "iframe[src*='//www.youtube.com']" in this example, and uses that as a selector. It finds all of the matching elements on the page and iterates over them individually: passing each one as a parameter into the function that is returned from the Javascript module.

That Javascript module is defined within the next key: "SwiftOtter_Youtube/js/responsive". If you are unfamiliar with how Magento 2 loads Javascript, that probably looks incredibly strange, but let's break it down. Just like the way Magento 1 changes some paths, there is some magic going on here. SwiftOtter_Youtube is the name of the Magento 2 module. It's the value declared in registration.php and etc/module.xml. The module name is replaced with the path to the module's web assets. This is helpful because the assets are symlinked in developer mode and copied in production. That means that the path is different in the source code versus the web path. In the module, those files reside in: view/[area]/web. In this example, [area] will be frontend because this will run on the store front, but other options include adminhtml and base (which applies to both).

The rest of the path is the location of the file with a RequireJS twist. RequireJS adds .js to the filename when it looks that up. As a result, the file we referenced in the example would be located here: app/code/SwiftOtter/Youtube/view/frontend/web/js/responsive.js.


The custom Javascript module:

Next, let's take a look at the Javascript module that is loaded. A RequireJS define() function is expected in this file. The callback within that function is expected to return a function. So, it's a function within a function, or technically, a closure within a callback. Here's an example:

// if no dependencies, the first argument is not required
define(['underscore'], function(_) {
    return function(config, element) {
        // ...
    }
});

For every element that matches the selector we provided, the function is called with two arguments. The first argument is the configuration that could be passed in via the script tag in the phtml template above. Instead of "SwiftOtter_Youtube/js/responsive": {} in the above, we could do:

"SwiftOtter_Youtube/js/responsive": {
    "defaultRatio": "0.56" // 16:9
}

The default aspect ratio would be accessible in the function like this: config.defaultRatio.

In just a few lines of configuration, many things happen for us. The Javascript file is loaded asynchronously, and with a win for performance, only if that selector exists. Configuration is easily passed into the Javascript module. This means that random variables littering the global namespace storing server-generated data should be nearly eliminated. Every element that matches the selector is found so we don't need to use querySelectorAll() and iterate over every element. All of that is done in one easy package.

By the way, if you do need to iterate over a NodeList, Underscore.js is bundled with Magento 2 and has a convenient _.each() method. When browser support becomes acceptable, we will simply be able to iterate over them like an array with NodeList.forEach, but Underscore makes it easy for now.


Additional Considerations

One downside of this technique is that files are not bundled. While not ideal, I do not consider this a significant downside because these files are executed completely asynchronously. With that, however, if there are significant visual changes happening with the Javascript module, the page will jank. If you need a module executed as soon as possible, I suggest putting the script in the head with the defer="defer" attribute (browser support is pretty good). That will still be loaded asynchronously but will execute sooner.

This technique is best applied for reasonably small Javascript modules. It has the advantage of being progressive enhancement and not hindering search engine optimization.

Other Tips & Details

We use Babel to transpile ES6. To do this, we use Frontools. Frontools transpiles files that include the .babel.js file extensions. As a result, in the above example, we would name the file responsive.babel.js and would reference it like this: SwiftOtter_Youtube/js/responsive.babel. One approach then is to create a class to encapsulate the functionality of the module. Notice how every element on the page has its own class.

// if no dependencies, the first argument is not required
define(function() {
    class ResponsiveVideo {
        constructor(config, element) {
            this.config = config;
            this.defaultRatio = config.defaultRatio;

            this.setupSize()
                .handleVideoSize();

            window.addEventListener('resize', this.handleVideoSize.bind(this));
        }

        setupSize() {
            //...

            return this;
        }

        handleVideoSize() {
            // ...

            return this;
        }
    }
    
    return (config, element) => { new ResponsiveVideo(config, element); }
});

Also note that instead of using a separate <script> tag, you can include a data-mage-init attribute on an element. The configuration structure is essentially the same but does not include the element selector. The element that has the data-mage-init attribute is passed in as the element.

SwiftOtter, Inc.
It relates to CSS, Javascript, Magento 2, Front end development and Back end development.
Jesse Maxwell - Front end developer at Swift Otter

Front end developer at SwiftOtter - @bassplayer_7