Skip to content
intermediate Phase 46 · Checkout Implementation

KnockoutJS in Checkout

Using KnockoutJS observables, bindings, view models, and checkout components in Magento 2

1h
0 problems
Topic Progress 0%

KnockoutJS Observables

Observable Basics

// Simple observable
var firstName = ko.observable('John');
console.log(firstName()); // 'John'
firstName('Jane');
console.log(firstName()); // 'Jane'

// Observable array
var items = ko.observableArray([1, 2, 3]);
items.push(4);
console.log(items()); // [1, 2, 3, 4]

// Computed observable
var fullName = ko.computed(function() {
    return firstName() + ' ' + lastName();
});

Checkout Observables

// Magento checkout uses observables extensively
var shippingMethod = ko.observable();
var paymentMethod = ko.observable();
var isShippingLoading = ko.observable(false);

// Observable for selected address
var selectedShippingAddress = ko.observable({
    firstname: 'John',
    lastname: 'Doe',
    street: ['123 Main St'],
    city: 'New York',
    country_id: 'US',
    region_id: 43,
    postcode: '10001',
    telephone: '555-1234'
});

// Computed: check if form is valid
var isFormValid = ko.computed(function() {
    var address = selectedShippingAddress();
    return address.firstname &&
           address.lastname &&
           address.street &&
           address.city &&
           address.postcode;
});

KnockoutJS Bindings

Common Bindings

<!-- text binding -->
<span data-bind="text: customerName"></span>

<!-- value binding -->
<input type="text" data-bind="value: firstName">

<!-- foreach binding -->
<div data-bind="foreach: shippingMethods">
    <div class="method">
        <input type="radio" data-bind="value: code, checked: $parent.selectedMethod">
        <span data-bind="text: title"></span>
        <span data-bind="text: price"></span>
    </div>
</div>

<!-- visible/if bindings -->
<div data-bind="visible: isSelected()">
    <!-- Visible when isSelected is true -->
</div>

<div data-bind="if: hasShippingAddress()">
    <!-- Rendered only if condition is true -->
</div>

<!-- click binding -->
<button data-bind="click: placeOrder">Place Order</button>

<!-- css binding -->
<div data-bind="css: { 'active': isActive(), 'loading': isLoading() }">
</div>

<!-- attr binding -->
<input type="text" data-bind="attr: { placeholder: fieldPlaceholder }">

View Models

Checkout View Model

// app/code/Vendor/Checkout/view/frontend/web/js/view/shipping.js
define([
    'jquery',
    'ko',
    'uiComponent',
    'Magento_Checkout/js/model/quote',
    'Magento_Checkout/js/action/set-shipping-information',
], function ($, ko, Component, quote, setShippingInformationAction) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_Checkout/shipping'
        },

        /** Initialize component */
        initialize: function () {
            this._super();

            // Subscribe to quote changes
            quote.shippingAddress.subscribe(this.onAddressChange.bind(this));

            return this;
        },

        /** Observable properties */
        shippingMethods: ko.observableArray([]),
        selectedMethod: ko.observable(),
        isLoading: ko.observable(false),

        /** Methods */
        getShippingMethods: function () {
            return this.shippingMethods();
        },

        selectMethod: function (method) {
            this.selectedMethod(method.code);
            this.setShippingMethod();
        },

        setShippingMethod: function () {
            var self = this;
            self.isLoading(true);

            setShippingInformationAction().done(function () {
                self.isLoading(false);
                // Navigate to next step
            });
        },

        onAddressChange: function (address) {
            this.loadShippingMethods(address);
        },

        loadShippingMethods: function (address) {
            var self = this;
            $.ajax({
                url: '/rest/V1/carts/mine/shipping-methods',
                data: JSON.stringify({ address: address }),
                success: function (methods) {
                    self.shippingMethods(methods);
                }
            });
        }
    });
});

Checkout Components

Component Registration

// Register component in checkout
require(
    ['Magento_Checkout/js/model/checkout-layout'],
    function (checkoutLayout) {
        checkoutLayout.registerComponent(
            'customStep',
            {
                component: 'Vendor_Checkout/js/view/custom-step',
                sortOrder: 2,
                template: 'Vendor_Checkout/custom-step'
            }
        );
    }
);

Checkout Provider

// Checkout data provider
define([
    'jquery',
    'uiComponent',
    'Magento_Checkout/js/checkout-data'
], function ($, Component, checkoutData) {
    return Component.extend({
        defaults: {
            listens: {
                '${$.shippingAddressForm}:value': 'handleShippingAddressChange'
            }
        },

        initObservable: function () {
            this._super()
                .observe({
                    shippingAddress: null,
                    paymentMethod: null
                });

            return this;
        },

        handleShippingAddressChange: function (address) {
            this.shippingAddress(address);
            checkoutData.setShippingAddressFromData(address);
        }
    });
});

Template Example

<!-- Vendor/Checkout/view/frontend/web/template/shipping.html -->
<div class="shipping-step" data-bind="visible: isVisible()">
    <h2>Shipping Information</h2>

    <div data-bind="foreach: getShippingMethods()">
        <div class="shipping-method" data-bind="click: $parent.selectMethod">
            <input type="radio" data-bind="checked: $parent.selectedMethod, value: code">
            <span data-bind="text: carrier_title"></span>
            <span data-bind="text: method_title"></span>
            <span data-bind="text: $parent.formatPrice(price)"></span>
        </div>
    </div>

    <div data-bind="visible: isLoading()">
        <span>Loading shipping methods...</span>
    </div>
</div>

Quiz

1. What does ko.observable() create?

Question 1 options

2. What does the 'foreach' binding do?

Question 2 options

3. What is a computed observable?

Question 3 options

Flashcards

Question

What is ko.observable()?

Answer

Creates a reactive property that notifies subscribers when changed

Question

What is a computed observable?

Answer

Auto-recalculates when any of its dependent observables change

Question

data-bind='foreach' does what?

Answer

Renders child elements for each item in an array

Question

How to subscribe to observable changes?

Answer

observable.subscribe(function(newValue) { ... })

Revision Notes

Key Takeaways

  • 1. Observables create reactive properties that auto-update UI on change
  • 2. Computed observables derive values from other observables automatically
  • 3. View models extend uiComponent and use knockout bindings in templates
  • 4. foreach binding renders array items as child elements
  • 5. Magento checkout components use observables for state management

Interview Tips

  • Explain the difference between observable, observableArray, and computed
  • Describe how KnockoutJS handles two-way data binding in forms
  • Discuss the component initialization flow: defaults → initialize → initObservable

Cheat Sheet

KnockoutJS:
  ko.observable(val)         → reactive property
  ko.observableArray([])     → reactive array
  ko.computed(fn)            → auto-recalculates

Bindings:
  text, value, checked       → content
  foreach, if, visible       → flow control
  click, event               → interactions
  css, attr                  → styling

View Model:
  extend uiComponent
  template: 'path/to/template'
  initialize() → this._super()