Categories


Archives


Recent Posts


Categories


How Magento’s Checkout Application Updates Shipping Rates

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This one’s a long winding journey through all the javascript code involved when Magento updates the Shipping Rates section of the checkout application.

The specifics here are Magento 2.1.1 – and I know for a fact there are some small differences in the 2.0.x branch w/r/t to how the validation listeners get setup. Concepts should be useful throughout Magento 2 though.

Also, this assumes you’ve at least skimmed my Advanced Javascript and UI Components articles. We’re using Commerce Bug 3.2 to match up RequireJS modules with their Knockout.js remote templates.

This one is probably confusing – no way around that. We’ll update each section with an executive summary. If you need further help ask a question on Stack Exchange and then ping me via Twitter.

Shipping Address Fields

We need to start with the Shipping Address form. Each element in the shipping address form is rendered with a RequireJS view model/Knockout.js remote template pair. For example, you can grab the view model for the first name field with

reg = requirejs('uiRegistry');    
reg.get('checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.firstname')

Its RequireJS view module constructor factory is Magento_Ui/js/form/element/abstract. Its template URI is ui/form/field. For reasons outside the scope of this post, this view model also has a elementTmpl property

reg = requirejs('uiRegistry');    
reg.get('checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.firstname').elementTmpl

with a value (for this specific view model) of ui/form/element/input. This elementTmpl property contains the actual form field HTML.

<!--
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<input class="admin__control-text" type="text" data-bind="
        event: {change: userChanges},
        value: value,
        hasFocus: focused,
        valueUpdate: valueUpdate,
        attr: {
            name: inputName,
            placeholder: placeholder,
            'aria-describedby': noticeId,
            id: uid,
            disabled: disabled
    }">

The input (as do all the inputs in the shipping form) has several Knockout.js bindings, including a value binding and an event binding. Ignore the event binding. Notice that the value binding binds to the value property of the view model.

reg = requirejs('uiRegistry');            
reg.get('checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.firstname').value        

This value property is an observable. How it gets setup as an observable is another story for another time, but you can confirm this by updating its value and seeing the form update.

reg = requirejs('uiRegistry');    
reg.get('checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.firstname').value("Test")        

We’re going to veer wildly in another direction. The main takeaway for this section is to remember that the value property of a field view model is an observable.

Shipping Method Event Bindings

The

requirejs('uiRegistry').get('checkout.steps.shipping-step.shippingAddress')

view model is responsible for rendering the shipping address form. It’s view model constructor factory (i.e. its “component”) is the RequireJS module Magento_Checkout/view/shipping. It has a child view model whose name is shipping-address-fieldset. This fieldset has child view models that are the individual form elements mentioned in the previous section.

If we look at the source for Magento_Checkout/view/shipping, we see its initialize method contains the following

initialize: function () {
    var self = this,
        hasNewAddress,
        fieldsetName = 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset';

    this._super();
    shippingRatesValidator.initFields(fieldsetName);
    //...
}        

We’re interested in the shippingRatesValidator.initFields(fieldsetName); call. The shippingRatesValidator variable is a Magento_Checkout/js/model/shipping-rates-validator RequireJS module. This module is responsible for setting up the form validation. Its source is worth investigating entirely on its own, but what we’re interested in is the following

#File: vendor/magento/module-checkout/view/frontend/web/js/model/shipping-rates-validator.js
element.on('value', function () {
    clearTimeout(self.validateAddressTimeout);
    self.validateAddressTimeout = setTimeout(function () {
        if (self.postcodeValidation()) {
            self.validateFields();
        }
    }, delay);
}); 

Via that initial call to initFields, Magento will eventually call the above code where element is each form element’s view model. The call to .on('value' sets up variable tracking for the observable value field we discussed earlier. In the validateFields method above (yes, we’re skipping a lot)

#File: vendor/magento/module-checkout/view/frontend/web/js/model/shipping-rates-validator.js
validateFields: function () {
    var addressFlat = addressConverter.formDataProviderToFlatData(
            this.collectObservedData(),
            'shippingAddress'
        ),
        address;

    if (this.validateAddressData(addressFlat)) {
        address = addressConverter.formAddressDataToQuoteAddress(addressFlat);
        selectShippingAddress(address);
    }
},

Magento will create a new address object with the call to

address = addressConverter.formAddressDataToQuoteAddress(addressFlat);

and then pass that address object to the selectShippingAddress function. This function comes from the Magento_Checkout/js/action/select-shipping-address module.

#File: vendor/magento//module-checkout/view/frontend/web/js/action/select-shipping-address.js
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
/*global define*/
define(
    [
        'Magento_Checkout/js/model/quote'
    ],
    function(quote) {
        'use strict';
        return function(shippingAddress) {
            quote.shippingAddress(shippingAddress);
        };
    }
);

which seems to pass the address on to the shippingAddress method of the Magento_Checkout/js/model/quote module. We say seems to, because if we look at the quote model

#File: vendor/magento/module-checkout/view/frontend/web/js/model/quote.js
define(
    ['ko'],
    function (ko) {
        'use strict';
        //...
        var shippingAddress = ko.observable(null);
        //...
        return {
            //...
            shippingAddress: shippingAddress,
    //...

we see that shippingAddress is not a method, its an observable. So what’s really happening here

quote.shippingAddress(shippingAddress);

is the shippingAddress observable on the quote object is having its value updated.

The main takeaway from all this is the value observables each have a listener/subscriber/tracker setup on them. This listener/subscriber/tracker performs form validation. When validation passes, Magento updates an observable object on the javascript quote model.

Also, so far we’re only using observables to store values, and listen for changes to inputs. No DOM manipulation is happening yet.

Rates Fetching

Next, we need to talk about where rates come from. If we talk a look at the Magento_Checkout/js/view/shipping module

#File: vendor/magento/module-checkout/view/frontend/web/js/view/shipping.js
define(
    [
        /* ... */
        'Magento_Checkout/js/model/shipping-service',

We see it includes the Magento_Checkout/js/model/shipping-rate-service module as a dependency. If we look at this module’s source

#File: vendor/magento/module-checkout/view/frontend/web/js/model/shipping-rate-service.js
define(
    [
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/shipping-rate-processor/new-address',
        'Magento_Checkout/js/model/shipping-rate-processor/customer-address'
    ],
    function (quote, defaultProcessor, customerAddressProcessor) {
        'use strict';

        var processors = [];

        processors.default =  defaultProcessor;
        processors['customer-address'] = customerAddressProcessor;

        quote.shippingAddress.subscribe(function () {
            var type = quote.shippingAddress().getType();

            if (processors[type]) {
                processors[type].getRates(quote.shippingAddress());
            } else {
                processors.default.getRates(quote.shippingAddress());
            }
        });

        return {
            registerProcessor: function (type, processor) {
                processors[type] = processor;
            }
        }
    }
);

there’s a few things to take note of. First, this module runs some code before returning an object. The way module loading works in RequireJS, the code prior to return will only ever run once, no matter how often the module is used throughout a program’s lifecycle.

Second is the following code

quote.shippingAddress.subscribe(function () {
    var type = quote.shippingAddress().getType();

    if (processors[type]) {
        processors[type].getRates(quote.shippingAddress());
    } else {
        processors.default.getRates(quote.shippingAddress());
    }
});        

Remember the observable shippingAddress from the Magento_Checkout/js/model/quote module? Well, here Magento has setup an observable listener (i.e. a subscriber). Knockout will invoke this callback whenever the value in the quote.shippingAddress() observable is updated.

The getRates method

#File: vendor/magento//module-checkout/view/frontend/web/js/model/shipping-rate-processor/new-address.js

getRates: function (address) {
    shippingService.isLoading(true);
    var cache = rateRegistry.get(address.getCacheKey()),
    //...

    if (cache) {
        shippingService.setShippingRates(cache);
        shippingService.isLoading(false);
    } else {
        storage.post(
            serviceUrl, payload, false
        ).done(
            function (result) {
                rateRegistry.set(address.getCacheKey(), result);
                shippingService.setShippingRates(result);
            }
        ). 
        /*...*/
    }
}

will make an ajax request for the shipping rates, and then cache them in the rateRegistry (a Magento_Checkout/js/model/shipping-rate-registry module), and also set them on the shippingService (a Magento_Checkout/js/model/shipping-service object). If we take a look at this shipping service, we see

#File: vendor/magento//module-checkout/view/frontend/web/js/model/shipping-service.js

function (ko, checkoutDataResolver) {
    "use strict";
    var shippingRates = ko.observableArray([]);
    return {
        isLoading: ko.observable(false),
        /**
         * Set shipping rates
         *
         * @param ratesData
         */
        setShippingRates: function(ratesData) {
            shippingRates(ratesData);
            shippingRates.valueHasMutated();
            checkoutDataResolver.resolveShippingRates(ratesData);
        },

        /**
         * Get shipping rates
         *
         * @returns {*}
         */
        getShippingRates: function() {
            return shippingRates;
        }
    };
}

that setShippingRates updates shippingRates, an internal to the module private data variables. The shippingRates variable is a Knockout observable array. Anyone can fetch this private variable using the getShippingRates method.

So, the takeaway from this section: The shippingAddress observable on a quote object has a subscriber setup. This subscriber fetches rates from a URL using ajax, and updates a second observable in the Magento_Checkout/js/model/shipping-service module.

Rates Data Binding

We’re almost there. The final piece of this puzzle is the shipping method template code

#File: vendor/magento/module-checkout/view/frontend/web/template/shipping.html
<!--ko foreach: { data: rates(), as: 'method'}-->
    <tr></tr>
        /*...*/

<!--/ko-->

Above, there’s a tag-less Knockout.js foreach binding that iterates over the array returned by the rates() function. The checkout/shipping remote template above has a view model constructor factory (i.e. a “component”) of Magento_Checkout/js/view/shipping, which means we’ll find the source for the rates() method in that module’s source code.

#File: vendor/magento/module-checkout/view/frontend/web/js/view/shipping.js    
define(
    [
        /* ... */
        'Magento_Checkout/js/model/shipping-service',
        /* ... */        ],
    function (
        /* ... */
        shippingService,
        /* ... */
    ) {
        'use strict';

        var popUp = null;

        return Component.extend({
            /* ... */
            rates: shippingService.getShippingRates(),            
            /* ... */                
        });
    }
);

The rates method is just an alias to the shippingService.getShippingRates method. This is the same getShippingRates method we discussed in the last section.

In other words, rates() returns the observable array from Magento_Checkout/js/model/shipping-service. The same observable array that Magento updates whenever it fetches shipping rates via getRates. The getRates that’s called in a subscriber to the shippingAddress object on the quote module object. The same shipingAddrsess object that’s updated whenever Magento’s form validates itself successfully. Validation that happens whenever a value in a form view model is updated. Values that are updated whenever a user enters text in a form.

Since the KnockoutJS template foreaches over this observable array via a tag-less data-binding, Knockout will update the DOM nodes whenever the rates are updated by a call to the Magento_Checkout/js/model/shipping-service module’s setShippingRates method.

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 16th November 2016

email hidden; JavaScript is required