Usecase
When I was working for a Client on the Hyva Checkout I ran into a Issue where the client requested a better performance on a specific field. To paint the picture, in the address form we have a VAT number field. This field is has a custom hook that contains server side validation from vies vat. This field gets validated very often and therefore also creates a lot of requests. We already minimized this by adding the debounce modifier with a debounce time of 1500ms. This results in the request being send only 1500ms after the last key press on that field.
This however did not satisfy our customer enough, and therefore we needed to dive a bit deeper in the functionality of the model directive (wire:model).
What options do we have
To improve the functioning of this field we need to look into the options we have. Since the Hyva Checkout is made with Magewire which in itself is build out of Livewire we can look at the options you have.
Debounce Modifier
As we already discussed in the premise that we use the debounce modifier. Which basicly waits a set amount of miliseconds after the last key press before it does the request to the server.
Implementation
To implement a debounce you add a modifier .debounce.1000ms to the end of the wire:model directive. This will debounce the request for 1 second. You can adjust the debounce time by replacing the 1000. This to make it conform to your desire. The end result would look like this:
<input name="foo" wire:key,lazy="foo"/>
Reason we did not use it
Whenever you are filling out a input element and you use this debounce feature the customer has to put this in within a certain amount of time. The customer will not always be as quick with the input, and the it will already fire the request.
In a lot of cases this will not be a huge issue, in this case however the validation of the field was done in a 3rd party services. This makes the request first send to Magento, then to this service. This creates a lot of overhead if this happens to much. So our client did not approve of this option.
Lazy Modifier
To keep it short you could for instance use a different modifier, like the .lazy modifier. This modifier will only trigger on the blur of a field (when you press outside of the field).
This way will make sure you can full[y type out the field in your own pace without having to be hasted by the debounce. This way only one call will be executed and you will have a decent experience.
Implementation
To implement the lazy modifier you simply place .lazy at the end of the directive wire:model. That will look like this:
<input name="foo" wire:key,lazy="foo"/>
Reason we did not use it
This however did not satisfy us fully, since it would be less desired since when you leave that field you will already start typing in another field. This is because the validation goes through a third party api after it came through our api. Then when the request returns this will result in a validation error on the field while you are already halfway through typing in the next field.
Custom directive
In some cases like ours there is a lack of coverage through the default features of a framework. In these cases you are required to build custom solutions for this.
To do this with Magewire you do this mainly through directives. A directive has the possibility to add features that you can reuse throughout the application.
Directives are applied to all sorts of HTML Elements. This can diverse from form elements where you put listeners for events or inputs where you link the data from the component to.
Implementation
To create a new directive we first need to specify a new layout where we can place this. But before we do this we will first have to make a file that will contain our script. Since we are making this for a specific module we will place it on this location: Elgentos_HyvaCheckoutViesVat::form/field/script.phtml.
Start with the file and give it just a simple bootstrap content:
<?php /** * Copyright Elgentos BV. All rights reserved. * https://www.elgentos.nl/ */ declare(strict_types=1); ?>
Now we have a file we can start creating the corresponding layout file. Since this will only be applied to the checkout we can use the handle: hyva_checkout_components.xml to insert our directive. For directives magewire reserves a specific container named: magewire.directive.scripts . So your layout file will look something like this:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="magewire.directive.scripts"> <block name="magewire.directive.model.vat" template="Elgentos_HyvaCheckoutViesVat::form/field/script.phtml"/> </referenceContainer> </body> </page>
We now have a template that is being loaded in when the customer is in the checkout. So this would be the moment where we start writing our first part of the directive. For starter we will write a simple directive to see how we can start working with it. We switch back to the script.phtml file and add this code in their:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { // ... }); </script>
This part will take care of the initialization of the directive we are building. You can now add this directive to a field that you want to add this to. You can add it like this:
<input type="text" wire:model.lazy="vat" wire:model-char-limit.1500ms="7"/>
This will initialize our char limit on the actual model. We still need the model directive to define that it is actually a model. The directive we are building will only be supporting the model defined.
As you see our directive has two parts, it has the modifier after the directive itself: .1500ms and the value 7 .
Let’s first focus on retrieving the value. We do this by adding this in the directive code:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); console.log(limit); }); </script>
This code will result in case of how we defined the directive in the input element we created earlier is 7. Next up, lets get the modifier:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); const time = directive.modifiers.length > 0 ? Number.parseInt(directive.modifiers[0].substr( 0, directive.modifiers[0].length-2) ) : 150; console.log(time); </script>
Again based on the input from earlier this will now give us 1500 . This is how you can extract the data that you define in the elements. Next up, we need to find what model has defined for us. In order to be able to support the model directive in its functionality we need to have the path it has defined in there. We do this by adding:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); const time = directive.modifiers.length > 0 ? Number.parseInt(directive.modifiers[0].substr( 0, directive.modifiers[0].length-2) ) : 150; const path = el.getAttributeNames() .filter(attr => attr.substr(0, 10) === 'wire:model' && attr.substr(0, 21) !== 'wire:model-char-limit') .map(attr => el.getAttribute(attr)).pop(); console.log(path); }); </script>
Now we have a script that will give us the path that we defined in the model itself vat. We can now start using this path to create our functionality. The first step we need is to take control of the elements actions. Since the core of the library does this with alpine’s x-model directive, we will do this as well. It creates this model directive pragmatically in JavaScript like this:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); const time = directive.modifiers.length > 0 ? Number.parseInt(directive.modifiers[0].substr( 0, directive.modifiers[0].length-2) ) : 150; const path = el.getAttributeNames() .filter(attr => attr.substr(0, 10) === 'wire:model' && attr.substr(0, 21) !== 'wire:model-char-limit') .map(attr => el.getAttribute(attr)).pop(); Alpine.bind(el, { ['x-model']() { return { get() { return component.$wire.get(path); }, set(value) { // }, } }, }); }); </script>
With that part done, we now have a component that can hook into. As you see we have inserted return component.$wire.get(path); . This takes care of connecting the magewire model with the alpine model. Otherwise one will just start overwriting the other and not work coherently.
Next up, we will start using the actual input data from the clients. Let’s start with reading the input and check if it pases the character limit:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); const time = directive.modifiers.length > 0 ? Number.parseInt(directive.modifiers[0].substr( 0, directive.modifiers[0].length-2) ) : 150; const path = el.getAttributeNames() .filter(attr => attr.substr(0, 10) === 'wire:model' && attr.substr(0, 21) !== 'wire:model-char-limit') .map(attr => el.getAttribute(attr)).pop(); let sharedValue; function update(value) { [el, directive, component] if (!sharedValue || sharedValue.length <= limit) { return; } component.$wire.sync(path, sharedValue); } Alpine.bind(el, { ['x-model']() { return { get() { return component.$wire.get(path); }, set(value) { update(value); }, } }, }); }); </script>
Now we have actually created our first actual feature with the method data. For the next step we will need to also implement the debounce. Since the scope of the debounce code of magewire blocks us from reusing this I took the liberty of copying it and using it in this directive. By doing this we will also have finished tha last step of implementing the directive in our code base. So here is the finished directive:
<script> Magewire.directive('model-char-limit', (el, directive, component) => { const limit = Number.parseInt(directive.method); const time = directive.modifiers.length > 0 ? Number.parseInt(directive.modifiers[0].substr( 0, directive.modifiers[0].length-2) ) : 150; const path = el.getAttributeNames() .filter(attr => attr.substr(0, 10) === 'wire:model' && attr.substr(0, 21) !== 'wire:model-char-limit') .map(attr => el.getAttribute(attr)).pop(); let sharedValue; function update() { [el, directive, component] if (!sharedValue || sharedValue.length <= limit) { return; } component.$wire.sync(path, sharedValue); } // Trigger a network request (only if .live or .lazy is added to wire:model)... const debouncedUpdate = debounce(update, time); Alpine.bind(el, { ['x-model']() { return { get() { return component.$wire.get(path); }, set(value) { sharedValue = value; debouncedUpdate(); }, } }, }); }); function debounce(func, wait) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; func.apply(context, args); } clearTimeout(timeout); timeout = setTimeout(later, wait); return () => clearTimeout(timeout); } } </script>
Conclusion
In conclusion, enhancing the performance and functionality of a specific VAT number field within the Hyva Checkout required a tailored approach beyond the standard options available. By examining and experimenting with the debounce and other modifiers, it became clear that neither fully met the client's needs due to the complexity of server-side validation involving third-party services. Therefore, a custom directive was created, offering a more refined solution by combining character limit enforcement with debounced input handling. This approach ensured the client's requirements were met, providing a smoother and more efficient user experience while maintaining robust validation processes.