Invoking javascript in the Magento frontend

Magento 2

​This is a post I definitely I'm writing for myself: as there are several options I always have to look up on how to do this part in Magento development. An example is always in some other branch or some other project and I never know how to remind which project or branch exactly.

Inline javascript

The first thing we are going to do is to invoke javascript directly from our .phtml file:

<script type="text/javascript">
require(['jquery'], function ($) {
$('.my-special-element').click( function () {
    alert('You clicked the special element');
});
});
</script>

This is a start and in some minor cases it is useful, but inline javascript is not recommended. We can do better.

x-magento-init

Magento provides the x-magento-init tag. When you use the tag and some configuration, Magento will initialize any javascript component for you, with the provided configuration.

my-custom-template.phtml

<script type="text/x-magento-init">
{
    ".element-selector": {
        "Vendor_Module/js/view/my-slick-component": {
            "slidesPerPage": 3
        }
    }
}
</script>

Now when initialized, Magento will try to load the view/frontend/web/js/view/my-slick-component.js file. The contents of this file will look like something like this:

define(['jquery'], function () {
    return function (options, element) {
        console.log(options.slidesPerPage); // 3
        
        console.log(element); // The element(s) found with the ".element-selector" selector.
    }
});

Using knockout

The previous example is nice, but you can't use Knockout. So how do we need to do this? We need to use the Magento_Ui/js/core/app module for this. This module is responsible for binding your knockout component to the element. 

my-custom-template.phtml

<div data-bind="scope: 'slick-component'">
    <span data-bind="click: add">Add</span>
    <span data-bind="click: sub">Sub</span>

    <span data-bind="text: currentCount"></span>
</div>

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "slick-component": {
                    "component": "Vendor_Module/js/view/my-slick-component",
                    "config": {
                        "slidesPerPage": 3
                    }
                }
            }
        }
    }
}
</script>

And the Knockout component looks like this:

define([
    'uiComponent',
    'ko'
], function (
    Component,
    ko
) {
    return Component.extend({
        defaults: {
            currentCount: ko.observable(1),
            slides: ko.observableArray([]),
        },

        add: function () {
            var currentCount = this.currentCount();
            this.currentCount(currentCount++);
        },

        sub: function () {
            var currentCount = this.currentCount();
            this.currentCount(currentCount--);
        },

        nextSlide: function () {
            console.log(this.slidesPerPage); // 3
        }
    });
});

As you can see, you can access the variables passed in the "config" section by using their key.

The big advantage of this method is that you can render HTML and sparkle javascript over this so you don't have any layout shifts.

Knockout with a template

Last but not least is using knockout with a template. The HTML in your .phtml file is different, but the x-magento-init part is exactly the same:

<div data-bind="scope: 'slick-component'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "slick-component": {
                    "component": "Vendor_Module/js/view/my-slick-component",
                    "config": {
                        "slidesPerPage": 3
                    }
                }
            }
        }
    }
}
</script>

The javascript is also exactly the same, except for the defaults.template part:

define([
    'uiComponent',
    'ko'
], function (
    Component,
    ko
) {
    return Component.extend({
        defaults: {
            template: 'Vendor_Magento/view/slick-component',
            currentCount: ko.observable(1),
            slides: ko.observableArray([]),
        },

        add: function () {
            var currentCount = this.currentCount();
            this.currentCount(currentCount++);
        },

        sub: function () {
            var currentCount = this.currentCount();
            this.currentCount(currentCount--);
        },

        nextSlide: function () {
            console.log(this.slidesPerPage); // 3
        }
    });
});

The HTML that was previously in your .phtml file is now moved to the view/frontend/web/template/view/slick-component.html file:

<span data-bind="click: add">Add</span>
<span data-bind="click: sub">Sub</span>

<span data-bind="text: currentCount"></span>

Advantages/disadvantages

Each method has its own advantages/disadvantages. Let's go over them:

Inline javascript:

It's fast, but inline javascript is discouraged in general. The same counts for this method.

x-magento-init

This is already way better than an inline script but only use this for small scripts as it is hard to maintain.

Using Knockout

Using knockout without a template is suitable for a lot of applications, especially when you have to prevent layout shifts, which seems to be a hot topic currently. The downside of this method is that the template is hard to override.

Using Knockout with a template

From a maintainability/extensibility perspective this is the best solution: It is easy to override the javascript and/or the HTML template. The big downside of this method is that the template is only rendered when the rest of the page is already loaded. On slower devices, this can take a moment which can look weird.

Want to respond?