Writing this one down to remember it – hopefully this makes sense to someone else.
The following method is the one responsible for moving the Checkout from one step to another.
#File: vendor/magento/module-checkout/view/frontend/web/js/model/step-navigator.js
next: function() {
var activeIndex = 0;
steps.sort(this.sortItems).forEach(function(element, index) {
if (element.isVisible()) {
element.isVisible(false);
activeIndex = index;
}
});
if (steps().length > activeIndex + 1) {
var code = steps()[activeIndex + 1].code;
steps()[activeIndex + 1].isVisible(true);
window.location = window.checkoutConfig.checkoutUrl + "#" + code;
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
}
The first loop hides all the steps, and marks the most recent visible step as active.
var activeIndex = 0;
steps.sort(this.sortItems).forEach(function(element, index) {
if (element.isVisible()) {
element.isVisible(false);
activeIndex = index;
}
});
The second makes the next step visible
if (steps().length > activeIndex + 1) {
var code = steps()[activeIndex + 1].code;
steps()[activeIndex + 1].isVisible(true);
window.location = window.checkoutConfig.checkoutUrl + "#" + code;
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
The isVisisble
method is tricky. Its set when you register a step
#File: vendor/magento/module-checkout/view/frontend/web/js/model/step-navigator.js
//look at the fourth parameter
registerStep: function(code, alias, title, isVisible, navigate, sortOrder) {
//...
}
If you look at step registration
#File: vendor/magento/module-checkout/view/frontend/web/js/view/shipping.js
//...
visible: ko.observable(!quote.isVirtual()),
//...
stepNavigator.registerStep(
'shipping',
'',
$t('Shipping'),
this.visible, _.bind(this.navigate, this),
10
);
The this.visible
refers to the visible: ko.observable(!quote.isVirtual())
earlier in the file. This means isVisisble
in the step is a Knockout Observable. Observables are objects that let you set or retrieve a value – a one value getter/setter. These objects also allow others to listen in for when a value is set, or get. They’re important in KnockoutJS.
So the last part of understanding how isVisisble
works? If you look at a Magento Remote Knockout Template for a step object/module (Require JS object/module: Magento_Checkout/shipping
, Knockout template: Magento_Checkout/shipping
)
#File: vendor/magento//module-checkout/view/frontend/web/template/shipping.html
<li id="shipping" class="checkout-shipping-address" data-bind="fadeVisible: visible()">
You’ll see the fadeVisisble
Knockout binding. This is a custom KnockoutJS binding from Magento, borrowed from the KnockoutJS docs
#File: vendor/magento/module-ui/view/base/web/js/lib/knockout/bindings/fadeVisible.js
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
define([
'jquery',
'ko',
], function($, ko) {
ko.bindingHandlers.fadeVisible = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden
//depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the
// element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
});
Whenever the observable’s value is updated (i.e. when the step changing code calls .isVisisble(true)
or .isVisisble(false
)), the update
method above gets called, making the element visible. Also, for reasons that aren’t 100% clear to me right now, is a step like payment is initially invisible
#File: vendor/magento/module-checkout/view/frontend/web/template/payment.html
<li id="payment" role="presentation" class="checkout-payment-method" data-bind="fadeVisible: isVisible">
<!-- ... -->
<!-- ko foreach: getRegion('beforeMethods') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
<!-- ... -->
<!-- ko foreach: getRegion('payment-methods-list') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
<!-- ko foreach: getRegion('afterMethods') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
</li>
its templates won’t be rendered (i.e. fetched) until isVisisble
gets called. It’s not clear if this happens because of other subscribers to the observable, or if Knockout doesn’t render something until its actually DOM visible. One thing to maybe do here is grep though the js source for .subscribe
calls.
Also, some weird Magento inconsistencies. On an individual step object, the method is always isVisible
.
<li ... data-bind="fadeVisible: visible()">
<li ... data-bind="fadeVisible: isVisible">
However, on the RequireJs components that register the two Magento steps, which are the view models of these templates, Magento was inconsistent naming one method visible
, the other isVisible
. Also, the ()
on visible
seem extraneous. I’m not sure if Knockout removes them and knows its an observable, or if that actually fetches the observable’s value and is equivalent to something like fadeVisible: true
/fadeVisible: false
.