Fear of Checkout #Codehacks
16 May, 2018 / 13 MIN readWe have been working with Magento 2 for more than two years and it seems that even today we are afraid to make changes in the checkout.
Using proverbs, we fear that we don’t know, so let’s try to get an understanding base and stablish a non-agression agreement with magento2’s checkout, by a basic customisation example. All code mentioned in this post is available in github.
Structure
A combination of requirejs and knockoutjs is used (apart of another essentials like jQuery, of course) for implement a model-view-viewmodel pattern, isolating any business logic for view logic. In this way using some (or infinity of) little static files, completely cacheable by any standard system, the interaction with Magento model is limited to a few REST calls. Communication between viewmodel and Magento is realised by two ways: “Static” from Magento to viewmodel via ConfigProviderInterface REST, as mentioned, and from viewmodel to Magento.
By this way is solved a great performance issue that can be the checkout, which can’t be cached, delegating the hard rendering work to the static files cache and the user’s browser. Perhaps user will not be very happy, we know that checkout speed is far to be optimal, specially during development. Because of that the PWA implementations as one that DEITY. offers are close to us. But during a while we’ll work with Magento2 front and checkout.
Rendering
Everyone of us (or almost) know how basic Magento2 frontend is structured. In a minimal reduction, we have Block, Model, Controller classes and phtml templates. For customizations we use an injection of dependencies defining them in a di.xml.
Well, at checkout we find a very similar structure, in components or js elements. We see View, Model, Action components and html templates. Customizations and dependencies are defined in a special file requirejs-config.js.
Obviously this is a reduction to conceptual minimums. With an example of how to add a new template to the ckeckout we will see that it is not very different to use blocks and templates as in the standard front.
First we will add a new component to the “sidebar” section. To do this we will create in our module the checkout_index_index.xml file. (To know where we want to place our new component previously we will have to familiarize ourselves minimally with the original layout checkout_index_index.xml in the Magento_Checkout module, with a lot of patience and an open mind).
[cc lang="xml"] <!--?xml version="1.0"?--> Misterge_FoolSample/js/view/FoolSample 90 foolsample HEY checkout.steps.billing-step.payment [/cc]
In this example we have added a new child to the sidebar element. As you can see, we have had to reference the entire parent structure from the body, in this layout the elements reference will not work since the final conversion of the layout will be a json and not an xml as in the standard front. To this new component we have established the component itself Misterge_FoolSample/js/view/FoolSample (which is the equivalent to the block), with an order number, the displayArea in which we want to render it, and a basic configuration (a title and a deps in which we can define what step of the checkout we want to show, although in this example we will not use it).
Let’s see the component that we have created in the path of our module
view/frontend/web/js/view/FoolSample.js
[cc lang="js"] define([ 'jquery', 'uiComponent', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/step-navigator' ], function ($, Component, quote) { 'use strict'; /** * Grabs the grand total from quote * @return {Number} */ var getTotal = function () { return quote.totals()['grand_total']; }; var getQuote = function() { return quote; }; return Component.extend({ defaults: { template: 'Misterge_FoolSample/checkout/foolsample' }, /** * Init component */ initialize: function () { this._super(); this.config = window.checkoutConfig.foolsample; }, getTitle: function () { return this.title; }, getImage: function () { if (getQuote().shippingAddress()) { if (getQuote().shippingAddress().regionCode && getQuote().shippingAddress().regionCode.toLowerCase() === 'albacete') { return require.toUrl(this.config.image2); } } if (getTotal() >= this.config.amount_edge) { return require.toUrl(this.config.image3); } return false; }, isVisible: function () { return (getTotal() >= this.config.minimum_amount); } }) }); [/cc]
We will return to this file repeatedly. At the moment we only have to pay attention to the fact that we have declared a dependency to uiComponent (line 3) and that in fact it extends it (line 21). Otherwise, we can not render anything. We set you a default template on line 24.
The template in question, view/frontend/template/checkout/foolsample.html will look something like this:
[cc lang="html"] <!-- ko if: (isVisible()) --> <div class="foolSample"> <div class="foolSample-title"></div> <div class="foolSample-content"><!-- ko if: (getImage()) --> <img data-bind="attr: {src: getImage(), alt: $t('Ojocuidao')}"> <!--/ko--></div> </div> <!--/ko--> [/cc]
Here (at the end) we already find Knockout.js. Those tags, that a priori seem like comments and start with ko (line 1, for example), are sentences that will be interpreted by the browser thanks to the library Knockout.js. Needless to go into detail of the syntax (see the official documentation: http://knockoutjs.com/documentation/introduction.html ).
Here we want to see that it refers to functions that will be the corresponding to the component that instantiates this template (in the same line 1 it is called isVisible () and this determines if the content of the ko tag will be rendered or not calling the function of line 52 of the component view/FoolSample.js). In the same way you can link attributes as we can see in the image rendered by line 8 of the template. In it the src path of the image will depend on the getImage () function of the component. The function $ t that we see linked to the alt attribute is a global function that translates the passed parameter into the language that is being rendered in the client’s browser. It will also translate the content of the span of line 4, by setting the key i18n to the output of the getTitle() function.
At the beginning this should be enough to paint a new block in the checkout, but this example has a trick. If we remember, in the checkout_index_index.xml file of our module we configured a custom displayArea to our new component. Well, if we look for the original sidebar template (Magento_Checkout / template / sidebar.html) we can see that it will only try to render a region, shipping-information:
[cc lang="html"]<!-- /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ --> <div id="opc-sidebar" data-bind="afterRender:setModalElement, mageInit: { 'Magento_Ui/js/modal/modal':{ 'type': 'custom', 'modalClass': 'opc-sidebar opc-summary-wrapper', 'wrapperClass': 'checkout-container', 'parentModalClass': '_has-modal-custom', 'responsive': true, 'responsiveClass': 'custom-slide', 'overlayClass': 'modal-custom-overlay', 'buttons': [] }}"> <!-- ko foreach: getRegion('summary') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> <div class="opc-block-shipping-information"><!-- ko foreach: getRegion('shipping-information') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--></div> </div> [/cc]
So we have two options, or configure one of our components in the shipping-information in the displayArea or change the original sidebar. Obviously it has been the second, that we are making an example of use for that.
We will create a new template in our module, view/frontend/web/template/checkout/sidebar.html:
[cc lang="html"] <div id="opc-sidebar" data-bind="afterRender:setModalElement, mageInit: { 'Magento_Ui/js/modal/modal':{ 'type': 'custom', 'modalClass': 'opc-sidebar opc-summary-wrapper', 'wrapperClass': 'checkout-container', 'parentModalClass': '_has-modal-custom', 'responsive': true, 'responsiveClass': 'custom-slide', 'overlayClass': 'modal-custom-overlay', 'buttons': [] }}"> <!-- ko foreach: getRegion('summary') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> <div class="opc-block-shipping-information"><!-- ko foreach: getRegion('shipping-information') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--></div> <div class="opc-block-foolSample"><!-- ko foreach: getRegion('foolsample') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--></div> </div> [/cc]
In it we have added the new class that will look for the elements configured in the new region or displayArea “foolsample”.
In order for Magento to replace its sidebar with ours, we will create in our module the file that we mentioned above, in view/frontend/requirejs-config.js
[cc lang="js"] var config = { map: { '*': { "Magento_Checkout/template/sidebar.html": "Misterge_FoolSample/template/checkout/sidebar.html" } } }; [/cc]
Here we “map” Magento indicating that when a file searches Magento_Checkout/template/sidebar.html have to return Misterge_FoolSample/template/checkout/sidebar.html
Now, we have a new block in the checkout sidebar. But just as we have left it, it still does not paint anything …
Configuration
Just to insert a template does not have much use. At least we are going to add a configuration that is passed to him from magento and that renders it.
Magento will search for all classes that implement
Magento\Checkout\Model\ConfigProviderInterface and are declared in Magento\Checkout\Model\CompositeConfigProvider and
mix them to give the checkout all the necessary configuration. That’s why we will create our class Model/Checkout/BasicConfigProvider.php
[cc lang="php"] <!--?php namespace Misterge\FoolSample\Model\Checkout; use Magento\Checkout\Model\ConfigProviderInterface; /** * Class BasicConfigProvider */ class BasicConfigProvider implements ConfigProviderInterface { const MINIMUM_AMOUNT = 10; const AMOUNT_EDGE = 100; const IMAGE = 'Misterge_FoolSample/images/foolsample2.gif'; const IMAGE2 = 'Misterge_FoolSample/images/foolsample3.gif'; const IMAGER = 'Misterge_FoolSample/images/foolsampler.gif'; /** * {@inheritdoc} */ public function getConfig() { $config = [ 'foolsample' =--> [ 'minimum_amount' => self::MINIMUM_AMOUNT, 'amount_edge' => self::AMOUNT_EDGE, 'image' => self::IMAGE, 'image2' => self::IMAGE2, 'imageR' => self::IMAGER, ] ]; return $config; } } [/cc]
Here in the function getConfig () we will return the configurations that we need, in the form of an array, in a key that we will create, or in an existing one if we want to overwrite it (In this case it is important to remember to establish the correct sequence in the declaration of the module in etc/module.xml). In our example we will simply add some constant values, but it goes without saying that here we can use configuration values in database, current quote data, or whatever our back imagination allows us to do.
We will declare it in etc/frontend/di.xml:
[cc lang="xml"] <!--?xml version="1.0"?--> Misterge\FoolSample\Model\Checkout\BasicConfigProvider [/cc]
In this simple way we already have dynamic data that can be used from any element of the checkout, within the global variable window.checkoutConfig. In our example component it is declared on line 31.
Now we will see a new block in the checkout if the “configured” conditions are met.
Modeling
If we use an inspector during the checkout we can see that magento has basically elevated all the basic entities to the viewmodel. We find models of customer, customer-address, quote, billing-address, shipping-address, payment-method, etc. And thanks to requirejs we can use these entities from almost anywhere.
In our same example, we have defined two dependencies that we have not mentioned before (see the file view/frontend/web/js/view/FoolSample.js lines 4 and 5), one to the entity quote and another to the step-navigator, which it would allow us, for example, to check what step of the checkout we are in, which we did not use in this example but we have left for instructional purposes. We see that with the statement define ([], function () {}) we establish entities that will be necessary to execute this component, and in the function it is assigned the global variable assigned to those entities, which we can use within our component. In our example, we will choose whether or not to show the content based on the total price of the cart (line 53 referencing line 14), or what image to show depending on the shipping province (line 40).
If we dive into the entities used, we will see that for example quote is an entity that returns observables. This Knockout.js utility allows us to always work with the real value of an entity, without worrying about updating it when it changes, since knockout takes care of it. When a value of observable entities changes, automatically all the functions that are observing this value, will change accordingly, without performing any additional action.
For example, an attribute is set in the quote billingAddress (line 28 of magento/module-checkout/view/frontend/web/js/model/quote.js):
[cc lang="js"] totals = ko.observable(totalsData), [/cc]
The one that is initialized as null, but during the execution of the checkout will be updated, calling (line 83):
[cc lang="js"] /** * * @return {*} */ getTotals: function () { return totals; }, /** * @param {Object} data */ setTotals: function (data) { data = proceedTotalsData(data); totals(data); this.setCollectedTotals('subtotal_with_discount', parseFloat(data['subtotal_with_discount'])); }, [/cc]
In this way, in our example, if the total amount of the cart is changed (applying shipping costs, or with discounts, etc.,) the rendering will be automatically changed since we are conditioning it to quote.totals ()
Mixins
The mixins are an invention of Magento using requirejs, to “simplify” the extension or overwriting of existing components. They are the equivalent of the “plugins” in the backend.
Using them we can define which element or component we want to extend, indicating the mixin that we create for that purpose. Magento will extend the original component by adding or replacing it with what we have in our mixin. For this we will add the config.mixins statement to our requirejs-config:
[cc lang="js"]var config = { map: { '*': { "Magento_Checkout/template/sidebar.html": "Misterge_FoolSample/template/checkout/sidebar.html" } }, config: { mixins: { 'Magento_Checkout/js/model/shipping-save-processor/default': { 'Misterge_FoolSample/js/model/shipping-save-processor/default-mixin': true } } } };[/cc]
In this case we will modify the operation of Magento_Checkout/js/model/shipping-save-processor/default . What we want to achieve is that the customer’s default billing address is set in the cart (something that in this version of Magento is not done, but sets the same shipping address). So we will create our mixin in view/frontend/web/js/model/shippin-save-processor/default-mixin.js:
[cc lang="js"] define([ 'mage/utils/wrapper', 'jquery', 'ko', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/action/select-billing-address', 'Misterge_FoolSample/js/action/get-default-billing-address' ], function (wrapper, $, ko, quote, selectBillingAddressAction, getDefaultBillingAddressAction) { 'use strict'; var extender = { saveShippingInformation: function (_super) { if (!quote.billingAddress()) { var defaultBillingAddress; defaultBillingAddress = getDefaultBillingAddressAction(); if (defaultBillingAddress) { selectBillingAddressAction(defaultBillingAddress); } else { selectBillingAddressAction(quote.shippingAddress()); } } return _super(); } }; return function (target) { return wrapper.extend(target, extender); }; }) [/cc]
We see here that we are using mage/utils/wrapper to perform the replacement (or extend concretely) of the parent model target with our extend. In our extension we can replace an attribute or function of the parent, or add new ones.
With this mixin what we do is replace the saveShippingInfomation function with one that checks if there is a billing address and if there is not, look for the one defined by the user in their addresses as the default billing address. And then call _super () that in is the parent function that we are overwriting. This would be the equivalent of a “before” plugin in the backend. To find the default billing address we use an “action” defined in our same module, in view/frontend/web/js/action/get-default-billing-address.js:
[cc lang="js"] define([ 'jquery', 'Magento_Customer/js/model/customer' ], function ($, customer) { 'use strict'; return function () { var defaultBillingAddress = false; $.each(customer.getBillingAddressList(), function (key, address) { if (address.isDefaultBilling()) { defaultBillingAddress = address; } }); return defaultBillingAddress; }; }) [/cc]
Here, as we have seen in the modeling section, we use the customer entity that has already declared the addresses, to find the one that is configured as default billing.
To consider
- This example has been programmed and tested in Magento Community 2.2.3.
- All js elements and templates must be under the frontend/web/path, otherwise they will not be accessible to the browser.
- When making dependencies and calls to other js, the extension .js is ignored, since it is understood. If we add it to it, it will fail, since it will look for the file *.js.js.
- To be all communication with Magento through webapi, the definitions of plugins, observers, etc. they will not work in the checkout if they are not defined globally or for the webapi_rest area. That is, defined in etc/di.xml or in etc/webapi_rest/di.xml
Conclusion
We have made a basic tour to perform a very basic customization in the Magento2 checkout. We have been able to verify the power and the weaknesses that it has, and that it fulfills the saying of “a front made by a back”. We hope to have shown that it is not an unfathomable black box and that those developers who have not yet dared, are encouraged to “play” with the Magento2 checkout.
While it lasts, that the bet for the PWA is strong and is very close 🙂 Then we can forget everything said in this article and manage the entire checkout via webservice. This is the point of view of a developer especially of backend, of course.