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
Good practice
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:
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>