Skip to content

Architecture

The checkout has always been one of the most complex parts of any online store. With Hyvä Checkout, we have carefully considered how to structure the vast array of Frontend API requirements and preferences in such a way that we can meet them to a certain extent without compromising scalability, and ensure it functions as an extension of the backend.

To achieve this, we have divided the API into various sections, which can be expanded in the future by us or third parties.

Directories & Files

With the Frontend API, we've tried to establish a structure that's easy to understand without necessarily needing explanation. However, we still want to justify the choices we've made to answer any potential questions in advance.

Name Description
v1.phtml Everything related to the API resides in the Hyva_Checkout::page/js/api directory. It starts with a v1.phtml file where the global hyvaCheckout namespace and its sub-namespaces are defined. This file is supported by an accompanying v1 directory where several important directories emerge.
init.phtml API bootstrapping
/alpinejs The place where all AlpineJS-specific plugins reside.
/directive Deprecated - The place where all AlpineJS-specific directives reside.
/evaluation UX-related elements that serve as additions to the hyvaCheckout.evaluation sub-namespace.
/message UX-related elements that serve as additions to the hyvaCheckout.message sub-namespace.
/navigation UX-related elements that serve as additions to the hyvaCheckout.navigation sub-namespace.

Each sub-namespace is required to contain a file prefixed with init- to ensure that the after child container is added. This allows developers to make pinpoint additions specific to a particular sub-namespace.

Missing directories

At the time of writing, not every sub-namespace has a separate directory. This is mainly because there haven't been any specific additions needed for those sub-namespaces yet.

This may change in the future. These directories will always be named identically to the sub-namespace.

Layout

A clear file and directory structure requires a clear layout XML structure. This structure is primarily built in hyva_checkout_index_index.xml. The reason for this setup is that while the Main component of the checkout works dynamically, the page itself is set up statically. To prevent repeatedly loading the API, it is loaded once during page load. After that, the Magewire components handle the dynamic tasks.

Initially, hyva_checkout_index_index.xml in view/frontend/layout may seem overwhelming when you open it for the first time. Let's break down everything block by block to guide you through what is happening.

hyva.checkout.api

  • Type: Block
  • Alias: N/A
  • After: hyva.checkout.main
  • Template: Hyva_Checkout::page/js/api/v1.phtml
  • Parent: main

Injects the Frontend API by its version into the page right after the checkout main component.

hyva.checkout.api-v1.after

  • Type: Container
  • Alias: after
  • After: N/A
  • Parent: hyva.checkout.api

A designated after container has been added to prevent any additional JavaScript from being added before the API. This helps avoid future issues and provides more control over what is placed where in the DOM.

All AlpineJS components are located in the root of this container. Additionally, this is also the place to inject any API sub-namespaced init- blocks. These sub-namespaced init files, such as init-config.phtml or init-evaluation.phtml, have a designated after container of their own to allow for the injection of specific sub-namespaced JavaScript right after any initialization occurs.

Best practice for those who wish to extend the API is to always include an after aliased block as a child.

<referenceContainer name="hyva.checkout.api-v1.after">
    <block name="hyva.checkout.init-analytics"
           template="Hyva_Checkout::page/js/api/v1/init-analytics.phtml"
    >
        <!-- Best practice: After analytics templates should go into a specific directory. -->
        <!-- Path: Module_Name::page/js/api/v1/analytics/logic-code-description.phtml -->
        <container name="hyva.checkout.init-analytics.after" as="after"/>
    </block>
</referenceContainer>

For more details about the extending the API, please refer to the documentation available here

API

File: Hyva_Checkout::page/js/api/v1.phtml

The basic setup of the Frontend API consists of a single file where all the fundamental sections are outlined. We intentionally did not split this file into multiple separate files to prevent parts from being overwritten. This is also the main reason why this file is marked as @internal.

Sub-namespaces

The checkout Frontend API is divided into so-called sub-namespaces. These act as primary points of reference within the hyvaCheckout namespace and contain standalone functions.

// Add "foo" with value "bar" to group "documentation" in the browser session storage.
hyvaCheckout.storage.setValue('foo', 'bar', 'documentation')

In the example above, we target the storage namespace by executing the setValue() method.

We always recommend keeping it single-layer deep.

Wrong practice

hyvaCheckout.storage.local.setValue('foo', 'bar', 'documentation')

Good practice

hyvaCheckout.storage.setLocalValue('foo', 'bar', 'documentation')

Initialization

The Frontend API is initialized on the window event DOMContentLoaded. This happens thanks to the main sub-namespace, which is responsible for initializing all other sub-namespaces.

// File: Hyva_Checkout::page/js/api/V1/init.phtml

window.addEventListener('DOMContentLoaded', () => {
    hyvaCheckout.main.init(
        // Main Magewire wrapper component ID.
        'hyva-checkout-main',
        // Main content container holding all checkout step components.
        'hyva-checkout-container',
        // Callback to be used when the API initialization fails.
        exception => console.log(exception)
    )
})

The init() Promise automatically iterates over all sub-namespaces within the hyvaCheckout namespace and checks if each is either an object with methods or a function. If it's an object, it looks for an optional initialize() method and executes it if present, passing along the config object from the backend.

When executed, a window event specific to the sub-namespace is dispatched to listen for:

window.dispatchEvent(new CustomEvent(`checkout:init:${ subnamespace }`))

For example, if you need to execute code right after the storage sub-namespace is initialized:

window.addEventListener('checkout:init:storage', event => console.log('Storage subnamespace was successfully initialized.'))

After all sub-namespaces are initialized, the API is flagged as active, and an additional checkout:init:after event is dispatched on the window.

When an exception is thrown, it will be passed along to the provided exceptionCallback callback.

Extending

Extending the API is also possible for situations where the current options do not meet your needs. However, extending is only recommended if the level of abstraction is sufficient for reuse in other scenarios. For very specific requirements, we always recommend using an AlpineJS plugin, for example.

It's important to be aware that a rule for extending the API is that you can never target specific elements on a page based on an id, class, or data- attribute. If you need to work with a specific element, ensure that a certain function receives this element as an argument. The reason behind this is that the checkout is dynamic, and you can never rely on the presence of an element on the page.

A specific area has been reserved within the layout for adding additional blocks.

<!-- File: view/frontend/layout/hyva_checkout_index_index.xml -->

<referenceBlock name="hyva.checkout.api-v1.after">
    <block name="hyva.checkout.utils-extend" template="My_Example::page/js/hyva-checkout/api/v1/company-name-analytics.phtml"/>
</referenceBlock>

We're looking into an option to have a dedicated hyva.checkout.api-v1.extensions container specifically for API extensions. This container would likely be required before the hyva.checkout.api-v1.after container.

Next up, we need to fill in the gaps to make the sub-namespace available.

<!-- File: My_Example::page/js/hyva-checkout/api/v1/company-name-analytics.phtml -->

<script>
    'use strict';

    if (hyvaCheckout && ! hyvaCheckout.hasOwnProperty('companyNameAnalytics')) {
        hyvaCheckout.companyNameAnalytics = {
            clicksHistory: [],
            clicksHistoryInterval: null,

            initialize() {
                document.addEventListener('click', event => this.clicksHistory.push(event.target))
            },
            getClicksHistory() {
                return this.clicksHistory
            },
            clearClicksHistory() {
                this.clicksHistory = []
            },
            tailClicksHistory() {
                this.clicksHistoryInterval = setInterval(() => {
                    console.log('Clicks log', hyvaCheckout.companyNameAnalytics.getClicksHistory())
                    this.clearClicksHistory()
                }, 5000)
            },
            disableClicksHistory() {
                clearInterval(this.clicksHistoryInterval)
                console.log('Clicks history tailing disabled. Restart using the tailClicksHistory() method.')
            }
        }
    }

    // This acts only as an example and should not be used in production.
    hyvaCheckout.companyNameAnalytics.tailClicksHistory()
</script>

Missing methods

For cases where you would like to extend an existing sub-namespace with additional functions, we always recommend making a pull request.

For each core sub-namespace, there is a layout after container. We recommend using this container to write additions, ensuring that your extension is always in the correct place in the DOM. In this example, we extend navigation.

<referenceContainer name="hyva.checkout.init-navigation.after">
    <block name="hyva.checkout.navigation.to-first-step"
           template="My_Example::page/js/hyva-checkout/api/v1/navigation/step-to-first.phtml"
    />
</referenceContainer>

Where the implementation could look like this.

<!-- File: My_Example::page/js/hyva-checkout/api/v1/navigation/step-to-first.phtml -->

<?php

/** @var \Hyva\Theme\Model\ViewModelRegistry $viewModels */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Hyva\Checkout\ViewModel\Navigation $viewModel */

$viewModel = $viewModels->require(\Hyva\Checkout\ViewModel\Navigation::class);
$navigator = $viewModel->getNavigator();
$first = $navigator->getActiveCheckout()->getFirstStep();
?>
<script>
    'use strict';

    if (hyvaCheckout.navigation) {
        if (! hyvaCheckout.navigation.hasOwnProperty('stepToFirst')) {
            hyvaCheckout.navigation.stepToFirst = function () {
                hyvaCheckout.navigation.stepTo(
                    '<?= $escaper->escapeJs($first->getRoute()) ?>',
                    false
                )
            }
        }
    }
</script>

We recommend to separate each method extension into its own distinctive phtml file.

Complementary elements

For situations where you don't necessarily want to extend the API but still want to build an addition based on a specific part of the API, you can write a so-called complementary extension. A good example of this is the universal dialog, which serves as a UX element for the hyvaCheckout.message.dialog() function.

Because the API itself cannot target or hold specific UX elements, this function dispatches a window event that the complementary extension then listens for and acts upon.

<!-- File: Hyva_Checkout::page/js/api/v1.phtml -->

message = {
    dialog(title) {
        window.dispatchEvent(
            new CustomEvent('checkout:dialog:new', { detail: { title: title || 'Something went wrong' } })
        )
    }
}

Next up, we need to create a complementary element located in a specific sub-namespace directory followed by a method named phtml file.

There can only be one single complementary element for each sub-namespace method. This keeps things structured and clear for others.

<!-- File: Hyva_Checkout::page/js/api/message/dialog.phtml -->

<referenceContainer name="hyva.checkout.init-message.after">
    <block name="hyva.checkout.message.dialog"
           template="Hyva_Checkout::page/js/api/v1/message/dialog.phtml"
    />
</referenceContainer>

<!-- File: Hyva_Checkout::page/js/api/message/dialog.phtml -->

<div x-data="{ show: false, title: null }"
     x-on:checkout:dialog:new.window="event => {
         show = true
         title = event.detail.title
     }"
     x-show="show"
>
    <template x-if="title">
        <h3 x-text="title">
            <!-- Title placeholder -->
        </h3>
    </template>
</div>