Today we’re starting a new series that will cover advanced Javascript systems in Magento 2. This series is sponsored by my patreon campaign. If you like what you see here, please consider donating to keep the tutorials coming.
Back in our Magento 2 for PHP MVC series, we stated that RequireJS was the library that underlies nearly every javascript feature built in Magento 2. This is true enough, but RequireJS only scratches the surface of what’s possible with javascript and Magento 2.
Today we’re going to explore the various systems Magento 2 has for kicking off execution of javascript code without an embedded <script type="text/javascript">
tag.
Javascript Init Methods
The Magento javascript init methods we’re going to discuss solve a few different problems.
First, they provide a standard mechanism to discourage directly embedding javascript into a page.
Second, they provide a way to invoke a stand alone RequireJS module (defined with define
) as a program.
Third, they provide a way to pass that program a server side generated JSON object.
Fourth, they provide a way to tell that program which (if any) DOM nodes it should operate on.
Keep these four goals in mind. They may help you if you’re struggling with the mental model behind these custom framework features.
Setting up a Module
Toady’s tutorial doesn’t involved much in the way of Magento’s PHP code — you can run these examples from any Magento phtml
template that’s rendered on a stock Magento page.
We’re going to use pestle to create a module named Pulsestorm_JavascriptInitTutorial
with a single URL endpoint by running the following three commands
$ pestle.phar generate_module Pulsestorm JavascriptInitTutorial 0.0.1
$ pestle.phar generate_route Pulsestorm_JavascriptInitTutorial frontend pulsestorm_javascriptinittutorial
$ pestle.phar generate_view Pulsestorm_JavascriptInitTutorial frontend pulsestorm_javascriptinittutorial_index_index Main content.phtml 1column
$ php bin/magento module:enable Pulsestorm_JavascriptInitTutorial
$ php bin/magento setup:upgrade
These commands should be familiar to anyone who’s worked their way through the Magento 2 for PHP MVC developers series. Once you’ve run the above, you should be able to access the following URL in your system
http://magento.example.com/pulsestorm_javascriptinittutorial/
and see the rendered app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
template.
Setting up a RequireJS Module
Now that we’ve setup a Magento module, lets do a quick review and create a RequireJS module. First, create the following file
//File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/web/example.js
define([], function(){
alert("A simple RequireJS module");
return {};
});
This is a very simple RequireJS module. Due to the module’s location on the file system, and the way Magento loads javascript files, this module’s name/identifier is Pulsestorm_JavascriptInitTutorial/example
Next, change the contents of content.phtml
so they match the following.
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
<script type="text/javascript">
requirejs(['Pulsestorm_JavascriptInitTutorial/example'],function(example){
alert("Loaded");
console.log(example);
});
</script>
Here we’re creating a RequireJS program with a single module dependency. The dependency is our just created module (Pulsestorm_JavascriptInitTutorial/example
). Load the
http://magento.example.com/pulsestorm_javascriptinittutorial/
URL in your system, and you should see the alerts.
If any of the above was foreign to you, you may want to review our Magento 2 and RequireJS article.
X-Magento-Init
The first initialization technique we’re going to discuss is the <script type="text/x-magento-init">
method. The most basic version of this initialization method allows you to run a RequireJS module as a program. Change the contents of the content.phtml
template so they match the following
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
<div id="one" class="foo">
Hello World
</div>
<div id="two" class="foo">
Goodbye World
</div>
<script type="text/x-magento-init">
{
"*": {
"Pulsestorm_JavascriptInitTutorial/example":{}
}
}
</script>
Give your page a reload with the above, and you should see the alert
statement from our example.js
file.
If you’ve never seen this syntax before, it can look a little strange. Let’s take it apart piece by piece.
First is the <script/>
tag
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
<script type="text/x-magento-init">
//...
</script>
This tag is not a javascript tag. Notice the type="text/x-magento-init"
attribute. When a browser doesn’t recognize the value in a script’s type tag, it will ignore the contents of that tag. Magento (similar to other modern javascript frameworks) uses this behavior to its advantage. While it’s beyond the scope of this tutorial, there’s Magento javascript code running that will scan for text/x-magento-init
script tags. If you want to explore this yourself, this Stack Exchange question and answer is a good place to start.
The other part of the x-magento-init
code chunk we can talk about immediately is the following object
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
{
"Pulsestorm_JavascriptInitTutorial/example":{}
}
Magento will look at the key of this object, and include it (the key) as a RequireJS module. That’s what loading our example.js
script.
You’re probably wondering why this is an object with no value. You may also be wondering why the whole thing is a part of another object with a *
as a key.
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
{
"*": {/*...*/}
}
Before we can talk about that, we’ll need to talk about javascript components.
Magento Javascript Components
The above example runs our RequireJS module as a program. This works, and Magento itself often uses the x-magento-init
method to invoke a RequireJS module as a program. However, the real power of x-magento-init
is the ability to create a Magento Javascript Component.
Magento Javascript Components are RequireJS modules that return a function. Magento’s system code will call this function in a specific way that exposes extra functionality.
If that didn’t make sense, try changing your RequireJS module so it matches the following.
//File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/web/example.js
define([], function () {
var mageJsComponent = function()
{
alert("A simple magento component.");
};
return mageJsComponent;
});
Here we’ve defined a function and assigned it to a variable named mageJsComponent
. Then, we return it.
If you reload the page with the above in place, you should see A Simple Magento Component in an alert box.
This may seem silly — what’s the point of returning a function if all Magento does is call it? You’d be right, but that’s because we left something out. Try changing our phtml
template so it matches the following
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
<div id="one" class="foo">
Hello World
</div>
<div id="two" class="foo">
Goodbye World
</div>
<script type="text/x-magento-init">
{
"*": {
"Pulsestorm_JavascriptInitTutorial/example":{"config":"value"}
}
}
</script>
and changing our RequireJS module so it looks like this
//File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/web/example.js
define([], function () {
var mageJsComponent = function(config)
{
alert("Look in your browser's console");
console.log(config);
//alert(config);
};
return mageJsComponent;
});
If you reload the page, you should see the changed alert message. If you look in your browser’s javascript console, you should also see the following
> Object {config:"value"}
When we create a Magento Javascript Component, Magento calls the returned function and includes the object from text/x-magento-init
.
"Pulsestorm_JavascriptInitTutorial/example":{"config":"value"}
This is why the RequireJS module name is a key — the value of this object is the object we want to pass to our component.
This may seem like a silly bit of abstraction in these examples. However, in a real module, we’d be generating that JSON with PHP. This system allows us to render the JSON in our phtml
template, and then have that passed to Javascript code. This helps avoid the problem of generating Javascript directly with PHP, which can lead to very messy code, and makes it easy to accidentally slip in an error or security problem.
Before we finish up with x-magento-init
, there’s one last feature to discuss. You’ll remember we said that x-magento-init
provides a way to pass that program a server side generated JSON object.
provides a way to provide that program with the DOM nodes it should operate on.
We’ve covered how to pass in server side generated JSON — but what about the DOM nodes?
Change your RequireJS module so it looks like the following
//File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/web/example.js
define([], function () {
var mageJsComponent = function(config, node)
{
console.log(config);
console.log(node);
//alert(config);
};
return mageJsComponent;
});
What we’ve done here is add a second parameter to our mageJsComponent
function. This second parameter will be the DOM node we want our program to operate on. However, if you reload the page with the above in place, you’ll see the following in your console.
> Object {config:"value"}
> false
Magento did pass in a value for node
— but that value was false
. What gives?
Well, Magento can’t magically know which DOM nodes we want to operate on. We need to tell it! Change your phtml
template so it matches the following.
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
<div id="one" class="foo">Hello World</div>
<div id="two" class="foo">
Goodbye World
</div>
<script type="text/x-magento-init">
{
"#one": {
"Pulsestorm_JavascriptInitTutorial/example":{"config":"value"}
}
}
</script>
Here, we’ve changed the *
to a #one
. The *
we used previously is actually a special case, for programs that don’t need to operate on DOM nodes. The key for this object is actually a CSS/jQuery style selector that tells Magento which DOM nodes the program in Pulsestorm_JavascriptInitTutorial/example
should operate on. If we reload our page with the above in place, we’ll see the following in your console
> Object {config: "value"}
> <div id="one" class="foo">Hello World</div>
You’re not limited to IDs — you can uses CSS class identifiers here as well. Change that to
#File: app/code/Pulsestorm/JavascriptInitTutorial/view/frontend/templates/content.phtml
".foo": {
"Pulsestorm_JavascriptInitTutorial/example":{"config":"value"}
}
and you’ll see the following in your console.
> Object {"config":"value"}
> <div id="one" class="foo">Hello World</div>
> Object {"config":"value"}
> <div id="two" class="foo">Goodbye World</div>
By building this sort of system, Magento is encouraging developers to avoid hard coding their DOM nodes into their RequireJS modules. The x-magento-init
means there’s a system level path forward for building Javascript modules that rely on server side rendered JSON, and operate on any arbitrary DOM node. It’s always been possible for Magento module developers to implement their own systems for this sort of functionality, but Magento 2 provides a standard, built in way to achieve this.
Data-mage-init Attribute
In addition to <script type="text/x-magento-init">
, there’s another way to invoke similar functionality on a specific DOM node, and that’s with the data-mage-init
attribute. Try replacing the existing phtml
template with the following
<div data-mage-init='{"Pulsestorm_JavascriptInitTutorial/example": {"another":"example"}}'>A single div</div>
Reload the page with the above in place, you should see the following in your javascript console.
> Object {another: "example"}
> <div>A single div</div>
Here, we’ve added a data-mage-init
attribute to a specific div. This attribute’s value is a JSON object. Similar to x-magento-init
, this object’s key is the RequireJS module we want to invoke as a program or Magento Javascript Component, and the value is a nested JSON object to pass into our Magento Javascript Component function as the config
parameter.
On a pedantic note — you’ll notice we used single quotes with our attribute
<div data-mage-init='...'>A single div</div>
This is, unfortunately, required. The value of the data-mage-init
attribute is parsed by a strict JSON parser, which means the JSON object’s quotes must be double quote — which means we can’t use double quotes for our attribute. While this is technically OK in HTML5, for web programmers of a certain age it brings back some bad Microsoft Frontpage memories.
Wrap Up
Whether you end up using the <script type="text/x-magento-init">
component or a data-mage-init
attribute in a specific node, both these techniques provide a standard, system unified way of introducing Javascript entry points onto your page. Many of Magento’s front end and back end UI features rely on this syntax, so even if you personally eschew them, understanding how these systems work is an important part of being a Magento 2 developer.
Between these techniques and the base RequireJS implementation in Magento 2, we’re ready to cover our next topic — Magento 2’s use of the KnockoutJS library.