Js Developer
Js Developer
JAVASCRIPT
STUDY GUIDE
JESSE MAXWELL
SWIFT OTTER STUDIOS
INTRODUCTION
You downloaded the most comprehensive resource for Magento 2 Javascript
Development. The material covered provides excellent methodology for training
developers who are new to these concepts.
This study guide answers the questions presented in Magento’s study guide for this
test. Reading this study guide is part of solid preparation for passing the test but
does not guarantee a passing grade. What will yield a passing grade is extensive
experience with Magento’s Javascript system combined with rigorous and careful
review of this study guide.
Most people who spend $260 on a test want confidence that they are likely to pass
it. Therefore the next step of taking our practice test is critical. The practice test
validates that you are ready for the test. Following completion of the practice test,
you see questions you answered correctly and the ones you didn’t.
Additionally, for a small fee, you can see your scores on the practice test by
objective (showing your weak areas) and get the objective scores emailed to you.
Think about it. After you take the real test, don’t you want to know where you were
weak? Now, you can know before you take the test how you are doing and where to
study more.
Acknowledgements
Jesse, my brother, spent many hours writing this study guide. He researched topics
that aren’t inherently clear to make it easier for you. He has mastered the trickier
aspects of Magento Javascript development and is investing back into you by
sharing this knowledge. Please take a moment to thank him on Twitter or email.
This study guide demonstrates our commitment to excellence and our love
for continuous learning and improvement. Enhancing the Magento developer
community is good for everyone: developers, agencies, site owners and customers.
There are a number of tools to assist in getting production data back to staging.
One of these is Sonassi’s. They work well.
Driver allows you to do all the above. For example, we generate data for three
environments: staging, local initialization and local refresh. For the latter two
environments, the customer and order data has been so sanitized that we have
Driver set a standard admin login and password. The only thing that touches the
production database is the initial mysqldump command.
This process has become so automated that we can run one command on our
development machine to refresh the data: reload_customer_name.
The way Driver works is the production data is dumped and uploaded to a fresh
Amazon RDS (relational data storage) instance. The transformations are run for
each environment; that environment is dumped and loaded into S3. You can easily
automate the synchronizations back to your staging and local environments.
https://github.com/SwiftOtter/Driver
CI
Are you familiar with Jenkins automation? Maybe you are having trouble getting
code to production. We have developed an open-source continuous integration and
deployment system built on Jenkins to make building and deploying Magento 1 and
2 websites easy.
Additionally, our system enables deploying Magento 2 code with zero downtime (or
very little—if database updates are necessary).
1.
TECHNOLOGY
STACK
Technology Stack
RequireJS allows deferred execution for the majority of the Javascript on the page
by asynchronously requesting modules. Loading JavaScript via RequireJS allows
a developer to ensure that required modules are accessible when referencing
them. RequireJS also allows grouping Javascript code into logical modules, making
writing and maintaining Javascript easier.
What are the pros and cons of the AMD approach to JavaScript
file organization?
AUTHOR NOTE
These pros and cons are in comparison to the Magento 1 technique
of including script tags directly into the head. They are not meant
to compare AMD with newer tools such as Webpack.
Pros:
• Javascript can be loaded asynchronously so it is not render-blocking on page
load.
• Logical groups of code are broken into modules so code is easier to understand.
• Smaller amounts of code can be overridden for the same effect because files are
smaller.
• Decreases time to first paint (although it can make the page’s Javascript-
powered functionality unusable for more time).
• JavaScript is only loaded when it’s required, where traditionally a library such
as jQuery may be included in the head and always loaded with RequireJS, it will
only be loaded if it is required by reference on the page in question.
Cons:
• AMD is not a real standard, just a proposition that gained traction in the
Javascript community.
• There is little control over the size of the modules because they are individually
loaded on the frontend. It is more efficient to group them to some degree to
minimize total requests. (Magento provides a way to group the modules for
deployment, but this comes with side effects that more than negate any benefit.)
RequireJS allows you to easily build and inject Javascript modules into a site
without incurring blocking page render. Modules can have dependencies without
needing complex configuration or conditional loading logic. There is a powerful
configuration layer which allows you to change which dependencies are loaded, or
add modules without needing to change the original Javascript files.
var config = {
//...
It is perhaps most helpful for modifying core code by overriding modules and
for adding mixins (described in more detail later). Essentially, any time updating
dependencies is necessary, requirejs-config.js is usually a good choice
because it does not require changing the Javascript modules themselves.
There are quite a few things you can do using only the configuration file but
following are some of the ones more commonly used throughout Magento. We
suggest you refer to the RequireJS docs for a comprehensive list.
• map: the object assigned to the map key allows substitution of module IDs. For
instance, the ID of a commonly used module is 'Magento_Ui/js/lib/core/
element/element'. In the requirejs-config.js for that module, however,
is an entry which allows other modules to use a much more succinct uiElement
to require the module:
var config = {
map: {
'*': {
uiElement: 'Magento_Ui/js/lib/core/element/element'
};
Note the '*' item in the map object. Inside the map object, keys target specific
modules with the exception of the asterisk: it (*) is used to target all modules. When
a module ID is used, however, it provides the powerful flexibility of being able to
change dependencies in a or any specific module.
• path: this provides path mappings for module names not found directly under
baseUrl. It is similar, but ultimately less targeted than the map key. In other
words, the path object is more global and will change all references to a portion
The following is from the Magento_Ui theme (random aside: this configuration item
attempts to patch the lone and confusing “templates” directory used in contrast
to the typical, singular “template” directory where HTML templates are placed
reference):
var config = {
paths: {
'ui/template': 'Magento_Ui/templates'
};
shim: {
'jquery/jquery-storageapi': {
'deps': ['jquery/jquery.cookie']
This is a function that is executed after deps have loaded. As of the time of this
writing, it isn’t used in the core but still could be leveraged.
More information is provided above. Using the map configuration object allows you
to selectively replace modules or update their location. It also allows you to provide
a shorter alias for a module.
When a plugin is used, that plugin module is loaded first. The dependency’s name is
passed into a load method—the only one required in the plugin. The load method
then handles processing the string it receives.
The text loader plugin processes a module as text. This allows HTML to be put
into separate templates which can be easier than managing unwieldy multi-line
strings in a Javascript file.
The domReady plugin waits for the DOM to load before executing the module.
This is often not essential, but if your module has a dependency on the DOM (i.e.
document.querySelector(.class-name)) it is good practice to include
because it removes the possibility of strange race conditions.
as to say you are doing your users a favor to drop IE 11 support and encourage them
to upgrade to a more secure browser.)
With that note aside, it is better to use the library than to write a custom function to
do the same thing. The benefit of using UnderscoreJS, in that case, is that it is fewer
lines of code you have to debug and maintain.
FURTHER RESOURCES:
Refer to the docs for the specific usage of templates.
an object for context. If you find a cool trick in the docs for this template, you’re
probably safe to use it.
Pros:
Cons:
• It’s not reactive like KnockoutJS (although, you can use the two frameworks
together).
• No translation.
Magento uses the text! RequireJS plugin (prefixed to the template’s path ID)
to load HTML files that are executed with the _.template() method (usually
indirectly via mage/template.js). This means chunks of HTML are loaded
asynchronously and executed with a data object for context.
and rendered in the context of an array of breadcrumbs (not edible). Since the
breadcrumbs don’t change after the page is loaded, there is not a specific need for
reactive data handling.
AUTHOR NOTE
Technically, jQuery is a "library."
jQuery is a large library of Javascript tools with the ubiquitous jQuery object,
$, being the linchpin. It quickly became immensely popular after its release
in 2006, because of the consistent API it provided between browsers. At that
time, developing Javascript to be cross-browser compatible was a complicated
process because various browsers used complete disparate techniques for
performing tasks.
The common alias for the jQuery object, the $, is usually global, although Magento
introduces some nuances with that. The $('.some-selector') method is
often used to select a collection of elements. Methods can then be called on
the collection to manipulate them or use them for some purpose. Methods can
be added to the jQuery object via widgets—something to be discussed in more
detail later.
There is an entire additional library that is built on jQuery: jQuery UI. Its goal is
to make certain types of functionality that are desired in many UIs available via a
simple interface. Examples of that include tabs, accordions, and drag and drop. It is
built on jQuery because it uses the jQuery object and DOM manipulation. In simpler
terms, jQuery UI makes jQuery more powerful.
jQuery contains an entire event system. Most of that has been eclipsed by
the native, well supported .addEventListener() but jQuery’s are still
frequent in the core and important to be familiar with should the need arise
to update one. There are some differences as well. Many of jQuery’s event
listeners can be attached using a method name that corresponds to the
event. For instance, jQuery has a .click() method. The native equivalent is
.addEventListener('click'). A significant aspect to jQuery’s event listeners
In addition to the DOM events, jQuery has global AJAX event listeners. These
over-achieving methods can be obscure when debugging because they perform
significant actions in code far from the place where the request is sent. Magento
core uses these event listeners to handle various tasks. For instance, in lib/web/
mage/backend/bootstrap.js, all requests are listened to. If one comes back
with a JSON object containing an ajaxRedirect key, the page is redirected—
automagically. It also injects the form_key if making a JSON type request.
In Chrome DevTools, you can see the names of these AJAX events by selecting a
DOM element—such as body. Select the “Event Listeners” from the group of tabs
that contains "Styles" and "Computed." There, if present, they will appear like:
ajax[Type] (ajaxComplete). This will lead you to the place in the jQuery library
where the event is triggered. You can place a breakpoint in that function and step
down to find the global event listener.
AUTHOR NOTE
It is our opinion that like UnderscoreJS, there’s little (if any) reason
to use jQuery if your targeted browsers contain native APIs that
do the same thing. We venture to offer the recommendation
to obtain the habit of using native APIs when available and
would reference this helpful website as a resource when doing
custom development.
With observables, KnockoutJS tracks any changes that occur in the underlying
data. It then updates the View. Consider the classic technique of: element.
textContent = newValue. With KnockoutJS and knockout-es5 library,
assuming that element has an attribute, data-bind="text: dataKey", it will
simply update the value.
• text: binds a value as the text content of an element. Would escape HTML as
a result.
• visible if the value is false, the element is hidden. if has the same end result
but actually removes the element’s content.
• foreach loops over an array and binds each item to the corresponding array
item. You probably aren’t surprised to learn that this is especially useful for
rendering lists and tables.
• click: adds an event handler. If you guessed it was for the click event, you are
correct. It can bind to a method, or you could assign a value in the attribute itself:
data-bind="click: total = timer + 1".
Consider the catalog product grid in the admin panel: when a manager clicks the
"next page" button to see more results, a new set of objects need to be displayed.
Instead of reloading the entire page, the visible list re-renders due to the change in
the data.
Pros:
• Links the UI more tightly and logically to the underlying data set reducing brittle
DOM query and update mechanisms
Cons:
• Potential loss of SEO. Because Javascript must be executed in order for the
content to render, it is possible that some search engine bots would miss the
data. Search engine bots are growing in their ability to run Javascript and
may not be an issue for much longer. It is not a problem for areas that require
authentication to access either, like customer or admin areas.
• Delayed rendering (in some cases). Data must be loaded and available before it
can be rendered.
• Underscore templates are rendered based on the data only when the string is
passed to the template() function. No further connection to the underlying
model is maintained.
• Underscore uses "ERB-style delimiters." ERB stands for "Embedded Ruby" (wait, what?).
initObservable: function () {
this._super()
.track([
'value',
'editing',
'customVisible',
])
HINT
There is a rudimentary but helpful Chrome extension for
KnockoutJS.
2.
MAGENTO
JAVASCRIPT
BASICS
Magento JavaScript Basics
FURTHER RESOURCES:
Refer to this DevDocs article.
Javascript modules are placed in a web folder of a Magento component. This allows
all of the component’s files to be managed under a single directory.
If a theme overrides a Javascript file, that version of the file will be loaded. It is
critically important to understand Magento’s fallback system. As a rudimentary
example, the following idea demonstrates the fallback system where the earlier
• Module_Name/base (area)
• Module_Name/frontend (area)
• base/theme_name
• frontend/parent_theme
• frontend/theme_name
Notice how a module’s assets are overridden by themes and a more specific area
overrides a less specific area. We want to stress that the preceding example does
not cover all factors of inheritance and suggest you pursue the following resources
if you are unfamiliar with these concepts.
FURTHER RESOURCES:
• Theme structure
• Theme inheritance
• Module Dependencies
Magento has a special folder with tons of libraries and files in it: lib/web/. It is
also in vendor/magento/magento2-base/lib/web/, but copied from there so
the primary use is lib/web. There’s a ton of stuff in there, and it would be a good
idea to at least take a brief look: lib/web/.
Publicly accessible files are symlinked (developer mode) or copied via CLI
(production mode) to the pub/static/ directory. From there, files are grouped
by area, theme, locale, and module name in the form of Vendor_Module (like
Magento_Ui).
The files in the view/[area]/web directory are copied to the pub/static folder
(symlinked in developer mode). For instance, a source Javascript file is placed in
app/code/SwiftOtter/Theme/view/frontend/web/js/hero.js. This file
is then referenced with SwiftOtter_Theme/js/hero. In developer mode, that
file is symlinked to its end location in pub/static. The full directory structure
of the public file is: pub/static/frontend/SwiftOtter/[Theme Name]/
[Locale]/SwiftOtter_Theme/js/hero.js. As a result, the identifier is the
last part of the path and does not include the file extension (.js).
FURTHER RESOURCES:
DevDocs guide
There are a few common tasks you can accomplish by adding a requirejs-
config.js to a custom extension:
not feasible but a modification must be made. It can also be helpful when a change
is so significant that it is more efficient to replace the module.
var config = {
map: {
'*': {
'Magento_GoogleAnalytics/js/google-analytics':
'SwiftOtter_Theme/js/google-analytics'
};
Run Javascript on every page. If you want to run a module on every page of the
site, you can include it as shown below:
var config = {
deps: [
"SwiftOtter_Theme/js/core/light-run"
Remember that listing a dependency after another one does not enforce load order
at all. It is necessary to explicitly define what modules must be loaded before a
module can be executed. It is possible to declare these dependencies using the
shim property of requirejs-config.js. Note that this has the same effect as
adding a module to the list of dependencies in a define() function, but allows you
to control order without modifying the target module. If you have access to the
define() function, it’s better to just use it because it is easier to comprehend when
working with it in the future.
var config = {
'shim': {
'MutationObserver': ['es6-collections'],
var config = {
map: {
'*': {
uiElement: 'Magento_Ui/js/lib/core/element/element'
};
Plain modules
These are regular ones. The sky's the limit, though, so you can use them for
anything. Like other modules, call the define function and include a callback within
it. This callback often returns another function. In fact, it should return a callback if
you use it with the data-mage-init attribute or text/x-magento-init script
tag. Here’s an example:
function handleWindow() {
// ...
return function () {
handleWindow();
window.addEventListener('resize', handleWindow);
});
jQuery UI widgets
Declares a jQuery UI widget which can be used elsewhere. Always return the newly
created widget as shown in the following example:
$.widget('mage.modal', {
options: {
// default options
},
/* Public Method */
_setKeyListener: function() {}
});
return $.mage.modal;
});
UiComponents
'use strict';
return Element.extend({
links: {
},
// KnockoutJS template
initObservable: function () {
this._super()
});
})
Note that there are a number of different modules that extend uiElement.
uiCollection is a common one and, as the name implies, facilitates dynamic
lists. All UI Component Javascript modules follow the basic pattern of extending a
base class with an object containing a defaults object and a number of functions.
For one thing, the base class handles the template. This is why the methods (and
properties of the defaults object) can be called from the template. We’ll cover
this in more detail later because it’s complex.
• Both require a significant amount of Javascript to load and execute before they
are processed, loaded, and executed. While this often occurs at an acceptable
speed, depending on the module’s purpose, it may be an issue that users notice.
either are valid. The following is an example taken from the state.phtml file
(located in Magento_LayeredNavigation)
<!-- … -->
</div>
// ...
})
This is the heavy hitter of Magento Javascript framework and is ultimately used for
many things, including initialization of UI Components. The same module that runs
the data-mage-init picks up and executes these script tags. Note that browsers
skip script tags with the type of text, so Magento can safely use them for storing
configuration objects. The object within them is similar to the data-mage-init
attribute with some important differences.
The initial key is a selector and its value (on the right side of the colon), is an
object with the module ID and its configuration data. Magento will use the
selector to query those elements and will execute the callback function for every
single element it finds that matches the selector. For instance, if “table” is the
key, Magento will load the component (once) and execute the callback for each
<table> found on the page with that element as the second argument and the
module’s component configuration as the first one.
<script type="text/x-magento-init">
".hero": {
"SwiftOtter_Theme/js/hero": {
</script>
This is pretty simple, right? It gets more complicated from here, but this minimal
setup covers quite a few situations and serves as the foundation for more
complicated renderings.
In this case, the hero module that was loaded has this structure:
define(['underscore'], function(_) {
// ...
};
});
Remember that the config comes first, and the element is second.
The syntax is quite loose: outer curly braces (an object) that surround necessary
configuration. The keys are element selectors, with one exception. If a * (asterisk)
is used, the module is run once and not bound to any specific element. Objects
are used for values there as well: keys are the module ID to load, and the value
is passed as an argument to that module. This is often an object and can be as
complex as needed.
There are a couple ways to do this. The first step will be to glean the Javascript
code from the response or string. Sometimes it will come in the form of a static
block that has a <script/> tag in it which must be separated from the rest of the
response. Then the raw string of Javascript can be passed to the eval() function,
or better Function(). Another option is to inject it into the <head>. If you do
choose to use Function() or eval(), read and understand the downsides
related to security and performance. While they are not “bad” in and of themselves,
they introduce risks that are important to understand first and reasonably mitigate.
FURTHER RESOURCES:
Refer to Magento DevDocs for information on some of the
widgets available.
FURTHER RESOURCES:
Here are a few of the widgets available and examples of their use:
It is important to take some time to review the various widgets’ functionality and
familiarize yourself with their different uses. For instance, consider three widgets
that perform a similar job of hiding and showing segments of content: tabs,
collapsible, and accordion. They each have nuances:
• Tabs show only one segment of content at a time. Selecting a new “tab” will
cause others in the same group to disappear and the new one to display alone.
The Alert, Confirmation, Prompt, and DropDown widgets are another case where
there are multiple widgets that may appear similar on the surface but have
important differences.
Widgets are declared with calling jQuery’s widget function like: $.widget('mage.
widget_name', {/* … everything else */}. They contain an options
key with an object of settings that are available in the widget. Practical defaults are
declared to minimize boilerplate elsewhere in the app. After the options object,
functions comprise the bulk of the widget with “private” functions starting with an
underscore (note that they can still be extended even when “private”), and public
methods without.
$.widget('mage.list', {
options: {
template: '[data-role=item]',
templateWrapper: null,
destinationSelector: '[data-role=container]',
itemCount: 0
},
_create: function () {
this.options.itemCount = this.options.itemIndex = 0;
this.element
.addClass('list-widget');
},
});
jQuery widgets are often executed through the template system as described in the
next section. However, another common use is to include them as a dependency in
the RequireJS module. The mage/validation widget is perhaps one of the most
frequent uses in this manner. They are appended to the list of other dependencies
but do not need to be added to the callback function’s definition because they are
accessed through the jQuery object.
define([
'jquery',
'uiComponent',
'ko',
'Magento_Customer/js/model/customer',
'Magento_Customer/js/action/check-email-availability',
'Magento_Customer/js/action/login',
'mage/validation'
loginAction) {
/* ... */
function validateEmail() {
possible-login]',
input[name=username]',
loginForm = $(loginFormSelector),
validator;
loginForm.validation();
validator = loginForm.validate();
return validator.check(usernameSelector);
});
The widget is initialized on the element that has the data-mage-init attribute or
is targeted in text/x-magento-init script. The module that contains the widget
is requested and after loading, the result is tested: if it is a function, it’s called like a
function, but if it is a jQuery widget, it’s executed like that. You can see the action in
the init() function of apply/main.js.
Mixins are to Javascript as plugins are to PHP in Magento. The logic is that you can
add actions before, after, or instead of core functions. This allows you to manipulate
core Javascript without always needing to override the entire file. The benefit is you
have to write and maintain less code. They are quite easy to use as long as the core
is facilitating. There are some places where things get weird, but they can still
be used.
FURTHER RESOURCES:
This DevDocs article will get you up to speed on the specifics of
building mixins
Conversely, because the mixin is an outside entity, it can only do so much. The
following is an example of something that works around a problem where various
functions were not very accessible from a mixin:
define(function () {
var original;
call(this, mode);
output['theme_advanced_buttons1'] =
output['theme
_
advanced_buttons1'] + ',styleselect';
return output;
original = tinyMceWysiwygSetup.prototype.getSettings;
tinyMceWysiwygSetup.prototype.getSettings =
getSettingOverride;
return target;
};
});
The biggest limitation mixins have is related to the architecture of the code within
the module you are targeting.
It is not possible to add mixins to Javascript that is not included with RequireJS,
including Javascript in the HTML.
There are many options available on most of Magento’s jQuery widgets; these
can be defined when the widget is called. Beyond whatever is available there,
overriding functions in the widget, or adding functions, can be done with mixins.
If the jQuery widget must be completely overhauled, it is possible to replace the
entire widget; at that point, it may be better to create a completely custom widget
and extend the core one.
This should be done with a mixin. There is a great answer on StackExchange that
details the approach.
When adding or modifying a widget, the callback inside the module should return
the widget. It is passed the original widget via a parameter. In effect, this callback
returns a brand new widget; there’s a great deal of control with that. This is
where jQuery’s widget factory is helpful: it will merge objects that are passed as
arguments into it. For instance:
methods */ } );
The answer is nearly the same as the last question and should be done with a mixin.
define(['jquery'], function($) {
// ...
return function(widget) {
$.widget('mage.priceBundle', widget, {
_create: function() {
this._super();
this.element.find('.options-list').
each(limitOptions.bind(this));
});
return $.mage.priceBundle;
});
jQuery widgets are customized by essentially creating a new version of the widget
with the same name as the original one. By including the original in the widget
factory method, it extends all functions and options that are not overridden. Other
Magento Javascript modules are extended via merging a mixin object onto the
original one.
3.
MAGENTO CORE
JAVASCRIPT
LIBRARY
Magento Core JavaScript Library
• Underscore
• jQuery (rarely used)
• Knockout
• Kinda: ES6
The underlying problem is that Internet Explorer does not support template literals
(also referred to as template strings). To workaround for that, while still allowing for
the use of template literals, Magento uses UnderscoreJS to compile strings used
as templates in non-compliant browsers. The action happens in lib/web/mage/
utils/template.js. Magento detects native template literal support; if not
present it falls back to UnderscoreJS for evaluating the string. UnderscoreJS allows
for the specification of interpolation regex which Magento provides as equivalent of
ES6: ${ […] }.
This remarkable system does come with a caveat: when using Magento’s template
literals, it is necessary to use regular quotes (') and not backticks (`).
These ES6-like template strings are commonly (or even primarily) used in the
defaults object of UI Component’s to specify dynamic variables in an otherwise
static medium. We will be discussing them in more detail later, so you can look
forward to that.
You will be shocked to learn that Magento uses a jQuery cookie plugin for helping
with cookies. Tasty comments aside, the jQuery Storage API is the primary
abstraction for leveraging browser storage.
define([
'jquery',
'uiComponent',
'jquery/jquery-storageapi'
return Component.extend({
defaults: {
cookieMessages: []
},
initialize: function () {
this._super();
this.cookieMessages = $.cookieStorage.get('mage-messages');
$.cookieStorage.set('mage-messages', '');
});
});
A few things to consider in the previous example. Note that the jquery/jquery-
storageapi dependency does not have a corresponding parameter in the
function declaration. Further, this module follows the UI Component pattern of
extending the uiComponent (which is a module alias). The Storage API is accessed
with $.cookieStorage. In this case, the module saves the message locally then
clears them.
AUTHOR NOTE
Note: if the cookies are set to be HTTP-Only, it’s not possible to set
them with Javascript (also, consider reading the security cautions
related to using document.cookie).
FURTHER RESOURCES:
Refer to the DevDocs topic on translation.
These Javascript translations for the user’s locale end up in JSON format in local
storage. They are then used to translate dynamic strings from Javascript modules
and Knockout templates.
This question is a bit unclear, but we will provide insight into how strings are
translated in Javascript. Ultimately, the answer is to use the i18n:collect-
phrases command. This command collects strings that meet the translation criteria
(as described here) and saves them into CSV format for translation. The .csv
language files provide a list of all strings that need to be translated—they can be
sent to translators for processing.
The translated strings still need to make it to the frontend. Here is how that
happens: there is a .phtml template (Magento_Translation/view/base/
templates/translate.phtml) comprised of a RequireJS call for translation
stuff from the server. It only fetches the strings for the current store, or, more
specifically, the locale. The server responds with a JSON object of the strings which
the module then persists to localStorage. The server actually saves the JSON
content as well, in a js‑translation.json file within the theme directory in
pub/static.
FURTHER RESOURCES:
Be sure to visit DevDocs for more guidance on translating the
strings.
AUTHOR NOTE
If you can’t get some Javascript translations to work, delete
js‑translation.json in your theme’s folder within
pub/ static.
AUTHOR NOTE
You can see what strings are available to Javascript two
ways. First, you could open the js-translation.json
file. Second, in Chrome DevTools, select Local Storage under
the Application tab. After picking the correct domain, filter to
mage‑translation‑storage.
FURTHER RESOURCES:
Do as DevDocs says.
• Generate a unique ID
• Compare values
The rules object is iterated and each one is used to extend jQuery’s
validation library.
HINT
There’s a great answer here.
The addition involves creating a mixin for the mage/validation module and
using addMethod() for the validator:
define(['jquery'], function($) {
"use strict";
return function() {
$.validator.addMethod(
'validate-example',
});
Like the previous example, create a mixin for mage/validation and use
addMethod() with the exact name of the validator to override. Copy and update
the logic inside the function that determines whether or not the value passes.
FURTHER RESOURCES:
We recommend that you review DevDocs for the widgets.
The collapsible widget hides content that follows a header until the heading is
clicked. It then expands to show the content:
Closed:
Open:
One way to use the collapsible widget is to add our trusty friend data-mage-
init to an element that surrounds both the heading and content. It is used in
this manner in the Luma theme’s state.phtml template (vendor/magento/
theme-frontend-luma/Magento_LayeredNavigation/templates/
layer/state.phtml). This is the attribute from the file: data-mage-init
='{"collapsible":{"openedState": "active", "collapsible":
true, "active": false }}'. The header has an important attribute: data-
role="title". The widget will also look for a related element with the attribute
data-role="content". If not found, it picks the element immediately following
the title as the content. In the example provided, “openedState” is a class applied
to the parent—this can be helpful if some secondary effect is desired when the
section is open. The last two key/value pairs are redundant because they match the
default settings.
The accordion is very similar to the collapsible widget. Use the data-mage‑init
attribute on an element with children that have data-role attributes. See this
question for more concrete examples. Demonstrate the ability to use the popup and
modal widgets
These widgets follow common jQuery style and DevDocs has articles on them, so
we will point you there:
• Modal
• Alert
• Confirmation
• Prompt
There are also wrappers for these widgets. They are found in vendor/magento/
module-ui/view/base/web/js/modal/ and handle some of the boilerplate
for you.
The DevDocs references above contain examples of their direct use. To use the
Magento wrappers, include the appropriate Magento_Ui module as a dependency.
Then, call it as a function with whatever configuration argument is needed. For
instance, let’s use the following core module as an example: vendor/magento/
module-ui/view/base/web/js/grid/massactions.js. This module
handles changes on a grid where multiple items are manipulated together.
alert({
content: this.noItemsMsg
});
• Calendar
• Magnifier
• Loader
• Menu
• Navigation
• Quick Search
DevDocs contains a good amount of detail regarding their use. This usually involves
the text/x-magento-init script tag, or selecting an element (or many) with
jQuery and initializing a widget with it.
It’s data that is only applicable to one user. An example would be products that
were recently viewed. These only are relevant and appropriate for the user who
actually visited those product pages.
The data must still be synced with the server at some point, but that can be done as
a background task. Beyond that, querying localStorage or even cookies is fast and
does not necessitate a network connection.
The module uses global event listeners for AJAX requests. This makes sense
because customer data would be manipulated through a request. If left unattended,
the data stored locally would then be stale. If the module decides it is necessary
to update the data, it will make a request of its own, after the original request
has completed.
define(['uiComponent', 'Magento_Customer/js/customer-data'],
return Component.extend({
initialize: function () {
this._super();
this.wishlist = customerData.get('wishlist');
});
});
Set: essentially the same as accessing the data—only use the set(methodName,
data) method.
<action name="catalog/product_compare/remove">
<section name="compare-products"/>
</action>
The action name is the (beginning of) a request path to be matched. The section is
the name of the section to update. If you do not include any <section/> nodes,
all are invalidated.
The sections.xml files are merged, so you can use the same action name
to update the sections. Beyond additions, a plugin for \Magento\Customer\
CustomerData\SectionConfigConverter::convert() would provide a great
deal of control.
Changing URL structure could break the way the existing sections work. For
instance, if for some reason, the URL used for adding a product to the cart was
different than the default checkout/cart/add, the minicart would no longer
update properly.
It is possible to add data to current sections by using a PHP plugin for the
appropriate class because they must implement \Magento\Customer\
CustomerData\SectionSourceInterface. \Magento\Catalog\
CustomerData\CompareProducts is an example of this where
getSectionData() could be intercepted.
4.
UI COMPONENTS
UI Components
Remote Template Engine: KnockoutJS has a template binding but it expects all
of the template as a string. To facilitate modularity in the platform, Magento added
the ability to load templates from afar—the server. If it cannot find the requested
template in the DOM, it will fetch it with RequireJS.
Template syntax, custom tags, and attributes: Magento uses a layer of abstraction
for many of the KnockoutJS bindings. This comes in the form of custom tags (nodes)
and attributes. Instead of <div data-bind="template: templateUrl">
div>, it is possible to do: <div template="templateUrl"> </div>. There
</
are many aliases available, and they do not all share a similar structure so we
recommend you spend some time reviewing them.
A few examples:
• <if args="isVisible"></if>
• <div if="isVisible"></div>
• <ifnot args="isVisible"></ifnot>
• For a template:
• <div template="templateUrl"></div>
FURTHER RESOURCES:
DevDocs Topic
History Note: officially, Magento says this custom syntax is to make templates
simpler to write and easier to read. Interestingly, according to a source familiar
with the development of Magento 2, the original idea was to build a system where
KnockoutJS could be replaced. Given the functionality that was built to facilitate the
custom template syntax, this make sense. It sounds like that goal was given up on,
though, due to the permeation of KnockoutJS through the platform and perhaps
disparity between it and the newer frameworks available.
Refer to the DevDocs for specifics; they cover a wide range of functionality.
collapsible is an example. It is similar to the jQuery widget with regards to
configuration options.
First, a note on context in Knockout: in the template, the current binding context
is equivalent to this in plain Javascript. The context can contain methods or
static values. Other variables are added to the current context in Magento but are
referenced by prepending the name and a period to the value: $parent.value.
var scope = {
foo: function() {
}
The scope binding may be the most important binding to understand due to
Magento’s use of it. The scope binding sets up the connection between a template
and its Knockout context by evaluating descendant nodes in the scope of an
object found in the Ui Registry. In Magento’s Javascript framework, KnockoutJS
templates must have a parent with a scope binding. Not all HTML templates will
have a scope binding, but this is because they are requested within a different
template that would have context.
$block->escapeHtml($block->getLabel()) ?>
qty"></span>
</a>
</li>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"wishlist": {
"component": "Magento_Wishlist/js/view/
wishlist"
}
}
}
}
}
</script>
The scope binding is used frequently because it is the way that the KnockoutJS
context is bound to a template. The following contains an example where a child
is loaded and bound to the child’s UI Component’s context: vendor/magento/
module-ui/view/base/web/templates/grid/sticky/sticky.html. The
requestChild() method loads a module from the Ui Registry with the applicable
key. This is a good way to nest modules. The basic concept is:
We recommend experimenting with the scope binding if you are not already
familiar with it. While the previous example has concrete configuration you can build
based off of, create yours with the following general steps:
• Create a components object with one or more unique keys and assign it to the
Magento_Ui/js/core/app module.
• Add a template with a scope binding that matches the unique key of its parent.
Each nested scope has a new binding scope as described here. Parent scopes are
accessible with $parentContext or $parents (an array of parents).
There are two ways to do this after the response has been received: fire a
contentUpdated event or manually run with mage/apply/main.
FURTHER RESOURCES:
Overview of UI Components.
The layout component is loaded and executed from the app component. It loads
the UI Components and saves them to the UI Registry. The entry point is the run()
function. There are more details in the DevDocs topic.
What is uiClass?
FURTHER RESOURCES:
Refer to the DevDocs for more information about its role.
The layout component initializes the UI Component through the uiClass. This
occurs in the initComponent() method of the layout module where it calls
new Constr(...) (presumably short for “Constructor” which is a reserved
keyword in Javascript). This constructor function is returned from uiClass and
handles some inheritance, while primarily calling the initialize() function.
This, in turns, calls initConfig() which takes the defaults object, processes it
(including getting the template from it and rendering that), and embeds it onto the
module directly while parsing various keys (i.e. uiComponent.defaults.key
to uiComponent.
key). Each of the functions that have been overridden in the
concrete UI Component are wrapped. This is what allows the extended function to
be able to call this._super().
the CMS Page Listing in the Magento admin panel and run the following in the
Javascript console: require('uiRegistry').get('cms_page_listing.
cms_page_listing_data_source'). It will return the listing’s data source
component instance.
The Registry is used from within components to access other components. Often
template literals are used to form the reference to the other module, but after it
has been processed, the plain, long form reference is used to look up the other
module. This reference is a chain of UI Component names. The 'cms_page_
listing.cms_page_listing_data_source' reference is the UI Component
named “cms_page_listing_data_source” which is inside the UI Component named:
“cms_page_listing”. For an example of how template literals are used in this way,
refer to the sample code regarding forms in section 4.4. One excerpt, though, is
the following: ${$.parentName}.master_field:checked. Here, we use the
“variable” $.parentName, which would evaluate to referral_form.dynamic_
fieldset, plus master_field to create the full reference of referral_form.
dynamic_fieldset.master_field, targeting a very specific UI Component. The
word after the colon (:) is a property of the module object.
Create a Javascript module similar to the one shown below. Instead of the
dependency on uiElement, use whatever UI Component is to be extended.
Override and add functions and properties as necessary.
define([
'uiElement'
], function (Element) {
'use strict';
return Element.extend({
defaults: {
links: {
}
},
hasAddons: function () {
return false;
},
initObservable: function () {
this._super()
return this;
}
});
});
Again, we will take a moment to point out the template literal in the previous
snippet. In this case, $.provider would evaluate to a string that can be used
to perform a module lookup in the UI Registry. Then, there is the all-important
colon (:) separator, and then another template literal that is evaluated to target a
specific property.
• uiElement
• uiCollection
define(['uiElement'], function(Element) {
return Element.extend({
})
});
There is a URL endpoint that can be used to fetch chunks of data: mui/
index/render. Loading data through that endpoint is common for listings
(grids). The grid provider, Magento_UI/js/grid/provider, configures
Magento_Ui/js/grid/data-storage for its storageConfig. The name
Add a callback for .done() on the request object returned from the storage
component. This callback receives the data as an argument. Here is an example
taken from the listing provider:
request
.done(this.onReload);
}
this.setData(data)
.trigger('reloaded');
The native approach is to use one of: imports, exports, links, and listens.
They are described in more detail on the DevDocs site.
A problem arises when a module that depends on another module loads first.
Referencing a dependency when it hasn’t loaded will result in an undefined
object. There are two ways the core works around this: add a deps (preferable),
or use a Knockout observable. The deps works in the same way that RequireJS
dependencies do: it loads the child before executing the parent. The deps must be
declared in the JSON configuration (although, often that would be via XML). Here
is an example from: vendor/magento/module-sales/view/adminhtml/ui_
component/sales_order_shipment_grid.xml.
<listing>
<settings>
<deps>
<dep>sales_order_shipment_grid.sales_order_shipment_
grid_data_source</dep>
</deps>
</settings>
</listing>
Using deps.
The “UI Component” way to use Knockout observables would be to set a property
as an observable (with tracks), then use imports to obtain the data from the
other source. Vinai Kopp proposes a better alternative of creating a module just for
the purpose of tracking state. It would return an observable object. See his video
for more details.
All the UI Component configuration that is handled with PHP and XML is processed
and encoded as JSON during the page load. It is rendered in a text/x-magento-init
script with the outer structure being:
<script type="text/x-magento-init">
"*": {
"Magento_Ui/js/core/app": {
"types": { /* ... */ },
"components": { /* ... */ }
</script>
The Magento_Ui/js/core/app is the entry point that must process the massive
Javascript object. The types object is processed first and contains configuration
information related to specific types of UI Components to be shared among
components that extend them. The components object is where most of the
configuration typically is held. Each component is loaded, with any dependencies
loaded first. Configuration for the component is passed into its constructor method,
and the module is added to the UI Registry for later use.
Now that the module has been spun up, it starts its work with the abstract uiClass
processing the defaults object and running the various init methods.
RELATED READING
Check out this article.
What is the role of the layout module, and how does it load
components, children, and data?
definition.xml (note our correct rendering of the file name) is a unique file that
forms the basis of all UI Components. It cannot be directly changed but can be
extended on a per-component basis. (Source). Be sure to spend some time going
through the file: it is also a helpful resource to refer back to when developing
UI Components.
Configuration XML files for any given UI Component are merged together. Take
product_listing.xml for instance. While the primary file is in Magento_
Catalog, there are other files in Magento_CatalogSearch and Magento_Inventory.
Any other files created in the same directory structure will be merged in as well.
In this example, that could be Namespace_ModuleName/view/adminhtml/
ui_component/product_list.xml. To ensure a module you work on is merged
after other modules, add those modules to your etc/module.xml sequence tag.
To summarize the above question: create a file that matches the (1) directory
structure and (2) schema structure of the file you are modifying and supply
different values.
The following is a very minimal example. The data source’s class is a PHP class that
has a CollectionFactory injected into its constructor.
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation=
"urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
</item>
</argument>
<settings>
<deps>
<dep>referral_listing.referral_listing_data_source</dep>
</deps>
</settings>
<dataSource name="referral_listing_data_source"component="Magento_Ui/js/grid/provider">
<settings>
<storageConfig>
</storageConfig>
<updateUrl path="mui/index/render"/>
</settings>
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>entity_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<columns name="referral_listing_columns">
<column name="name">
<settings>
<label translate="true">Name</label>
</settings>
</column>
</columns>
</listing>
Image:
Listing\Columns\Thumbnail" component="Magento_Ui/js/grid/columns/
thumbnail" sortOrder="20">
<settings>
<altField>name</altField>
<hasPreview>1</hasPreview>
<addField>true</addField>
<label translate="true">Thumbnail</label>
<sortable>false</sortable>
</settings>
</column>
Standard Validation:
<column name="title">
<settings>
<filter>text</filter>
<editor>
<validation>
<rule name="required-entry"xsi:type="boolean">true</rule>
</validation>
<editorType>text</editorType>
</editor>
<label translate="true">Title</label>
</settings>
</column>
Custom Validation:
Add a custom validator with the technique shown later in relation to adding custom
validation for form elements in UI Components. Then, reference it as shown in the
preceding example.
As described above, place an XML file in the same relative directory as the one
that contains the grid component. Match the schema and use different value or add
applicable sections.
Manipulating data can often be done on the server side. In this case, a
DataModifier is an excellent tool. When customization should be done client-side,
there are two options that should be chosen depending on the use: either create a
mixin or custom Javascript module. Creating a mixin is a good option when the UI
Component has a non-generic Javascript module assigned to it. An example of that
is in product_attribute_add_form.xml: component="Magento_Catalog/
How do you create a form, a form with tabs, a form with groups
of fields, a form with dynamic fields (when one field change will
cause a change in another place)?
Even Javascript developers need to be familiar with XML when working with
Magento. At this point, there is a stack of configuration necessary to create a
form with tabs (or whatever) with UI Components. As a result, we recommend you
carefully look through the following example and remember as much as possible.
We have made notes throughout it. While it is long, we have worked to make this a
minimal example that fills each of the operatives listed.
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchema
Location="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
referral_form.referral_form_data_source</item>
</item>
</argument>
<settings>
<layout>
<navContainerName>left</navContainerName>
<type>tabs</type>
</layout>
<deps>
<dep>referral_form.referral_form_data_source</dep>
</deps>
</settings>
<dataSource name="referral_form_data_source">
provider</item>
</item>
</argument>
<dataProvider class="SwiftOtter\Example\Ui\DataProvider"
name="referral_form_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>entity_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<fieldset name="referral_fieldset">
<settings>
</settings>
<settings>
<label>Hidden Entity</label>
<dataType>text</dataType>
<visible>false</visible>
</settings>
</field>
<settings>
<label>Custom Field</label>
<validation>
<rule name="validate-test"
xsi:type="boolean">true</rule>
</validation>
</settings>
</field>
</fieldset>
<fieldset name="second_fieldset">
<settings>
<label>Second Tab</label>
</settings>
<settings>
<label>Simple Input</label>
</settings>
</field>
</fieldset>
<fieldset name="dynamic_fieldset">
<settings>
<label>Dynamic Tab</label>
</settings>
<settings>
<label>Toggle Child</label>
</settings>
</field>
<settings>
<label>Dependent Field</label>
<imports>
<link name="visible">${$.parentName}.master_field:checked
</link>
</imports>
</settings>
</field>
</fieldset>
</form>
See the <validation/> node in the section above. Adding a custom rule for a UI
Component is similar to the jQuery style on the front end but not identical.
Create the validator module that follows this pattern. It is important to return the
validator or nothing will work:
define(['jquery'], function($) {
"use strict";
return function(validator) {
validator.addRule(
'validate-example',
);
return validator;
});
var config = {
config: {
mixins: {
'Magento_Ui/js/lib/validation/validator': {
'SwiftOtter_Example/js/validator-trial': true
},
};
How can you add a file upload field, an image field, and a custom
field to a form?
File/Image Upload:
<settings>
<componentType>fileUploader</componentType>
</settings>
<formElements>
<fileUploader>
<settings>
</allowedExtensions>
<maxFileSize>2097152</maxFileSize>
<uploaderConfig>
design_config_fileUploader/save</param>
</uploaderConfig>
</settings>
</fileUploader>
</formElements>
</field>
For custom fields, define a elementTmpl which references a .html file that
contains the field you desire. This could be a completely custom field that you
built, or, like our recent case, a different core field. For instance, we recently used
Magento’s fancy select field (with filterable results and the like) in place of the
default select by simply setting the elementTmpl and component.
5.
CHECKOUT
Checkout
FURTHER RESOURCES:
We will once again recommend the terrific DevDocs which contain
a list of common customizations.
The Rest API provides the integration with the checkout front end and server.
Model classes (Javascript modules) are primarily relegated the task of handling this
transfer of data. However, a few action modules also perform similar functions.
FURTHER RESOURCES:
Refer to the DevDocs topic.
</item>
</item>
</item>
</item>
There is no substitute for you getting in there and stepping through the Javascript
modules in the checkout.
There are multiple places to find the applicable module to start with, but one way
is to start at the request and work backward. In the Chrome DevTools Network tab,
one column is the Initiator (don’t see it? right-click the column headers). Hover over
the initiator to see a stack trace of the code that led up to the request. We triggered
the following requests by simply completing the first step of the checkout:
This is a broad question. Depending on the logic you need to alter, you can
either use a mixin for the Javascript component or XML (or a PHP Interceptor) for
configuration. Read this DevDocs article.
The persistence (POST request to the API) process is essentially the same
across the various types with a utility looking up the applicable URL based on
the quote (customer or guest). If the user is logged in, their addresses are part of
the configuration embedded in the page (see \Magento\Checkout\Model\
DefaultConfigProvider::getCustomerData() for the injection point). Each
of them has a key which is used to communicate with the server which of those
addresses was selected. If a new address was added, it is treated similar to the
guest’s address.
HINT
It is possible to trick it to update as well.
When the customer moves to the second step, an API call is made to save the
shipping-information. The chosen carrier and address information is sent and
saved into the customer’s quote.
FURTHER RESOURCES:
For more information and a clear, step-by-step guide, refer to this
resource.
It is important not to store sensitive data on your server. As a result, the best way to
handle this payment data is to integrate with a provider such as Braintree. Payment
tokens or nonces can be used to reference the payment type or transaction.
Further, it is not safe to store sensitive info in localStorage.
RELATED READING
Check out this article.
There are a number of methods that facilitate extension as well. For instance,
after the order is placed, an afterPlaceOrder() method is called. It doesn’t do
anything in the core but is a good place to extend via a new module or mixin.
• Magento_Checkout/js/view/payment/cc-form.js
• Magento_Checkout/js/view/payment/default.js
• Magento_Checkout/js/action/place-order
• Magento_Checkout/js/model/place-order.js
The new step should have a higher sort order value than the payment step. It is
then necessary to change the submit payment API call to use set-payment-
information, instead of payment-information. The latter will submit the
order, while set-payment-information saves it to be used later.
FURTHER RESOURCES:
DevDocs has a topic regarding adding a step.
EXAM NOTES:
Like Magento’s other exams, there is no substitute for experience. We have
provided you with a thorough overview of Magento’s Javascript framework, but
there were many concepts that we simply touched on without giving the detail that
they may deserve. Magento recommends at least 1 year of experience, but due
to the fact that many developers who work with Javascript do not solely develop
with it, it may take several years for the average front-end developer to reach
the equivalent.
This exam had a number of questions that had specific nuances in the question
that were important given the available answers. While that may seem normal for
an exam, we encourage you to carefully consider all the details offered in each
question and then choose the best question.
GLOSSARY
Terms:
• UI Component: an entire system comprised of XML, PHP, and Javascript. May be
used to generally refer to one of those aspects.
• Javascript module: a Javascript file that contains a define() function and returns
a function - of which a jQuery widget would qualify.