Form rendering
Ultimately, the goal is to make the form visible on the frontend. This can be achieved in various ways and depends on the driver behind the form. In the case of the checkout, forms will often be powered by Magewire.
Forms consist of different elements, each of which can be rendered independently. This means that each element and each field has its own template, with fields automatically defaulting to the "text" renderer.
Below, we explain the entire process in detail.
Renderers
The checkout contains a wrapper component called Main and all components that can contain forms are housed within it. These forms consist of elements and fields and, given the previous statement, the checkout can only render the elements and fields that fall within the Main component.
Elements and fields each have their own renderer. For elements, this is typically Hyva\Checkout\Model\Form\EntityFormElement\Renderer\Element
,
and for fields, it's Hyva\Checkout\Model\Form\EntityField\EntityFieldRenderer
. It is also possible to define a custom
renderer for each element or field, but we won't delve into that here.
A renderer can be accessed within a PHTML file using the getRenderer()
function. We can render the element itself
using a chained function render()
that expects an instance of the element itself.
A shortcut for this is $element->render()
, which automatically performs this for you.
<?php foreach ($form->getElements() as $element): ?>
<?= /* @noEscape */ $element->render() ?>
<?php endforeach ?>
The renderer object is important because, in addition to rendering the element or field itself, it can also be used within the template to render so-called "accessories".
Layout XML
Important
Everything comes together under a Magento concept that you should be familiar with: Layout XML. It is therefore important to understand how both elements, fields, and accessories can be rendered.
In addition to the layout handle hyva_checkout_components
, an additional handle is also loaded in the form of hyva_checkout_form_elements
.
The latter is responsible for assigning templates to form elements and therefore the entity-form.field-renderers
and
entity-form.element-renderers
blocks are made available.
These two blocks will be searched by the renderer for a matching child block alias
.
Elements
The element renderer will, by default, search for the following block aliases in entity-form.element-renderers
:
Level | Alias | Since |
---|---|---|
2 | {form_namespace}.{element_id} | 1.1.0 |
1 | {element_id} | 1.1.0 |
Which in the case of a "banner" example could look like:
Where we bind it to a specific template like:
<!-- File: view/frontend/layout/hyva_checkout_form_elements.xml -->
<referenceBlock name="entity-form.element-renderers">
<block name="form-element.promotion-banner"
as="promotion-banner"
template="My_Example::form/element/promotion-banner.phtml"
/>
</referenceBlock>
Block (element) aliases
As you can see, the element renderer is looking for two specific aliases: {form_namespace}.{element_id}
and {element.id}
.
Which of these aliases to choose depends on the situation and the uniqueness of the element ID.
In particular cases, you can imagine a global component renderer that may be used in multiple forms. In such scenarios, it may be the case that for one of those forms, the element should behave the same, but the HTML should differ. This means creating a template to serve as a generic base, and make a copy of it to apply modifications for the form-specific version.
Fields
The field renderer will search for a wider range of block aliases in entity-form.field-renderers
:
Level | Alias | Since |
---|---|---|
7 | {form_namespace}.{field_id}.{input_type} | 1.1.0 |
6 | {form_namespace}.{field_id} | 1.1.0 |
5 | {field_id}.{input_type} | 1.1.0 |
4 | {field_id} | 1.1.0 |
3 | {form_namespace}.{input_type} | 1.1.0 |
2 | {input_type} | 1.1.0 |
1 | text | 1.1.0 |
Due to this fallback mechanism, every field will eventually default to a text
type renderer, which inherently includes a template.
Hyvä Checkout also offers default templates for the most common input types such as select
, checkbox
, hidden
, and password
.
Accessories
Accessories are helper templates that can be utilized across multiple elements or fields and shouldn't necessarily have a direct relationship with the component itself. However, in some cases, this may depend on the accessory name itself.
It is important to note that accessories are specific to either elements or fields, and they cannot be interchanged. In other words, if you have a label accessory for a field, you cannot use it within an element, and vice versa.
Accessories are prefixed with an accessory.
alias prefix. This means that if you have a "label" accessory,
you add a child block to either the entity-form.element-renderers
or entity-form.field-renderers
, where you alias it with accessory.label
.
<!-- File: view/frontend/layout/hyva_checkout_form_elements.xml -->
<referenceBlock name="entity-form.field-renderers">
<!-- A form field label template. -->
<block name="form-field.global.label"
template="Hyva_Checkout::form/element/html/label.phtml"
as="accessory.label"
/>
</referenceBlock>
<referenceBlock name="entity-form.element-renderers">
<!-- A before accessory rendered with a container template that lets a block function as a container. -->
<block name="form-element.global.before"
template="Hyva_Checkout::form/element/html/container.phtml"
as="accessory.before"
>
<!-- Will automatically render like it sits within a container. -->
<block name="element-script-block"
template="My_Example::page/js/element-script.phtml"/>
</block>
</referenceBlock>
Accessory blocks will automatically receive the element or field block as the element
data attribute. In some cases there is also a parent
attribute available.
Accessories act as subordinates to the main element or field. They serve as support, where it's a best practice to always
add a renderBefore()
and renderAfter()
within your component. This provides the opportunity to subsequently insert
additional blocks in a controlled manner without having to override the template.
<?= $element->getRenderer()->renderBefore($element) ?>
<?= $element->getRenderer()->renderLabel($element) ?>
<input <?= /* @noEscape */ $element->renderAttributes($escaper) ?> />
<?= $element->getRenderer()->renderComment($element) ?>
<?= $element->getRenderer()->renderAfter($element) ?>
Accessory specific render method
Do not confuse the render methods as magic methods, where we have opted to only include predefined methods for the
accessories provided out of the box. We have, however, opened up the renderAccessory()
method to enable you to
render your own custom accessory (since 1.1.11).
Hyvä Checkout has already provided some of the most global accessories out of the box:
Accessory | Element | Field | Template | Method |
---|---|---|---|---|
before | Yes | Yes | Hyva_Checkout::form/element/html/container.phtml | renderBefore |
after | Yes | Yes | Hyva_Checkout::form/element/html/container.phtml | renderAfter |
label | No | Yes | Hyva_Checkout::form/element/html/label.phtml | renderLabel |
comment | No | Yes | Hyva_Checkout::form/element/html/comment.phtml | renderComment |
tooltip | No | Yes | Hyva_Checkout::form/element/html/tooltip.phtml | renderTooltip |
Forms
Forms are nothing more than a PHTML file that can receive a form object, through a ViewModel or Magewire component for example.
In this example, we assume the use of a Magewire component of type Hyva\Checkout\Magewire\Component\AbstractForm
.
<block name="example.form"
as="form"
template="Hyva_Checkout::magewire/component/form.phtml"
>
<arguments>
<argument name="magewire" xsi:type="object">
\My\Example\Magewire\Checkout\ExampleForm
</argument>
</arguments>
</block>
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Hyva\Checkout\Magewire\Component\AbstractForm $magewire */
$form = $magewire->getPublicForm();
?>
<form <?= /* @noEscape */ $form->renderAttributes($escaper) ?>>
<?php foreach ($form->getElements() as $element): ?>
<?php if ($element->canRender()): ?>
<?= /* @noEscape */ $element->render() ?>
<?php endif ?>
<?php endforeach ?>
</form>
For Magewire-driven forms, you may also utilize our pre-built form template, which can be found at Hyva_Checkout::magewire/component/form.phtml
As you can see, a form is very basic and primarily set up so that it can be modified from modifiers without the need to override the template.
Advanced rendering
Typically, many of your needs are already addressed by the out-of-the-box element templates we offer. However, behind the scenes, we've incorporated numerous unique rendering patterns to circumvent the need for template overrides in cases where a highly specific alteration is required.
Targeted accessories
Picture a scenario where you simply need to alter the label of a particular element or field, such as within the shipping address form,
without the hassle of assigning a dedicated template for that element. In such cases, we've implemented a pattern enabling
you to pinpoint a specific form element and designate a child block aliased as label
.
<!-- File: view/frontend/layout/hyva_checkout_form_elements.xml -->
<referenceBlock name="entity-form.field-renderers">
<!-- Target field by alias {form-namespace}.{field-id}. -->
<block name="specific-shipping-firstname" as="shipping.firstname">
<!-- Assign a custom template just for the label. -->
<block name="specific-shipping-firstname.label"
as="label"
template="My_Example::form/shipping/field/firstname-label.phtml"
/>
</block>
</referenceBlock>
Removing the label block will allow the field to automatically fallback onto the accessory.label
template.
The label rendering would resemble something along these lines:
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Hyva\Checkout\Model\Form\EntityField\AbstractEntityField $element */
/** @var \Magento\Framework\Escaper $escaper */
// The element object is automatically being passed along to the block object.
$element = $block->getData('element');
?>
<span class="font-bold text-blue-700 italic">
<?= $escaper->escapeHtml($element->getLabel()) ?>
</span>
The example above illustrates a form field, where elements can also be targeted by referencing the block entity-form.element-renderers
.
Wrapping a form element
Imagine a situation where you have a "Save to address book" checkbox, which is rendered using our native Hyva_Checkout::form/field/checkbox.pthml
template.
However, you want to enclose it with a <div class="bg-gray-100">...</div>
without having to rewrite all the code for the checkbox.
Doing so not only leads to a significant amount of duplicated code but also introduces inconsistencies across your checkboxes.
Therefore, we devised a solution that allows you to render the standard checkbox field within your custom template for the checkbox.
<!-- File: view/frontend/layout/hyva_checkout_form_elements.xml -->
<referenceBlock name="entity-form.field-renderers">
<!-- Target checkbox field by alias {form-namespace}.{field-id}.{input-type} -->
<block name="save_address_book_shipping"
as="shipping.save.checkbox"
template="My_Example::form/shipping/field/save-address-book.phtml"
/>
</referenceBlock>
The checkbox rendering would resemble something along these lines:
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Hyva\Checkout\Model\Form\EntityField\AbstractEntityField $element */
/** @var \Magento\Framework\Escaper $escaper */
// The element object is automatically being passed along to the block object.
$element = $block->getData('element');
?>
<div class="bg-gray-100 px-6 py-4">
<?= $element->getRenderer()->renderWithTemplate('Hyva_Checkout::form/field/checkbox.phtml', $element) ?>
</div>
Why opt for the specific use of the .checkbox
type specification?
Employing the shipping.save.checkbox
alias helps prevent a scenario where a different input type is unintentionally rendered using this custom template. In simpler terms, if a form modifier were implemented to change the input type to a select or button, it would default to its standard rendering. Failing to narrow it down specifically to .checkbox
could result in a select being rendered as a checkbox, as you can envision by examining the HTML structure.
Conditional displayment
Consider a scenario where a form element or field is meant to be displayed only if a specific system configuration setting permits it.
This can be achieved by incorporating a standard ifconfig
statement within your form element block.
This example illustrates a method to display or conceal the street field based on a setting, where it can seamlessly
revert to the standard text
field type if the config doesn't exist or returns false
.
<!-- File: view/frontend/layout/hyva_checkout_form_elements.xml -->
<block name="form-field.street"
as="street"
template="Hyva_Checkout::form/field/street.phtml"
ifconfig="hyva_themes_checkout/developer/address_form/use_street_renderer"
/>
Street field variants
By default, Magento adopts the approach of incorporating multiple (1-4) street fields to accommodate various address formats,
recognizing that some addresses require more than just a street and a house number. Determining the number of fields
displayed is a native Magento setting. However, fine-tuning their presentation to match your preferences is a feature
provided by Hyvä Checkout out of the box since version 1.1.1
.
Since its release, in addition to the default rendering that displays the fields underneath each other, we've introduced two new options: "Two Column Grid" and "One Column Row." You can find these options within the Hyvä Checkout system settings > Developer > Address Forms.