Your web browser is out of date. Update your browser for more security, speed and the best experience on this site.

Angular Signal Forms: één schema, drie schermen

Hoe maak je formuliervalidatie herbruikbaar met Angular Signal Forms?

Kopieer je hetzelfde adresformulier naar drie schermen, dan groeien de validatieregels vroeg of laat uit elkaar. Angular Signal Forms legt die regels bij het type in plaats van bij het scherm. Het resultaat: één schema, drie schermen en conditionele validatie zonder één subscription.

Door Tim Massart, Fullstack Developer

Deel dit artikel
Front End

Waarom lopen grote formulieren uit de hand?

Grote formulieren lopen zelden vast op één slechte beslissing. Meestal begint het met een gekopieerd blok: hetzelfde adresformulier voor checkout, levering en het gebruikersprofiel.

Daarna groeien de kopieën uit elkaar. Het leveringsadres moet plots anders gevalideerd worden dan het facturatieadres. Die logica belandt in een setValidators() binnen een valueChanges()-subscription, en daar ontstaan volgordebugs.

Intussen betekent elke wijziging in validatie: drie plekken aanpassen, en hopen dat je er geen vergeet.

Kort antwoord: het probleem is niet de hoeveelheid code, maar dat dezelfde regels op meerdere plekken leven. Elke kopie is een plek waar ze kunnen afwijken.

Wat verandert Angular Signal Forms aan formuliervalidatie?

Signal Forms verplaatst validatie van het scherm naar het type. Je data zit in een WritableSignal<T> en form(model, schema) leidt daaruit een field tree af. De regels staan in een schema: een gewone functie die je één keer schrijft en mount waar die shape opduikt.

10 tabel reactive template signal forms

Hoe bouw je een herbruikbaar adresformulier met Signal Forms?

Stap 1: begin bij het model

Elke signal form start bij een model. Twee regels zijn hier belangrijk: geen optionele properties, en geen undefined of null als startwaarde.

De FieldTree wordt afgeleid uit het model. Een veld met waarde undefined bestaat daardoor niet in je formulier. Initialiseer dus met lege strings.

01 address model

Stap 2: zet alle regels in één schema

Het schema bevat alle validatie voor het model. De postcoderegel leest het land via valueOf(address.country) en valideert opnieuw zodra het land verandert. Geen subscription, geen updateValueAndValidity().

02 address schema

Drie details maken het verschil:

  • Type-safe mounten. Het expliciete type in schema<Address> laat de compiler controleren of je het schema op de juiste subtree mount.
  • Ingebouwde validators eerst. required, min, max, minLength, maxLength, minDate, maxDate en pattern leveren metadata die [formField] doorgeeft aan native controls. Zo worden onder meer required, min, max, minlength en maxlength automatisch native attributes; pattern is de uitzondering.
  • Niet elke validator levert metadata. email() valideert wel, maar geeft geen constraint-metadata door.

Stap 3: laat de component enkel de mark-up bezitten

De adrescomponent roept nooit form() aan, declareert geen validatie, heeft geen output en weet niet of ze een facturatie- of leveringsadres toont. Ze krijgt een FieldTree binnen en bindt die aan de inputs.

03 address form component

Schrijven via [formField] werkt rechtstreeks het model signal van de parent bij. De field tree houdt geen kopie van de data bij, dus er valt niets te synchroniseren. Bij Reactive Forms heb je daarvoor vaak extra plumbing nodig, zoals een ControlValueAccessor of ControlContainer met viewProviders.

Waarom geen eigen form() in de child? Eén form()-call betekent één validity tree. customer().valid() houdt automatisch rekening met elke descendant, en een submit markeert de hele boom in één keer als touched.

De uitzondering is een sub-formulier met een eigen levenscyclus, zoals een modal die je kunt annuleren. Daar bezit de modal zijn eigen model en geeft hij pas een afgewerkte waarde terug.

04 customer schema

De twee adresblokken verschillen enkel in de FieldTree die ze krijgen, plus een legend en een idPrefix. Geen formGroupName, geen botsende namen: de identiteit van een veld zit in het pad, niet in een string. Een derde adres is één extra <app-address-form> en één extra apply().

Stap 4: mount hetzelfde schema op drie schermen

05 apply drie schermen

Dat is het volledige hergebruikverhaal. applyEach() mount het schema op elk element van een array, ook op rijen die later worden toegevoegd. Bij FormArray.push() bouwt elke call site zijn eigen group op, en precies daar lopen implementaties uit elkaar.

Wanneer extraheer je géén component? Als het blok één keer voorkomt en klein is. Vijf velden op één scherm kosten je dan een extra bestand, een input-contract en een laag indirectie, zonder iets op te leveren.

Hoe werkt conditionele validatie zonder subscriptions?

Facturatie- en leveringsadres koppelen met hidden()

Eén checkbox “zelfde als facturatie” volstaat. Je zet nergens validators aan of uit.

06 checkout schema hidden

Een hidden field slaat validatie volledig over en telt niet mee voor de validity van zijn parent. Vink je de checkbox aan, dan ligt de hele shipping-subtree stil. Vink je ze af, dan gelden exact dezelfde regels opnieuw, zonder restanten. Net daar was clearValidators() gevoelig voor fouten.

hidden heeft geen native HTML-tegenhanger. Je template slaat het blok dus zelf over:

07 checkout template hidden

applyWhen of applyWhenValue: wanneer gebruik je welke?

De when-optie schakelt één veld. Voor een heel blok regels heb je twee tools.

11 tabel applywhen vs applywhenvalue
08 delivery applywhenvalue

Beide bepalen of regels actief zijn, niet of ze bestaan. Gebruik binnen de callback altijd de scoped path-parameter (address, separate), niet de buitenste.

De afweging tussen boolean en union is reëel:

  • Boolean: bindt rechtstreeks aan een checkbox en bewaart wat de gebruiker al typte, maar houdt een betekenisloze shippingAddress in je model.
  • Union: maakt illegale toestanden onmogelijk, maar verliest het concept bij elke moduswissel en vraagt een guarded cast in een computed om te renderen.

Vuistregel: kies de union als de varianten echt andere velden hebben. Kies de boolean als de ene variant gewoon de andere is, met de validatie uit.

Hoe pak je async validatie aan in Signal Forms?

Of een adres leverbaar is, kan enkel de server beoordelen. Met Reactive Forms schreef je daarvoor een AsyncValidator met eigen debounce en switchMap. In Signal Forms zit die bouwsteen in het schema: validateHttp() verstuurt de request, debounce() bepaalt wanneer.

09 verify address rule

Vier dingen om te onthouden:

  • Sync eerst, dan async. Async validatie start pas als elke synchrone regel op dat veld geslaagd is. Een lege of foute postcode bereikt het netwerk dus nooit.
  • Geen stale responses. validateHttp() bouwt op Angulars resource primitives: een lopende request wordt geannuleerd zodra de parameters wijzigen. De switchMap die je vroeger zelf schreef, valt weg.
  • Debounce werkt op het hele veld. debounce(address.postalCode, 'blur') vertraagt niet enkel de validator: andere logica die van postalCode afhangt, ziet de nieuwe waarde ook pas na blur.
  • valid() is niet !invalid(). Tijdens async validatie kunnen beide false zijn terwijl pending() true is. Houd daar rekening mee in je UI.

De regel staat bewust buiten addressSchema. Het profielscherm hoeft niet elk opgeslagen adres te verifiëren, en een rule function componeert net als een schema. De caller kiest.

Wanneer kies je voor Signal Forms?

Het patroon in één zin: validatieregels wonen bij het type, niet bij het scherm, en elk scherm met dezelfde shape erft ze automatisch mee.

Signal Forms wint daarbij niet door minder code, maar door code die maar op één plek kan staan. Het is een gewone, getypeerde functie, en de compiler bewaakt dat je ze op de juiste shape mount.

Een wondermiddel is het niet. Een dure validator wordt er niet goedkoper van. Voor een klein, eenmalig formulier blijven Template-Driven Forms een lichte keuze, en Reactive Forms blijven redelijk in een codebase die niet toch al in beweging is.

Voor een formulier dat op meerdere schermen dezelfde vorm aanneemt, is het verschil niet cosmetisch. Dan kies je tussen een regel op één plek wijzigen, of dezelfde wijziging drie keer consistent doorvoeren.

Zo pak je het aan:

  1. Start met een model zonder optionele properties en met lege strings als startwaarde.
  2. Schrijf één schema per type en mount het met apply() of applyEach().
  3. Houd één form() per scherm; geef child-componenten enkel een FieldTree.
  4. Gebruik hidden() in plaats van validators aan en uit te zetten.
  5. Zet dure async checks in een aparte rule function en mount ze enkel waar nodig.

Veelgestelde vragen over Angular Signal Forms

Wat is een schema in Angular Signal Forms? Een gewone, getypeerde functie die alle validatieregels voor een model bevat. Je mount ze met apply() op elke plek waar dezelfde shape voorkomt.

Wat is het verschil tussen applyWhen en applyWhenValue? applyWhen() activeert regels op basis van eender welke reactieve state. applyWhenValue() kijkt naar de waarde van het veld zelf en versmalt het type wanneer je een type guard gebruikt.

Waarom is valid() niet hetzelfde als !invalid()? Tijdens async validatie kunnen valid() en invalid() allebei false zijn, terwijl pending() true is.

Moet ik al mijn Reactive Forms vervangen door Signal Forms? Niet per se. De winst zit vooral bij formulieren die op meerdere schermen dezelfde vorm hebben. Voor kleine, eenmalige formulieren is het verschil beperkt.

Nood aan onze experts?

Wil je weten hoe onze Front End-consultants dit patroon toepassen in een bestaande codebase? Neem contact op met ons team.

Bekijk onze vacatures
Axxes