Blue background with pattern

Hyva: Improve wire:model in the Hyva Checkout

Jerke CombeeOrange dot22 Jul 2024

Usecase

Toen ik voor een klant aan de Hyva Checkout werkte, kwam ik een probleem tegen waarbij de klant om betere prestaties op een specifiek veld vroeg. Om het beeld te schetsen, in het adresformulier hebben we een veld voor het btw-nummer. Dit veld heeft een aangepaste haak die server-side validatie van vies btw bevat. Dit veld wordt heel vaak gevalideerd en creëert daarom ook veel verzoeken. We hebben dit al geminimaliseerd door de debounce-modifier toe te voegen met een debounce-tijd van 1500ms. Dit resulteert erin dat het verzoek slechts 1500ms na de laatste toetsaanslag op dat veld wordt verzonden.

Dit voldeed echter niet voldoende aan de eisen van onze klant, en daarom moesten we wat dieper in de functionaliteit van de modeldirectief (wire) duiken.

Welke opties hebben we

Om de werking van dit veld te verbeteren, moeten we kijken naar de opties die we hebben. Aangezien de Hyva Checkout is gemaakt met Magewire, dat zelf uit Livewire is opgebouwd, kunnen we kijken naar de opties die je hebt.

Debounce Modifier

Zoals we al in de inleiding besproken hebben, gebruiken we de debounce-modifier. Die wacht een bepaald aantal milliseconden na de laatste toetsaanslag voordat het het verzoek naar de server stuurt.

Implementatie

Om een debounce toe te passen, voeg je een modifier .debounce.1000ms toe aan het einde van de wire. Dit zal het verzoek voor 1 seconde vertragen. Je kunt de debounce-tijd aanpassen door de 1000 te vervangen. Dit om het aan te passen aan jouw wensen. Het eindresultaat ziet er als volgt uit:

<input name="foo" wire:key,lazy="foo"/>

Reden waarom we het niet gebruikten

Wanneer je een invoerelement invult en je gebruikt deze debounce-functie, moet de klant dit binnen een bepaalde tijd doen. De klant zal niet altijd zo snel zijn met de invoer, en dan zal het al het verzoek versturen.

In veel gevallen zal dit geen enorm probleem zijn, maar in dit geval was de validatie van het veld gedaan in een dienst van derden. Dit maakt dat het verzoek eerst naar Magento wordt gestuurd, dan naar deze dienst. Dit creëert veel overhead als dit te vaak gebeurt. Dus onze klant keurde deze optie niet goed.

Lazy Modifier

Om het kort te houden, zou je bijvoorbeeld een andere modifier kunnen gebruiken, zoals de .lazy modifier. Deze modifier activeert alleen bij het verlaten van een veld (wanneer je buiten het veld drukt).

Dit zorgt ervoor dat je het veld in je eigen tempo volledig kunt uittypen zonder gehaast te worden door de debounce. Op deze manier wordt slechts één oproep uitgevoerd en heb je een fatsoenlijke ervaring.

Implementatie

Om de lazy modifier te implementeren, plaats je simpelweg .lazy aan het einde van de wire. Dat ziet er zo uit:

<input name="foo" wire:key,lazy="foo"/>

Reden waarom we het niet gebruikten

Dit voldeed echter niet volledig aan onze wensen, aangezien het minder gewenst zou zijn omdat wanneer je dat veld verlaat, je al begint met typen in een ander veld. Dit komt omdat de validatie doorgaat via een API van derden nadat deze door onze API is gekomen. Dan wanneer het verzoek terugkomt zal dit resulteren in een validatiefout op het veld terwijl je al halverwege het typen in het volgende veld bent.

Aangepaste Directief

In sommige gevallen zoals de onze is er een gebrek aan dekking door de standaardfuncties van een framework. In deze gevallen ben je verplicht om aangepaste oplossingen voor dit te bouwen.

Om dit met Magewire te doen, doe je dit voornamelijk via directieven. Een directief heeft de mogelijkheid om functies toe te voegen die je door de hele applicatie heen kunt hergebruiken.

Directieven worden toegepast op allerlei HTML-elementen. Dit kan variëren van formulierelementen waar je luisteraars voor gebeurtenissen plaatst of invoervelden waar je de gegevens van het component aan koppelt.

Implementatie

Om een ​​nieuwe directive te maken moeten we eerst een nieuwe lay-out specificeren waar we dit kunnen plaatsen. Maar voordat we dit doen moeten we eerst een bestand maken dat ons script zal bevatten. Omdat we dit voor een specifieke module maken, plaatsen we het op deze locatie: Elgentos_HyvaCheckoutViesVat::form/field/script.phtml.

Begin met het bestand en geef het een eenvoudige bootstrap-inhoud:

<?php

/**
 * Copyright Elgentos BV. All rights reserved.
 * https://www.elgentos.nl/
 */

declare(strict_types=1);

?>

Nu we een bestand hebben, kunnen we beginnen met het maken van het bijbehorende layoutbestand. Omdat dit alleen wordt toegepast op de checkout, kunnen we de handle: hyva_checkout_components.xml gebruiken om onze directive in te voegen. Voor directives reserveert magewire een specifieke container met de naam: magewire.directive.scripts . Je layoutbestand ziet er dus ongeveer zo uit:

<?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 hebben nu een template die wordt geladen wanneer de klant in de checkout is. Dit zou dus het moment zijn waarop we beginnen met het schrijven van ons eerste deel van de directive. Om te beginnen schrijven we een eenvoudige directive om te zien hoe we ermee aan de slag kunnen. We schakelen terug naar het script.phtml-bestand en voegen deze code toe aan hun:


<script>
    Magewire.directive('model-char-limit', (el, directive, component) => {
        // ...
    });
</script>

Dit onderdeel zorgt voor de initialisatie van de directive die we bouwen. Je kunt deze directive nu toevoegen aan een veld waaraan je dit wilt toevoegen. Je kunt het als volgt toevoegen:

<input type="text" wire:model.lazy="vat" wire:model-char-limit.1500ms="7"/>

Dit zal onze char-limiet op het werkelijke model initialiseren. We hebben nog steeds de modeldirective nodig om te definiëren dat het daadwerkelijk een model is. De directive die we bouwen, zal alleen het gedefinieerde model ondersteunen.

Zoals je ziet, bestaat onze directive uit twee delen, het heeft de modifier na de directive zelf: .1500ms en de waarde 7 .

Laten we ons eerst richten op het ophalen van de waarde. We doen dit door dit toe te voegen aan de directivecode:


<script>
    Magewire.directive('model-char-limit', (el, directive, component) => {
        const limit = Number.parseInt(directive.method);
        console.log(limit);
    });
</script>

Deze code resulteert in het geval dat we de directive in het invoerelement dat we eerder hebben gemaakt, hebben gedefinieerd op 7. Laten we nu de modifier ophalen:


<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>

Opnieuw gebaseerd op de input van eerder zal dit ons nu 1500 geven. Dit is hoe je de data kunt extraheren die je definieert in de elementen. Vervolgens moeten we vinden wat het model voor ons heeft gedefinieerd. Om de modeldirective in zijn functionaliteit te kunnen ondersteunen, moeten we het pad dat het heeft gedefinieerd daarin hebben. We doen dit door het volgende toe te voegen:


<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>

Nu hebben we een script dat ons het pad geeft dat we in het model zelf hebben gedefinieerd. We kunnen dit pad nu gaan gebruiken om onze functionaliteit te creëren. De eerste stap die we moeten nemen is om de controle over de acties van de elementen te nemen. Omdat de kern van de bibliotheek dit doet met de x-model-directive van Alpine, zullen we dit ook doen. Het creëert deze modeldirective pragmatisch in JavaScript, zoals dit:


<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>

Nu dat gedeelte is afgerond, hebben we een component die kan worden aangesloten. Zoals je ziet hebben we return component.$wire.get(path); ingevoegd. Dit zorgt ervoor dat het magewire-model wordt verbonden met het alpine-model. Anders zal de ene de andere gewoon overschrijven en niet coherent werken.

Vervolgens gaan we de werkelijke invoergegevens van de clients gebruiken. Laten we beginnen met het lezen van de invoer en controleren of deze de tekenlimiet overschrijdt:


<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>

Nu hebben we daadwerkelijk onze eerste echte feature gemaakt met de methode data. Voor de volgende stap moeten we ook de debounce implementeren. Omdat de scope van de debounce code van magewire ons blokkeert om dit te hergebruiken, heb ik de vrijheid genomen om het te kopiëren en te gebruiken in deze directive. Door dit te doen, hebben we ook de laatste stap van het implementeren van de directive in onze codebase voltooid. Hier is dus de voltooide 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

Concluderend vereiste het verbeteren van de prestaties en functionaliteit van een specifiek BTW-nummerveld binnen de Hyva Checkout een op maat gemaakte aanpak die verder ging dan de beschikbare standaardopties. Door de debounce en andere modifiers te onderzoeken en ermee te experimenteren, werd het duidelijk dat geen van beide volledig voldeed aan de behoeften van de klant vanwege de complexiteit van server-side validatie met services van derden. Daarom werd een aangepaste directive gemaakt, die een verfijndere oplossing bood door het combineren van tekenlimiethandhaving met debounced input handling. Deze aanpak zorgde ervoor dat aan de vereisten van de klant werd voldaan, wat zorgde voor een soepelere en efficiëntere gebruikerservaring terwijl robuuste validatieprocessen werden gehandhaafd.