Back to blog home

A technical guide to the Magento 2 checkout

The checkout in Magento 2 has undergone a number of improvements and changes to its visual appeal and general flow. What's more, a total overhaul means it's now driven with Javascript and KnockoutJS

The checkout is now split into multiple steps (two by default) and within these steps there are multiple components, all of which can communicate with each other using models and other dependency injection techniques that we'll explore in this article. 

How the checkout is rendered

The checkout in Magento 2 is built up from a series of Knockout JS components which are then rendered using the Knockout JS templating system. Magneto 2 defines each one of these components and their parent / child relationship in a large XML file which can be extended or overridden in your own theme or module.

Magento 2 parses the XML file and runs it through the layout processor which processes each XML node and reads its configuration, which it then inserts into a large multi-dimensional associative array,where each key represents a component or a group of components within.

This Array is then converted into a JSON object which is then passed into the main app checkout component (Magento_Ui/js/core/app) on the main checkout template file (onepage.phtml) and initialised.

<script type="text/x-magento-init">
    {
        "#checkout": {
            "Magento_Ui/js/core/app": <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
        }
    }

The main app.js file looks as follows:

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define([
    './renderer/types',
    './renderer/layout',
    '../lib/knockout/bootstrap'
], function (types, layout) {
    'use strict';

    return function (data, merge) {
        types.set(data.types);
        layout(data.components, undefined, true, merge);
    };
});

As you can see within app.js it injects the layout renderer as a dependency./renderer/layout which is then passed the data.components object which contains all the components that need to be rendered (which were passed in previously when app.js was first initialised).

The renderer then loops through each of these components iteratively rendering each one and checking to see if each node has any children.

If children are found within a node it loops them in the same manner renders each component in the list it receives. Once the renderer reaches a component / node with no children then the layout renderer simply returns as normal and processes the current node with no further rendering of children.

You can also add dependencies within the XML, making sure other components are present before initialisation of other components.

This method of rendering components means the layout renderer can be used anywhere on the site. It’s not available just in the checkout, so it opens up even more possibilities for the rest of your shop!

How the XML works

As previously mentioned, the XML file is ultimately parsed into a JSON object which is passed into the main checkout component when it is initialised. So let’s take a closer look at how the XML is structured. The base checkout XML file can be found either in:

/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml

or:

vendor/magento/module-checkout/view/frontend/layout/checkout_index_index.xml depending on where you are viewing the code.

The main thing we want to look for in the XML file is the components item. This is the parent array of all the checkout components defined in checkout_index_index.xml.

<item name="components" xsi:type="array">

As we discussed earlier the way the renderer works is to loop through each element it finds, render it and then render any of its children. We can see the next item down is the checkout item.

<item name="checkout" xsi:type="array">

Within this we see our first configuration and component definitions.

<item name="component" xsi:type="string">uiComponent</item>

You can see here the definition is simply uiComponent. This is just an alias for the full file path of the base Knockout JS components used in the Magento checkout and can be found at:

app/code/Magento/Ui/view/base/web/js/lib/core/collection.js

All other components must extend from this or another component which has already extended from this base component. The value passed into the component attribute can be either an alias to the JS file you want to use as your component (as with uiComponent or the module path to the JS file you want to use, for example):

Magento_Ui/js/view/messages

You can also pass configuration through to the component from the XML file which can be anything, including any custom attributes you might want to pass into your component from the XML. Any custom attributes are simply added to the components object.

So as an example:

<item name="config" xsi:type="array">
    <item name="template" xsi:type="string">Magento_Checkout/onepage</item>
</item>

The XML is setting the template for the component. This is normally done in the component itself, however, because we are using the base component (uiComponent) – which does not define a template – we need to set a template for it here if our component is going to use one.

Define child components

As discussed earlier in the article, we know renderer loops through each of the children defined within the children attribute for that component and renders them as well. To define this in the checkout_index_index.xml we simply nest any components we want as children for the parent node within the children attribute, for example:

<item name="children" xsi:type="array">
    <item name="errors" xsi:type="array">
        <item name="sortOrder" xsi:type="string">0</item>
        <item name="component" xsi:type="string">Magento_Ui/js/view/messages</item>
        <item name="displayArea" xsi:type="string">messages</item>
    </item>
</item>

As you can see, you can nest as many components as you like within the children attribute, and each one will be rendered in the order it is defined in the XML.

Render components where needed

You may be wondering after looking at all that XML how it knows where to render each component in the HTML? The answer is in the displayArea attribute. This refers to the getRegion command in knockoutJS which tells the KnockoutJS renderer where to actually place the component in the HTML.

So let’s look at an example of how the progressBar component is rendered.

One thing to note is that the position of where child components are rendered is always controlled and specified by the parent component template file (unless no template file is specificed; it can then be controlled with sortOrder). So in our example here the parent component of progressBar is checkout and, as we saw above, its template is set to Magento_Checkout/onepage.

Each component template in the checkout can be found easily by simply visiting the template folder of the module within the web directory. So in our case we need to look in the:

vendor/magento/checkout/view/frontend/web/template directory 

to find the onepage.html (.html not .phtml) file. When you reference a module in checkout_index_index.xml it will reference its template directory with the same structure we’ve just seen. So in our case we are looking for:

vendor/magento/checkout/view/frontend/web/template/onepage.html

Let’s have a look at the template. We can see in the template there are a series of HTML comments with ko and getRegion commands. This is what the component reads in order to interpret simple JS bindings and what templates to render.

For our progressBar component we can see the following:

<!-- ko foreach: getRegion('progressBar') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->

So for each component in the region progressBar it will render that component (or template of the component) inside the foreach loop.

If we go back and look at our XML file. We will see the following within the progressBar item array.

<item name="displayArea" xsi:type="string">progressBar</item>

This displayArea attribute refers to the region in getRegion. So if I wanted the progressBar component to render instead in the messages region for example (which is a region also defined in onepage.html), we could simply change the value of displayAreafrom progressBar to messages and the component would render within the messages getRegion foreach instead.

You can wrap HTML around these foreach loops / move them around the template as you wish. Wherever they are placed in the template that’s where your component and other nested components (defined within the children attribute) are rendered.

This functionality iterates through all the nested children and renders component after component, in exactly the same way as above. It’s just at a lower level inside the component tree. As you go deeper into the tree you have a new template file where you can then render new components or move existing ones – which brings us nicely onto our next topic.

How to reposition components

There are a few ways you can reposition components within the checkout page. If you are simply looking to move a component 'up' or 'down', above or below another component, as long as they are defined in the same children attribute array, you can use the sortOrder attribute to move them up or down the render list.

Within the component array definition itself we simply add another item:

<item name="sortOrder" xsi:type="string">0</item>

A value of 0 is at the very top (but negative values are also supported). If you always want it to be at the bottom of the other components you can simply give it a very high number such as 9999 for example.

The next method to move components we have seen previously. You can simply change the displayArea value within the component definition to something unique (or existing) and as long as the parent template of that component has that getRegion value defined in there, it will render your template. As mentioned before, you can move the getRegion loop wherever you want within that template, however just to clarify, the getRegion value must be defined in the direct parent of the component you are trying to move.

Eg. If we add a new region to our onepage.html file:

<!-- ko foreach: getRegion('myNewRegion')-->
    <!-- ko template: getTemplate() --><!-- /ko -->
<--/ko-->

Then change the progressBar displayArea in checkout_index_index.xml to:

<item name="displayArea" xsi:type="string">myNewRegion</item>

It will render as expected in the new region.

Note: for this demo you can simply edit the core files we are looking at however on a real project you would extend the checkout_index_index.xml file in your own theme or module (which is shown in the next section). You also need to copy the .html template files you were editing to the same module location in your theme or module. Please see the Magento 2 documentation to understand how to do this.

If you wish to move components to a different template, it’s possible, but beyond the scope of this article. You have to disable the original component and then redefine it where you wish it to be rendered in the render tree. If the component is fairly standalone (like messages) this works fine, however other components, with more components and a lot more nested children, are far more complex to move.

Create your own components

So now we’ve seen how the layout system works for the checkout and understand exactly how the XML builds the configuration necessary for the layout system to render components in the correct location, we can take a look at making our own simple component and adding it to the checkout.

One of the main issues with adding your own components, and indeed extending existing components (which is up next), is that there is no way to simply target the item in the XML where you wish to add your component via a reference attribute, like there is with containers and blocks (i.e. referenceContainer=”my-container-name”).

With the checkout XML you need reproduce the entire attribute tree to the item where you wish your component to render. It’s not really a case of specifying a displayArea and the component appearing there (although I wish it was!). 

So let’s take a look at an example:

In our theme (or module) we need to make a new checkout_index_index.xml file which will extend the original and let us add in our new component. In our example we are going to simply add in a new message which is displayed above the shipping address form depending on whether you are logged in or not.

If you are doing this from your theme you can add the xml file to <your_theme>/Magento_Checkout/layout/.

If you are doing it from your own custom module simply add it to the <your_namespace>/<module_name>/view/frontend/layout folder of your module.

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="shipping-step" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="shippingAddress" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="before-shipping-method-form" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="our-shiny-component" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Magento_Checkout/js/view/shiny</item>
                                                                    <item name="config" xsi:type="array">
                                                                        <item name="message" xsi:type="string" translate="true">Your new message above the shipping form. Welcome!</item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Wow, so that's a lot of XML! So as we can see, we have to target and define each node matching the hierarchy of the original checkout_index_index.xml file to add the component where we need. One thing to note as well is that if the parent component doesn’t have a template defined (in this case before-shipping-method-form) all items specified in its children will be rendered in the order they are defined. You can change this using the sortOrder attribute as mentioned earlier in the article.

If you add a template to the parent component you then need to specify a displayArea in the child component XML where it is due to render in parent template. You then need to add where to render that displayArea via a getRegion foreach call in your parent template.

You will also notice we defined a config array with a message item inside. You can pass data into your component from the XML, in this case a translatable string, which will be our message.

So let’s create a simple Component JS file now to match the component attribute we just defined a value in. In this case I’m using Magento_Checkout/js/view/shiny which will be defined in my active Theme. However you if are using a module just use that path instead and place the file there i.e. 

<your_namespace>/<your_module>/view/frontend/web/js/view

So create a new file called shiny.js in your chosen location and paste in the code below:

define(
    [
        "uiComponent",
        'ko',
        'Magento_Customer/js/model/customer',
    ],
    function(
        Component,
        ko,
        customer,
    ) {
        'use strict';
        return Component.extend({
             defaults: {
                template: 'Magento_Checkout/shiny'
            },
            isCustomerLoggedIn: customer.isLoggedIn,
            initialize: function () {
                this._super(); //you must call super on components or they will not render
            }
        });
    }
);

This is a standard component file in Magento 2. In our case we are doing two things here. The first is that we are injecting the customer model 'Magento_Customer/js/model/customer' into our component which gets aliased as customer . This then lets us set an attribute on our component called isCustomerLoggedIn which aliases this value from the customer model and allows us to access whether the customer is logged in or not in out component template.

The second thing we are doing is defining our template as Magento_Checkout/shiny. This uses the shorthand module alias to target the correct file, but essentially when you call your module name, it’s really linking it to the web/template folder in your module i.e. Magento_Checkout/shiny will actually link to Magento_Checkout/web/template/shiny.html. You don’t need to define the .html in the template definition; it already assumes it is a .html file.

So let's create our template (shiny.html):

<!-- ko if: (isCustomerLoggedIn) -->
        <div class="message warning"><span data-bind="text: message"></span></div>
<!-- /ko -->

We can see that there is a ko if statement which references our isCustomerLoggedIn value we defined in our component. This is a ko.observable which means that when the value is updated the contents of the HTML between the if will be dynamically added/removed from the DOM depending on its live value.

You can also see that the message attribute we passed in via the config in the XML is now bound to the span inside the div which will place the contents of the message inside the span. This is also a Knockout JS feature.

We will discuss more about Knockout JS in a future article.

So if you now clear all your caches, especially restarting varnish if you are running it along with full page cache, you should now be able to see a warning message above your shipping address form, but only if you are logged into you store!

Extend existing components

In most cases you will simply want to extend an existing component as opposed to making your own. In earlier versions of Magento 2 it wasn’t really possible, but since 2.0.2 it is now possible to overwrite the component defintion of a component in your checkout_index_index.xml file meaning it will use your new file’s definition vs. the original. You can then inject the original into your new component and extend from that, rather than the normal base uiComponent.

Probably one of the biggest and most important components is the main Shipping component and it's one you are likely to want to extend and customise. So continuing to edit the same checkout_index_index.xml, let’s examine what our file should now look like:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="shipping-step" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="shippingAddress" xsi:type="array">
                                                    <!-- please note we are now redefining the component for the shipping step -->
                                                    <item name="component" xsi:type="string">Magento_Checkout/js/view/shipping-shiny</item>
                                                    <item name="children" xsi:type="array">
                                                        <item name="before-shipping-method-form" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="our-shiny-component" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Magento_Checkout/js/view/shiny</item>
                                                                    <item name="config" xsi:type="array">
                                                                        <item name="message" xsi:type="string" translate="true">Your new message above the shipping form. Welcome!</item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

We’ve called our new shipping file shipping-shiny.js and if you are placing this in the Magento_Checkout module in your theme, it needs to have a different name. If you are including it from your custom module you can keep the same name as it’s using a different namespace and won’t override the original when we come to include it as a dependency in our new extended shipping component.

So let’s see what this file looks like:

define(
    [
        'jquery',
        'ko',
        'Magento_Checkout/js/view/shipping'
    ],
    function(
        $,
        ko,
        Component
    ) {
        'use strict';
        return Component.extend({
            defaults: {
                template: 'Magento_Checkout/shipping-shiny'
            },
            initialize: function () {
                var self = this;
                this._super();
            }
        });
    }
);

You can see that we add the original shipping component as a dependencyMagento_Checkout/js/view/shipping and alias this as the third item passed into the init callback calling it Component we then call Component.extend and define our additional or overridden functions or attributes.

It’s up to you what you do from this point. To extend a function you can redefine the function. In the example above we have redefined the initialize function. So it will now use this over the original components. If you want to call the parents’ version of the function we need to call this_super(); within the function. You can also redefine attributes such as the templatedefinition as we can see in our new file. Simply create this new file in the template  directory as explained earlier in the article and it will use your new template file for the component.

Note: when overriding the initialise function you always have to call this._super as otherwise the component will not initialise correctly and will error

Although there are many components you can extend, mostly anything in the view directory of the checkout, and anything extended from uiComponent, models, and action files are not really extensible. This is something that Magento needs to look at in the future as, for a number of things in the checkout, the only way to extend what you need is to simply copy over the files you need, or simply create your own versions from the original file (which is the best option currently).

Conclusion

Overall the checkout in Magento 2 is a much needed improvement to Magento 1.x. It’s essentially the core of the shopping experience and one which needs to be quick and easy to use. From the technical side of things, everything is much more modular and separated out into components as we have seen – which makes it very easy to add and change functionality where you need to.

However, one of the issues of switching to a fully JS and template-driven system is that it has a very sharp learning curve. It’s not just a case of learning some JS and HTML anymore – the checkout in Magento 2 is probably one of the most complex areas technically for both frontend and backend developers.

If you can learn and understand the checkout, people will love you for it. Customising it to exactly how your client wants is a big plus in the world of ecommerce.

There are however some lingering issues, not neccesasrily with the technology used, but in how it’s been implemented, and in some areas it’s very hard if not impossible to modify. You can end up overwriting or copying files over in order to customise them to your needs, which is the total anti-pattern of the approach they have tried to implement in Magento 2.

I hope this guide has helped ease the learning curve of the Magento 2 checkout. Once you understand it, everything will fall into place. You'll see that what's been achieved with the platform is decent – it just needs some further enchancements before it can be really considered truly great.