Magewire driven forms
You have complete freedom in deciding how to manage your forms. While utilizing Magewire is one option, you could alternatively opt for a more frontend-centric approach such as AlpineJS. Each method has its own advantages and disadvantages, and the ideal choice depends on the specific use case.
Why use it?
Magewire presents a technique that provides both backend and frontend developers a means to create dynamic forms run via the server by XHR requests. This enables direct data syncing to the server, either on field completion or full form submission. Magewire offers a wide range of features to accommodate various requirements. If you lean towards a server-driven approach, Magewire can be utilized, powering default forms such as shipping and billing forms out of the box.
Example scenario
Since version 1.1.13, a new Magewire component has been unveiled, offering extensibility when constructing your form object. This component serves as a robust abstraction, converting your form into a dynamic entity that can be augmented with modifiers. It comes equipped with numerous modification hooks, allowing for the integration of custom hooks as needed to meet specific requirements.
For more global details on constructing a form, please refer to the documentation available here
We'll analyze a minimized version of the Guest Details component that is available from version 1.1.12.
1. Constructing the form component
<?php
class GuestDetails extends \Hyva\Checkout\Magewire\Component\AbstractForm
{
public bool $customerExists = false;
public function boot(): void
{
parent::boot();
$email = $this->getForm()->getField(GuestDetailsForm::FIELD_EMAIL);
if ($email && $email->hasValue()) {
$this->handleCustomerExistence($email->getValue());
}
}
// Magewire magic property hook method for $this->data['email_address'].
public function updatedDataEmailAddress($value)
{
$this->handleCustomerExistence($value);
// Submit and save the email value using the forms Save Service.
$this->submit([GuestDetailsForm::FIELD_EMAIL => $value]);
return $value;
}
// Try and toggle the customer existence variable value to either true or false.
private function handleCustomerExistence(string $email): void
{
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
try {
$this->customerExists = !$this->accountManagement->isEmailAvailable($email);
} catch (LocalizedException $exception) {
$this->logger->critical(sprintf('Something went wrong while checking the availability of the email address %s', $email));
}
}
}
}
2. Apply modifications (optional)
Additionally, we have the option to extend the base form on various triggers including form:init
, form:build:magewire
,
and form:execute:submit:magewire
.
For readability purposes, the hook callback methods are omitted in this example.
These modifier hooks ensure the following requirements:
On form:init
:
- The email field has the
autocomplete
attribute set tooff
. - An advanced form validation rule
email
is set totrue
. - The password field, along with a comment, is created and added.
- The submit button element, along with a custom
Submit
label, is created and added.
On form:build:magewire
:
- Utilizing the extended AlpineJS component
initMagewireFormForGuestDetails($el, $wire)
via ax-data
attribute on the form. - Setting a
wire:loading.attr
attribute asdisabled
on each field. - Hiding both the submit element and password field if the customer email address is non-existent.
- Making the fields compatible with the Magewire component by setting a
wire:model.defer
attribute asdata.{field_id}
. - Binding a new
submit
value onto the Magewire$loader
property, displayingTrying to authenticate
when the button is clicked.
<?php
class WithAuthenticationModifier extends \Hyva\Checkout\Model\Form\EntityFormModifierInterface
{
public function construct(
private readonly \Hyva\Checkout\Model\ConfigData\HyvaThemes\SystemConfigGuestDetails $systemConfigGuestDetails
) {
//
}
public function apply(\Hyva\Checkout\Model\Form\EntityFormInterface $form): \Hyva\Checkout\Model\Form\EntityFormInterface
{
if (!$this->systemConfigGuestDetails->enableLogin()) {
return $form;
}
$form->registerModificationListener(
'includeAuthentication',
'form:init',
fn ($form) => $this->includeAuthentication($form)
);
$form->registerModificationListener(
'handleMagewireAuthenticationForm',
'form:build:magewire',
fn (\Hyva\Checkout\Model\Form\AbstractEntityForm $form, \Hyva\Checkout\Magewire\Checkout\GuestDetails $component)
=> $this->handleMagewireAuthenticationForm($form)
);
$form->registerModificationListener(
'handleMagewireAuthenticationVisibility',
'form:build:magewire',
fn (\Hyva\Checkout\Model\Form\AbstractEntityForm $form, \Hyva\Checkout\Magewire\Checkout\GuestDetails $component)
=> $this->handleMagewireAuthenticationVisibility($form, $component)
);
$form->registerModificationListener(
'handleAuthenticationSubmitAction',
'form:execute:submit:magewire',
fn (\Hyva\Checkout\Model\Form\AbstractEntityForm $form, \Hyva\Checkout\Magewire\Checkout\GuestDetails $component, $result, $data, $exception)
=> $this->handleAuthenticationSubmitAction($form, $component, $result, $data, $exception)
);
return $form;
}
}
For a comprehensive understanding of the callback methods, please refer to \Hyva\Checkout\Model\Form\EntityFormModifier\GuestDetailsForm\WithAuthenticationModifier
3. Assign the modifier to the form
<!-- File: etc/frontend/di.xml -->
<type name="Hyva\Checkout\Model\Form\EntityForm\GuestDetailsForm">
<arguments>
<argument name="entityFormModifiers" xsi:type="array">
<item name="with_authentication_feature" xsi:type="object">Hyva\Checkout\Model\Form\EntityFormModifier\GuestDetailsForm\WithAuthenticationModifier</item>
</argument>
</arguments>
</type>
4. Make the checkout aware of the component
<!-- File: view/frontend/layout/hyva_checkout_components.xml -->
<referenceBlock name="hyva.checkout.components">
<container name="checkout.guest-details.section">
<block name="checkout.guest-details"
template="Hyva_Checkout::magewire/component/form.phtml"
>
<arguments>
<!-- Transform the form into a Magewire driven form. -->
<argument name="magewire" xsi:type="object">
\Hyva\Checkout\Magewire\Checkout\GuestDetails
</argument>
</arguments>
</block>
</container>
</referenceBlock>
5. Inject the component into the checkout shipping step
<!-- File: view/frontend/layout/hyva_checkout_default_shipping.xml -->
<body>
<move element="checkout.guest-details.section"
destination="column.main"
before="-"/>
</body>
Auto Saving
Available since 1.1.27
Until version 1.1.27
, auto-saving forms in Hyvä Checkout was an automated process. This ensured that form data was
always synchronized with the server, optionally required by other components like the price summary.
While this approach guaranteed all data was readily available at all times, we realized that only a small subset of data
is typically necessary for the checkout to function properly. Therefore, we have adjusted this concept,
giving developers greater control over which data is stored.
The shift away from forms that automatically save themselves offers several key benefits:
- Greater control over when and where data is stored.
- Improved user experience, with no more unexpected behaviors while filling in forms.
- Frontend JavaScript validation can now occur before any XHR requests are made.
The solution
Hyvä Checkout relies heavily on navigation buttons to guide users through the checkout process, either advancing them to the next step or completing their order, depending on the configuration. While Hyvä Checkout offers exceptional flexibility, these navigation buttons remain a core element, serving as the optimal anchor point for binding functionality.
From a framework perspective, the Evaluation API integrates with these buttons to trigger asynchronous functionality as needed. This enables Evaluation Navigation Tasks—dynamically injected via PHP code—to be executed when a button is clicked. These tasks take priority, running before any other actions are performed and optionally fail when for instance form validation failed.
Forms built using either Hyva\Checkout\Magewire\Components\AbstractForm
or the deprecated Hyva\Checkout\Magewire\Checkout\AddressView\AbstractMagewireAddressForm
will automatically include a validation task. This ensures the checkout checks for any pending auto-save actions. If such actions are required, they are executed before the primary action, such as moving to the next step.
Example on how this is done:
<?php
$evaluationBatch->misses(fn (EvaluationResult $result) => $result->hasAlias('submit'), function (EvaluationResultBatch $batch) {
$batch->push(
$batch->getFactory()
->createValidation('magewire-form')
->withDetails([
'saveAction' => 'autosave'
])
->withAlias('submit')
->withStackPosition(100)
);
});
The code is designed to be self-explanatory, automatically injecting a custom validation when a batch item flagged with submit
is missing.
However, developers can still inject their own validations, which will take precedence and skip the default injection if they already exist.
For more details about the Evaluation API, please refer to the documentation available here
The wire:auto-save
Directive
As we delegate more responsibility to developers, a way to flag fields for auto-saving became essential.
This is where the new custom wire:auto-save
directive comes into play. It can be applied to an input field within a Magewire component,
such as a form, alongside the wire:model.defer
directive.
These two directives complement each other: wire:model.defer
binds the field's data, while wire:auto-save
indicates
that the field's data doesn’t need to be continuously saved to the backend. Instead, it will be automatically saved when
the user interacts with one of the primary navigation buttons.
<form id="shipping">
<!-- Marks this input for inclusion in the form's auto-save navigation task. -->
<input type="text" wire:model.defer="firstname" wire:auto-save/>
<!-- Explicitly links this input to the form with the specified ID for auto-saving. -->
<input type="text" wire:model.defer="firstname" wire:auto-save="shipping"/>
<!-- Configures this input to auto-save itself after the default delay of 1.5 seconds. -->
<input type="text" wire:model.defer="firstname" wire:auto-save.self/>
<!-- Configures this input to auto-save itself after a custom delay of 3 seconds. -->
<input type="text" wire:model.defer="firstname" wire:auto-save.self.3000ms/>
</form>
The Auto Save directive requires a corresponding wire:model.defer
directive. If it is missing, a console error will be thrown.