Vue 3 formulierarchitectuur voor bedrijfsapps: herbruikbare patronen
Vue 3 formulierarchitectuur voor bedrijfsapps: herbruikbare veldcomponenten, duidelijke validatieregels en praktische manieren om serverfouten bij het juiste invoerveld te tonen.

Waarom formuliercode faalt in echte bedrijfsapps
Een formulier in een bedrijfsapp blijft zelden klein. Het begint als "slechts een paar velden" en groeit dan naar tientallen velden, voorwaardelijke secties, permissies en regels die synchroon moeten blijven met backendlogica. Na een paar productwijzigingen werkt het formulier nog wel, maar voelt de code fragiel aan.
Vue 3 formulierarchitectuur is belangrijk omdat formulieren de plek zijn waar "quick fixes" zich opstapelen: nog één watcher, nog één speciale case, nog één gekopieerde component. Het werkt vandaag, maar het wordt lastiger om te vertrouwen en moeilijker om te veranderen.
De waarschuwingssignalen zijn vertrouwd: gedrag van invoeren wordt herhaald over pagina's heen (labels, formattering, verplichte markeringen, hints), inconsistente foutplaatsing, validatieregels verspreid over componenten, en backendfouten die gereduceerd worden tot een generieke toast die gebruikers niet vertelt wat ze moeten aanpassen.
Die inconsistenties zijn niet alleen stijlkwesties in code. Ze worden UX-problemen: mensen verzenden formulieren opnieuw, supporttickets nemen toe en teams vermijden formulieren omdat iets in een verborgen edge-case kapot kan gaan.
Een goede setup maakt formulieren saai op de beste manier. Met een voorspelbare structuur kun je velden toevoegen, regels wijzigen en serverresponses afhandelen zonder alles te herbedraden.
Je wilt een formsysteem dat hergebruik biedt (een veld gedraagt zich overal hetzelfde), helderheid (regels en foutafhandeling zijn makkelijk te overzien), voorspelbaar gedrag (aangeraakt, gewijzigd, reset, verzenden) en betere feedback (server-side fouten verschijnen bij precies de invoer die aandacht nodig heeft). De patronen hieronder richten zich op herbruikbare veldcomponenten, leesbare validatie en het mappen van serverfouten terug naar specifieke invoeren.
Een simpel mentaal model voor formulierstructuur
Een formulier dat over tijd standhoudt is een klein systeem met duidelijke onderdelen, niet een stapel losse velden.
Denk aan vier lagen die één richting met elkaar praten: de UI verzamelt invoer, form state slaat het op, validatie legt uit wat er mis is, en de API-laag laadt en slaat op.
De vier lagen (en wat elke laag beheert)
- Field UI component: rendert de invoer, label, hint en fouttekst. Emit value changes.
- Form state: houdt waarden en fouten bij (plus
aangeraaktengewijzigdflags). - Validatieregels: pure functies die waarden lezen en foutmeldingen teruggeven.
- API-calls: laden van initiële data, verzenden van wijzigingen en vertalen van serverresponses naar veldfouten.
Deze scheiding houdt veranderingen bevat. Wanneer er een nieuwe vereiste komt, werk je meestal één laag bij zonder de anderen kapot te maken.
Wat hoort in een veld en wat in het ouderformulier
Een herbruikbare veldcomponent moet saai zijn. Hij zou niets moeten weten over je API, datamodel of validatieregels. Hij zou alleen een waarde tonen en een fout weergeven.
Het ouderformulier coördineert de rest: welke velden er zijn, waar waarden leven, wanneer te valideren en hoe te submitten.
Een simpele vuistregel helpt: als logica afhankelijk is van andere velden (bijvoorbeeld: "State" is verplicht alleen wanneer "Country" US is), houd die logica in het ouderformulier of in de validatielaag, niet binnen de veldcomponent.
Als het toevoegen van een nieuw veld echt weinig werk is, wijzig je meestal alleen de defaults of het schema, de markup waar het veld geplaatst wordt en de validatieregels van het veld. Als het toevoegen van één invoer veranderingen in ongerelateerde componenten afdwingt, zijn je grenzen vaag.
Herbruikbare veldcomponenten: wat te standaardiseren
Wanneer formulieren groeien is de snelste winst om te stoppen met elk invoerveld als een one-off te bouwen. Veldcomponenten moeten voorspelbaar aanvoelen. Dat maakt ze snel in gebruik en makkelijk te reviewen.
Een praktisch set bouwstenen:
- BaseField: wrapper voor label, hint, fouttekst, spacing en toegankelijkheidsattributen.
- Inputcomponenten: TextInput, SelectInput, DateInput, Checkbox, enz. Elke component concentreert zich op de control.
- FormSection: groepeert gerelateerde velden met een titel, korte helptekst en consistente spacing.
Voor props: houd een kleine set en gebruik die overal. Een propnaam wijzigen over 40 formulieren is pijnlijk.
Deze betalen zich meestal direct terug:
modelValueenupdate:modelValuevoorv-modellabelrequireddisablederror(enkele boodschap, of een array als je dat prefereert)hint
Slots zijn waar je flexibiliteit toestaat zonder consistentie te breken. Houd de BaseField-layout stabiel, maar laat kleine variaties toe zoals een actie aan de rechterzijde ("Verstuur code") of een icoon aan de voorkant. Als een variatie twee keer voorkomt, maak er een slot van in plaats van de component te forken.
Standaardiseer de render-volgorde (label, control, hint, error). Gebruikers scannen sneller, tests worden eenvoudiger en serverfoutmapping wordt overzichtelijk omdat elk veld één duidelijke plek heeft om berichten te tonen.
Form state: values, touched, dirty en reset
De meeste form-bugs in bedrijfsapps gaan niet over inputs. Ze komen door verspreide state: waarden op één plek, fouten op een andere, en een resetknop die maar half werkt. Een schone Vue 3 formulierarchitectuur begint met één consistente state-shape.
Kies eerst een naamgevingsschema voor veldkeys en houd je eraan. De simpelste regel is: de veldkey is gelijk aan de API payload key. Als je server first_name verwacht, moet je formulierkey ook first_name zijn. Deze kleine keuze maakt validatie, opslaan en serverfoutmapping veel eenvoudiger.
Houd je formulierstate op één plek (een composable, een Pinia-store of een parentcomponent), en laat elk veld daar lezen en schrijven. Een platte structuur werkt voor de meeste schermen. Ga pas genest wanneer je API echt genest is.
const state = reactive({
values: { first_name: '', last_name: '', email: '' },
touched: { first_name: false, last_name: false, email: false },
dirty: { first_name: false, last_name: false, email: false },
errors: { first_name: '', last_name: '', email: '' },
defaults: { first_name: '', last_name: '', email: '' }
})
Een praktische manier om over de flags na te denken:
aangeraakt: heeft de gebruiker met dit veld interactie gehad?gewijzigd: is de waarde verschillend van de default (of laatst opgeslagen) waarde?errors: welke boodschap moet de gebruiker nu zien?defaults: waar zetten we op terug?
Resetgedrag moet voorspelbaar zijn. Wanneer je een bestaand record laadt, zet zowel values als defaults vanuit dezelfde bron. Dan kan reset() defaults terugkopiëren naar values, touched wissen, dirty wissen en errors wissen.
Voorbeeld: een klantprofielformulier laadt email van de server. Als de gebruiker het aanpast, wordt dirty.email true. Als ze Reset klikken, gaat het e-mailadres terug naar de geladen waarde (niet een lege string), en ziet het scherm er weer schoon uit.
Validatieregels die leesbaar blijven
Leesbare validatie draait minder om de library en meer om hoe je regels formuleert. Als je in één oogopslag de regels van een veld begrijpt, blijft je formuliercode onderhoudbaar.
Kies een regelsstijl waar je bij blijft
De meeste teams kiezen één van deze benaderingen:
- Per-veld regels: regels leven dichtbij het veldgebruik. Makkelijk te scannen, goed voor kleine tot middelgrote formulieren.
- Schema-gebaseerde regels: regels leven in één object of bestand. Handig wanneer veel schermen hetzelfde model hergebruiken.
- Hybride: eenvoudige regels bij de velden, gedeelde of complexe regels in een centraal schema.
Wat je ook kiest, houd regelnamen en -berichten voorspelbaar. Een paar veelgebruikte regels (required, length, format, range) verslaan een lange lijst van éénmalige helpers.
Schrijf regels als gewoon Nederlands
Een goede regel leest als een zin: "E-mail is verplicht en moet op een e-mail lijken." Vermijd slimme one-liners die intentie verbergen.
Voor de meeste bedrijfsformulieren helpt het om één bericht per veld terug te geven (de eerste fout). Dat houdt de UI rustig en helpt gebruikers sneller te herstellen.
Veelgebruikte regels die gebruikersvriendelijk blijven:
- Required alleen wanneer het veld echt ingevuld moet worden.
- Length met reële nummers (bijv. 2 tot 50 tekens).
- Format voor e-mail, telefoon, postcode, zonder overdreven strikte regex die echte invoer afwijst.
- Range zoals "datum niet in de toekomst" of "aantal tussen 1 en 999."
Maak async-checks zichtbaar
Async-validatie (zoals "gebruikersnaam al in gebruik") wordt verwarrend als het stil gebeurt.
Trigger checks op blur of na een korte pauze, toon een duidelijke "Controleren..."-status en annuleer of negeer verouderde requests wanneer de gebruiker blijft typen.
Bepaal wanneer validatie draait
Timing is net zo belangrijk als regels. Een gebruiksvriendelijke setup is:
- On change voor velden die baat hebben bij live feedback (zoals wachtwoordsterkte), maar houd het mild.
- On blur voor de meeste velden, zodat gebruikers kunnen typen zonder constant fouten te krijgen.
- On submit voor het volledige formulier als laatste vangnet.
Serverfouten mappen op het juiste invoerveld
Client-side checks zijn maar de helft van het verhaal. In bedrijfsapps wijst de server saves af voor regels die de browser niet kan weten: duplicaten, permissiechecks, verouderde data, statuswijzigingen en meer. Goede form-UX hangt af van het omzetten van die response naar duidelijke berichten naast de juiste invoeren.
Normaliseer fouten naar één interne vorm
Backends zijn zelden eensgezind in foutformaten. Sommige geven een enkel object terug, andere lijsten, weer anderen geneste mappen met veldnamen als keys. Zet alles om naar één interne vorm die je formulier kan renderen.
// wat je formcode consumeert
{
fieldErrors: { "email": ["Already taken"], "address.street": ["Required"] },
formErrors: ["You do not have permission to edit this customer"]
}
Houd enkele regels consistent:
- Sla veldfouten op als arrays (zelfs als er maar één bericht is).
- Converteer verschillende padstijlen naar één stijl (dot-paths werken goed:
address.street). - Houd niet-veld-fouten apart als
formErrors. - Bewaar de ruwe serverpayload voor logging, maar render die niet.
Map serverpaden naar jouw veldkeys
Het lastige is het afstemmen van het serveridee van een "pad" op de veldkeys in jouw formulier. Bepaal de key voor elk veldcomponent (bijv. email, profile.phone, contacts.0.type) en houd je daaraan.
Schrijf daarna een kleine mapper die de veelvoorkomende gevallen afhandelt:
address.street(dot-notatie)address[0].street(brackets voor arrays)/address/street(JSON Pointer-stijl)
Na normalisatie moet \u003cField name="address.street" /\u003e in staat zijn om fieldErrors["address.street"] te lezen zonder speciale gevallen.
Ondersteun aliassen waar nodig. Als de backend customer_email terugstuurt maar je UI email gebruikt, houd dan een mapping zoals { customer_email: "email" } tijdens normalisatie.
Veldfouten, formulierfouten en focus
Niet elke fout hoort bij één invoer. Als de server zegt "Plangrens bereikt" of "Betaling vereist", toon dat boven het formulier als een formulierniveau-bericht.
Voor veldspecifieke fouten toon je de boodschap naast het invoerveld en leid je de gebruiker naar het eerste probleem:
- Nadat serverfouten gezet zijn, vind je de eerste key in
fieldErrorsdie in je gerenderde formulier voorkomt. - Scroll er naartoe en focus het (gebruik een ref per veld en
nextTick). - Wis serverfouten voor een veld wanneer de gebruiker dat veld weer bewerkt.
Stap voor stap: de architectuur samenvoegen
Formulieren blijven rustig wanneer je vroeg beslist wat bij form state, UI, validatie en API hoort, en ze daarna met een paar kleine functies verbindt.
Een volgorde die voor de meeste bedrijfsapps werkt:
- Begin met één formmodel en stabiele veldkeys. Die keys worden het contract tussen componenten, validators en serverfouten.
- Maak één BaseField-wrapper voor label, helptekst, verplichte markering en foutweergave. Houd inputcomponenten klein en consistent.
- Voeg een validatielaag toe die per veld kan draaien en alles op submit kan valideren.
- Submit naar de API. Als het faalt, vertaal serverfouten naar
{ [fieldKey]: message }zodat het juiste invoerveld de juiste boodschap toont. - Houd success-afhandeling gescheiden (reset, toast, navigatie) zodat het niet in componenten en validators lekt.
Een simpel startpunt voor state:
const values = reactive({ email: '', name: '', phone: '' })
const touched = reactive({ email: false, name: false, phone: false })
const errors = reactive({}) // { email: '...', name: '...' }
Je BaseField krijgt label, error en misschien touched, en rendert het bericht op één plek. Elke inputcomponent houdt zich alleen bezig met binden en emitten van updates.
Voor validatie, houd regels bij het model met dezelfde keys:
const rules = {
email: v => (!v ? 'Email is required' : /@/.test(v) ? '' : 'Enter a valid email'),
name: v => (v.length < 2 ? 'Name is too short' : ''),
}
function validateAll() {
Object.keys(rules).forEach(k => {
const msg = rules[k](values[k])
if (msg) errors[k] = msg
else delete errors[k]
touched[k] = true
})
return Object.keys(errors).length === 0
}
Wanneer de server met fouten terugkomt, map je ze met dezelfde keys. Als de API { "field": "email", "message": "Already taken" } terugstuurt, zet errors.email = 'Already taken' en markeer het als touched. Als de fout globaal is (zoals "permission denied"), toon het boven het formulier.
Voorbeeldscenario: een klantprofiel bewerken
Stel je een intern adminscherm voor waar een supportmedewerker een klantprofiel bewerkt. Het formulier heeft vier velden: name, email, phone en role (Customer, Manager, Admin). Het is klein, maar het laat veel voorkomende issues zien.
Client-side regels moeten duidelijk zijn:
- Name: verplicht, minimale lengte.
- Email: verplicht, geldig e-mailformaat.
- Phone: optioneel, maar als ingevuld moet het aan je geaccepteerde formaat voldoen.
- Role: verplicht, en soms conditioneel (alleen gebruikers met de juiste permissies kunnen Admin toewijzen).
Een consistente componentcontract helpt: elk veld ontvangt de huidige waarde, de huidige fouttekst (indien aanwezig) en een paar booleans zoals touched en disabled. Labels, verplichte markeringen, spacing en foutstyling moeten niet op elk scherm opnieuw uitgevonden worden.
Nu de UX-flow. De agent past het e-mailadres aan, tabt weg en ziet onder Email een inline bericht als het formaat onjuist is. Ze corrigeren het, klikken op Opslaan en de server reageert:
- email already exists: toon het onder Email en focus dat veld.
- phone invalid: toon het onder Phone.
- permission denied: toon één formulierniveau-bericht bovenaan.
Als je fouten bij veldnaam sleutel (email, phone, role) houdt, is mapping simpel. Veldfouten landen naast invoeren, formulierfouten in een eigen gebied.
Veelgemaakte fouten en hoe ze te vermijden
Houd logica op één plek
Kopieer van validatieregels naar elk scherm voelt snel, totdat beleid verandert (wachtwoordregels, verplichte belasting-ID's, toegestane domeinen). Houd regels gecentraliseerd (schema, rules-file, gedeelde functie) en laat formulieren dezelfde regels consumeren.
Vermijd ook dat low-level inputs te veel doen. Als je <TextField> de API kan aanroepen, fouten opnieuw probeert en serverpayloads kan parsen, stopt het met herbruikbaar zijn. Veldcomponenten moeten renderen, value-changes emitten en fouten tonen. Zet API-calls en mappinglogica in de form-container of een composable.
Symptomen dat je zorgen hebt over gemengde verantwoordelijkheden:
- Dezelfde validatieboodschap staat op meerdere plaatsen.
- Een veldcomponent importeert een API-client.
- Het wijzigen van één endpoint breekt meerdere ongerelateerde formulieren.
- Tests vereisen het mounten van half de app om één input te controleren.
UX- en toegankelijkheid-valkuilen
Een enkele foutbanner zoals "Er is iets misgegaan" is niet genoeg. Mensen moeten weten welk veld fout is en wat ze moeten doen. Gebruik banners voor globale fouten (netwerkfouten, permissies), en map serverfouten naar specifieke velden zodat gebruikers ze snel kunnen oplossen.
Laad- en double-submit-issues creëren verwarrende toestanden. Tijdens submitten: disable submit, disable velden die niet tijdens het opslaan mogen veranderen en toon een duidelijke busy-status. Zorg dat reset en cancel het formulier netjes herstellen.
Toegankelijkheidsbasics zijn makkelijk over te slaan met custom componenten. Een paar keuzes voorkomen echte ellende:
- Elke input heeft een zichtbaar label (niet alleen placeholder).
- Fouten zijn gekoppeld aan velden met de juiste aria-attributen.
- Focus gaat naar het eerste ongeldige veld na submit.
- Uitgeschakelde velden zijn echt niet-interactief en worden correct aangekondigd.
- Keyboardnavigatie werkt volledig.
Snelle checklist en vervolgstappen
Voordat je een nieuw formulier uitrolt, doorloop een korte checklist. Die vangt de kleine gaten die later supporttickets veroorzaken.
- Heeft elk veld een stabiele key die overeenkomt met de payload en serverresponse (inclusief geneste paden zoals
billing.address.zip)? - Kun je elk veld renderen met één consistente fieldcomponent-API (waarde in, events uit, error en hint in)?
- Valideer je bij submit één keer, blokkeer je dubbele submits en focus je het eerste ongeldige veld zodat gebruikers weten waar te beginnen?
- Kun je fouten op de juiste plek tonen: per veld (naast de input) en op formulierniveau (algemene boodschap indien nodig)?
- Reset je na succes de state correct (values, touched, dirty) zodat de volgende bewerking schoon begint?
Als één antwoord "nee" is, los dat eerst op. De meest voorkomende pijn is mismatch: veldnamen drijven weg van de API, of serverfouten komen terug in een vorm die je UI niet kan plaatsen.
Als je interne tools bouwt en sneller wilt bewegen, volgt AppMaster (appmaster.io) dezelfde fundamenten: houd veld-UI consistent, centraliseer regels en workflows, en laat serverresponses daar verschijnen waar gebruikers er iets mee kunnen doen.
FAQ
Standaardiseer wanneer je hetzelfde label, hint, verplicht-markering, afstand en foutstyling over meerdere pagina’s herhaald ziet. Als één “kleine” wijziging betekent dat je veel bestanden moet aanpassen, bespaar je veel tijd met een gedeelde BaseField-wrapper en een paar consistente invoercomponenten.
Houd de veldcomponent dumb: render de label, control, hint en fout en emit value-updates. Zet cross-field-logica, conditionele regels en alles wat afhankelijk is van andere waarden in het parent-formulier of een validatielaag, zodat het veld herbruikbaar blijft.
Gebruik stabiele sleutels die standaard overeenkomen met je API-payload, zoals first_name of billing.address.zip. Dat maakt validatie en serverfout-mapping eenvoudig omdat je niet constant namen tussen lagen hoeft te vertalen.
Een eenvoudige standaard is één state-object dat values, errors, touched, dirty en defaults bevat. Wanneer alles via dezelfde vorm gelezen en geschreven wordt, wordt reset- en submitgedrag voorspelbaar en voorkom je halve reset-bugs.
Zet zowel values als defaults vanuit dezelfde geladen data. Laat reset() vervolgens defaults terugkopiëren naar values en clear touched, dirty en errors zodat de UI er schoon uitziet en overeenkomt met wat de server het laatst teruggaf.
Begin met regels als simpele functies die geordend zijn op dezelfde veldnamen als je form-state. Geef per veld één duidelijke boodschap terug (de eerste fout) zodat de UI rustig blijft en gebruikers weten wat ze moeten oplossen.
Valideer de meeste velden op blur, en valideer alles op submit als de laatste controle. Gebruik on-change validatie alleen waar het echt helpt (zoals wachtwoordsterkte) zodat gebruikers niet tijdens het typen afgestraft worden met foutmeldingen.
Voer async-checks op blur uit of na een korte debounce, en toon een expliciete “controleren…”-status. Annuleer of negeer verouderde requests zodat trage responses geen nieuwere invoer overschrijven en verwarrende fouten veroorzaken.
Normaliseer elk backend-formaat naar één interne vorm zoals { fieldErrors: { key: [messages] }, formErrors: [messages] }. Gebruik één padstijl (dot-notatie werkt goed) zodat een veld genaamd address.street altijd fieldErrors['address.street'] kan lezen zonder speciale gevallen.
Toon algemene formulierfouten boven het formulier, maar plaats veldfouten naast het specifieke invoerveld. Na een mislukte submit focus je het eerste veld met een fout en verwijder je die serverfout zodra de gebruiker het veld weer aanpast.


