Architettura dei form in Vue 3 per app aziendali: pattern riutilizzabili
Architettura dei form in Vue 3 per app aziendali: componenti campo riutilizzabili, regole di validazione chiare e modi pratici per mostrare gli errori server su ciascun input.

Perché il codice dei form si rompe nelle app aziendali reali
Un form in un'app aziendale raramente resta piccolo. Inizia come "solo qualche input", poi cresce fino a dozzine di campi, sezioni condizionali, permessi e regole che devono rimanere allineati con la logica del backend. Dopo qualche modifica di prodotto, il form funziona ancora, ma il codice inizia a sembrare fragile.
L'architettura dei form in Vue 3 è importante perché è lì che si accumulano le "correzioni rapide": un watcher in più, un caso speciale, un componente copiato. Oggi funziona, ma diventa più difficile fidarsi e modificare il sistema.
I segnali d'allarme sono familiari: comportamento degli input ripetuto su più pagine (etichette, formattazione, marcatori di required, suggerimenti), posizionamento degli errori incoerente, regole di validazione sparse fra i componenti e errori backend ridotti a una toast generica che non dice all'utente cosa correggere.
Queste incoerenze non sono solo questioni di stile di codice. Diventano problemi di UX: gli utenti reinviano i form, aumentano i ticket di supporto e i team evitano di toccare i form perché qualcosa potrebbe rompersi in un caso limite nascosto.
Una buona configurazione rende i form noiosi nel senso migliore del termine. Con una struttura prevedibile puoi aggiungere campi, cambiare regole e gestire le risposte del server senza rifare tutto.
Vuoi un sistema di form che offra riuso (un campo si comporta uguale ovunque), chiarezza (regole e gestione degli errori facili da rivedere), comportamento prevedibile (touched, dirty, reset, submit) e feedback migliore (gli errori server compaiono sugli input esatti che richiedono attenzione). I pattern qui sotto si concentrano su componenti campo riutilizzabili, validazione leggibile e mappatura degli errori server ai singoli input.
Un semplice modello mentale per la struttura del form
Un form che regge nel tempo è un piccolo sistema con parti chiare, non un ammasso di input.
Pensa a quattro livelli che si parlano in una sola direzione: l'interfaccia raccoglie input, lo stato del form li conserva, la validazione spiega cosa non va e lo strato API carica/salva.
I quattro livelli (e cosa gestisce ciascuno)
- Componente campo UI: renderizza l'input, l'etichetta, il suggerimento e il testo d'errore. Emette i cambi di valore.
- Stato del form: contiene valori ed errori (più i flag touched e dirty).
- Regole di validazione: funzioni pure che leggono i valori e restituiscono messaggi d'errore.
- Chiamate API: caricano i dati iniziali, inviano le modifiche e traducono le risposte del server in errori di campo.
Questa separazione mantiene le modifiche contenute. Quando arriva un nuovo requisito, aggiorni uno strato senza rompere gli altri.
Cosa appartiene a un campo vs al form padre
Un componente campo riutilizzabile dovrebbe essere noioso. Non dovrebbe conoscere la tua API, il modello dei dati o le regole di validazione. Deve solo mostrare un valore e un errore.
Il form padre coordina tutto il resto: quali campi esistono, dove vivono i valori, quando validare e come inviare.
Una regola semplice aiuta: se la logica dipende da altri campi (ad esempio, "State" è richiesto solo quando "Country" è US), tienila nel form padre o nello strato di validazione, non dentro il componente campo.
Quando aggiungere un nuovo campo è davvero a basso sforzo, di solito tocchi solo i default o lo schema, il markup dove il campo è posizionato e le regole di validazione del campo. Se aggiungere un input forza cambiamenti in componenti non correlati, i confini sono sfocati.
Componenti campo riutilizzabili: cosa standardizzare
Quando i form crescono, il guadagno più veloce è smettere di costruire ogni input come se fosse unico. I componenti campo dovrebbero risultare prevedibili. Questo li rende veloci da usare e facili da rivedere.
Un set pratico di building block:
- BaseField: wrapper per etichetta, hint, testo d'errore, spaziatura e attributi di accessibilità.
- Componenti input: TextInput, SelectInput, DateInput, Checkbox, ecc. Ognuno si concentra sul controllo.
- FormSection: raggruppa campi correlati con un titolo, breve aiuto e spaziatura coerente.
Per le props, mantieni un set piccolo e applicalo ovunque. Cambiare il nome di una prop in 40 form è doloroso.
Questi pagano subito:
modelValueeupdate:modelValueperv-modellabelrequireddisablederror(messaggio singolo, o un array se preferisci)hint
Gli slot sono dove permetti flessibilità senza rompere la coerenza. Mantieni il layout di BaseField stabile, ma consenti piccole variazioni come un'azione sul lato destro ("Invia codice") o un'icona iniziale. Se una variazione ricorre due volte, falla diventare uno slot invece di creare un fork del componente.
Standardizza l'ordine di render (etichetta, controllo, hint, errore). Gli utenti scansionano più velocemente, i test diventano più semplici e la mappatura degli errori server diventa immediata perché ogni campo ha un posto ovvio dove mostrare i messaggi.
Stato del form: values, touched, dirty e reset
La maggior parte dei bug dei form nelle app aziendali non riguardano gli input. Nascono dallo stato sparso: valori in un posto, errori in un altro e un pulsante reset che funziona a metà. Un'architettura pulita per i form in Vue 3 parte da una forma di stato consistente.
Per prima cosa, scegli uno schema di naming per le chiavi dei campi e mantienilo. La regola più semplice è: la chiave del campo equivale alla chiave del payload API. Se il server si aspetta first_name, la chiave del form dovrebbe essere first_name anche tu. Questa piccola scelta semplifica validazione, salvataggio e mappatura degli errori server.
Tieni lo stato del form in un solo posto (una composable, uno store Pinia o il componente padre) e fai in modo che ogni campo legga e scriva attraverso quello stato. Una struttura piatta funziona per la maggior parte delle schermate. Vai a strutture nidificate solo quando la tua API è veramente nidificata.
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: '' }
})
Un modo pratico per pensare ai flag:
touched: l'utente ha interagito con questo campo?dirty: il valore è diverso dal default (o dall'ultimo valore salvato)?errors: quale messaggio dovrebbe vedere l'utente in questo momento?defaults: a cosa dobbiamo tornare con il reset?
Il comportamento di reset deve essere prevedibile. Quando carichi un record esistente, imposta sia values sia defaults dalla stessa fonte. Poi reset() può copiare defaults nuovamente in values, pulire touched, pulire dirty e pulire errors.
Esempio: un form profilo cliente carica email dal server. Se l'utente la modifica, dirty.email diventa true. Se clicca Reset, l'email torna al valore caricato (non a stringa vuota) e lo schermo torna pulito.
Regole di validazione che restano leggibili
Una validazione leggibile riguarda meno la libreria e più il modo in cui esprimi le regole. Se puoi dare uno sguardo a un campo e capire le regole in pochi secondi, il codice del form resta manutenibile.
Scegli uno stile di regole a cui attenerti
La maggior parte dei team si stabilizza su uno di questi approcci:
- Regole per campo: le regole stanno vicino all'uso del campo. Facile da scansionare, ottimo per form piccoli o medi.
- Regole basate su schema: le regole stanno in un oggetto o file unico. Ottimo quando molte schermate riutilizzano lo stesso modello.
- Ibrido: regole semplici vicino ai campi, regole condivise o complesse in uno schema centrale.
Qualunque tu scelga, mantieni nomi e messaggi prevedibili. Poche regole comuni (required, length, format, range) battono una lunga lista di helper ad hoc.
Scrivi regole come inglese semplice
Una buona regola si legge come una frase: "L'email è obbligatoria e deve avere formato email." Evita one-liner criptici che nascondono l'intento.
Per la maggior parte dei form aziendali, restituire un solo messaggio per campo alla volta (la prima fallitura) mantiene l'interfaccia calma e aiuta gli utenti a correggere più in fretta.
Regole comuni user-friendly:
- Required solo quando l'utente deve davvero compilare il campo.
- Length con numeri reali (per esempio, 2 a 50 caratteri).
- Format per email, telefono, CAP, senza regex troppo rigide che respingono input reali.
- Range come "data non nel futuro" o "quantità tra 1 e 999."
Rendi evidenti i controlli async
La validazione async (come "username già preso") diventa confusa se si attiva silenziosamente.
Attiva i controlli su blur o dopo una breve pausa, mostra uno stato chiaro "Verifico..." e annulla o ignora le richieste obsolete quando l'utente continua a digitare.
Decidi quando eseguire la validazione
Il timing conta tanto quanto le regole. Una configurazione user-friendly è:
- On change per campi che beneficiano di feedback live (come la forza della password), ma falla delicata.
- On blur per la maggior parte dei campi, così l'utente può digitare senza errori continui.
- On submit per l'intero form come rete di sicurezza finale.
Mappare gli errori server sull'input giusto
I controlli client sono solo metà della storia. Nelle app aziendali, il server rifiuta salvataggi per regole che il browser non può conoscere: duplicati, controlli di permesso, dati obsoleti, transizioni di stato e altro. Una buona UX dipende dal trasformare quella risposta in messaggi chiari accanto agli input giusti.
Normalizza gli errori in una sola forma interna
I backend raramente si mettono d'accordo sul formato degli errori. Alcuni restituiscono un oggetto singolo, altri liste, altri mappe nidificate indicizzate per nome campo. Converti qualsiasi cosa arrivi in una forma interna unica che il form possa renderizzare.
// what your form code consumes
{
fieldErrors: { "email": ["Already taken"], "address.street": ["Required"] },
formErrors: ["You do not have permission to edit this customer"]
}
Mantieni poche regole costanti:
- Memorizza gli errori di campo come array (anche se c'è un solo messaggio).
- Converti diversi stili di path in uno solo (i path con notazione a punti funzionano bene:
address.street). - Tieni gli errori non di campo separati come
formErrors. - Conserva il payload raw del server per logging, ma non renderizzarlo.
Mappa i path del server alle chiavi del tuo form
La parte difficile è allineare l'idea di "path" del server con le chiavi dei campi del tuo form. Decidi la chiave per ogni componente campo (per esempio email, profile.phone, contacts.0.type) e mantienila.
Poi scrivi un piccolo mapper che gestisca i casi comuni:
address.street(notazione a punti)address[0].street(parentesi per array)/address/street(stile JSON Pointer)
Dopo la normalizzazione, <Field name="address.street" /> dovrebbe poter leggere fieldErrors["address.street"] senza casi speciali.
Supporta alias quando necessario. Se il backend restituisce customer_email ma la tua UI usa email, tieni una mappatura come { customer_email: "email" } durante la normalizzazione.
Errori di campo, errori di form e messa a fuoco
Non tutti gli errori appartengono a un singolo input. Se il server dice "Limite piano raggiunto" o "Pagamento richiesto", mostralo sopra il form come messaggio a livello di form.
Per gli errori specifici dei campi, mostra il messaggio accanto all'input e guida l'utente verso il primo problema:
- Dopo aver impostato gli errori server, trova la prima chiave in
fieldErrorsche esiste nel form renderizzato. - Scrollala in vista e portaci il focus (usando un ref per campo e
nextTick). - Pulisci gli errori server di un campo quando l'utente modifica di nuovo quel campo.
Passo dopo passo: mettere insieme l'architettura
I form restano calmi quando decidi presto cosa appartiene allo stato del form, all'UI, alla validazione e all'API, poi li colleghi con poche funzioni semplici.
Una sequenza che funziona per la maggior parte delle app aziendali:
- Inizia con un modello di form unico e chiavi campo stabili. Quelle chiavi diventano il contratto tra componenti, validator e errori server.
- Crea un wrapper BaseField per etichetta, testo di aiuto, marcatore required e visualizzazione errore. Mantieni piccoli e coerenti i componenti input.
- Aggiungi uno strato di validazione che possa eseguire la validazione per campo e validare tutto su submit.
- Invia all'API. Se fallisce, traduci gli errori server in
{ [fieldKey]: message }così l'input giusto mostra il messaggio giusto. - Tieni la gestione del successo separata (reset, toast, navigazione) in modo che non si riverberi in componenti e validator.
Un punto di partenza semplice per lo stato:
const values = reactive({ email: '', name: '', phone: '' })
const touched = reactive({ email: false, name: false, phone: false })
const errors = reactive({}) // { email: '...', name: '...' }
Il tuo BaseField riceve label, error e magari touched, e renderizza il messaggio in un unico posto. Ogni componente input si preoccupa solo di bindare e emettere aggiornamenti.
Per la validazione, tieni le regole vicino al modello usando le stesse chiavi:
const rules = {
email: v => (!v ? 'Email è obbligatoria' : /@/.test(v) ? '' : 'Inserisci una email valida'),
name: v => (v.length < 2 ? 'Il nome è troppo corto' : ''),
}
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
}
Quando il server risponde con errori, mappali usando le stesse chiavi. Se l'API restituisce { "field": "email", "message": "Already taken" }, imposta errors.email = 'Already taken' e segnalo come touched. Se l'errore è globale (per esempio "permesso negato"), mostralo sopra il form.
Scenario esempio: modifica del profilo cliente
Immagina una schermata interna dove un addetto al supporto modifica il profilo cliente. Il form ha quattro campi: name, email, phone e role (Customer, Manager, Admin). È piccolo, ma mostra i problemi comuni.
Le regole client dovrebbero essere chiare:
- Name: obbligatorio, lunghezza minima.
- Email: obbligatoria, formato email valido.
- Phone: opzionale, ma se compilato deve rispettare il formato accettato.
- Role: obbligatorio, e a volte condizionale (solo gli utenti con i permessi giusti possono assegnare Admin).
Un contratto di componente coerente aiuta: ogni campo riceve il valore corrente, il testo d'errore corrente (se presente) e un paio di booleani come touched e disabled. Etichette, marcatori required, spaziature e stile degli errori non devono essere reinventati su ogni schermata.
Ora il flusso UX. L'addetto modifica l'email, fa tab out e vede un messaggio inline sotto Email se il formato è sbagliato. Lo corregge, clicca Salva e il server risponde:
- email già esistente: mostralo sotto Email e porta il focus su quel campo.
- phone non valido: mostralo sotto Phone.
- permesso negato: mostra un messaggio a livello di form in cima.
Se tieni gli errori indicizzati per nome campo (email, phone, role), la mappatura è semplice. Gli errori di campo finiscono accanto agli input, gli errori di form finiscono in un'area dedicata.
Errori comuni e come evitarli
Tieni la logica in un posto solo
Copiare regole di validazione in ogni schermata sembra veloce finché le politiche cambiano (regole password, ID fiscali richiesti, domini ammessi). Centralizza le regole (schema, file di regole, funzione condivisa) e fai consumare ai form lo stesso set di regole.
Evita anche di far fare troppo ai componenti low-level. Se il tuo <TextField> sa chiamare l'API, ritentare sui fallimenti e parsare payload di errori server, smette di essere riutilizzabile. I componenti campo devono renderizzare, emettere cambi di valore e mostrare errori. Metti le chiamate API e la logica di mappatura nel container del form o in una composable.
Sintomi che stai mescolando i concern:
- Lo stesso messaggio di validazione è scritto in più posti.
- Un componente campo importa un client API.
- Cambiare un endpoint rompe diversi form non correlati.
- I test richiedono di montare metà app solo per verificare un input.
Trappole UX e accessibilità
Un singolo banner d'errore come "Qualcosa è andato storto" non basta. Le persone hanno bisogno di sapere quale campo è sbagliato e cosa fare dopo. Usa i banner per fallimenti globali (rete giù, permessi negati) e mappa gli errori server ai singoli input così gli utenti possono agire rapidamente.
Problemi di caricamento e double-submit creano stati confusi. Durante l'invio, disabilita il submit, disabilita i campi che non devono cambiare durante il salvataggio e mostra uno stato di busy chiaro. Assicurati che reset e annulla ripristinino il form in modo pulito.
Le basi dell'accessibilità sono facili da saltare con componenti personalizzati. Alcune scelte prevengono problemi reali:
- Ogni input ha un'etichetta visibile (non solo placeholder).
- Gli errori sono collegati ai campi con gli attributi aria corretti.
- Il focus si sposta sul primo campo invalido dopo il submit.
- I campi disabilitati sono veramente non interattivi e annunciati correttamente.
- La navigazione da tastiera funziona end to end.
Checklist rapida e passi successivi
Prima di mettere in produzione un nuovo form, fai una checklist veloce. Cattura i piccoli gap che poi diventano ticket di supporto.
- Ogni campo ha una chiave stabile che corrisponde al payload e alla risposta server (inclusi path nidificati come
billing.address.zip)? - Puoi renderizzare qualsiasi campo usando un'API coerente del componente campo (valore in, eventi out, errore e hint in)?
- Al submit, validi una volta, blocchi double submit e porti il focus sul primo campo invalido così l'utente sa da dove iniziare?
- Puoi mostrare gli errori nel posto giusto: per campo (accanto all'input) e a livello di form (messaggio generale quando necessario)?
- Dopo il successo, resetti lo stato correttamente (values, touched, dirty) così la prossima modifica parte pulita?
Se una risposta è "no", correggi quella prima. Il problema più comune dei form è lo scollamento: i nomi dei campi si allontanano dall'API o gli errori server tornano in una forma che la UI non sa piazzare.
Se stai costruendo strumenti interni e vuoi andare più veloce, AppMaster (appmaster.io) segue gli stessi fondamenti: mantieni la UI dei campi coerente, centralizza regole e workflow e fai comparire le risposte server dove gli utenti possono agire su di esse.
FAQ
Standardizzali quando noti le stesse etichette, suggerimenti, marcatori di required, spaziature e stili di errore ripetuti su più pagine. Se una piccola modifica ti costringe a editare molti file, un wrapper condiviso BaseField e pochi componenti di input coerenti fanno risparmiare tempo subito.
Mantieni il componente campo 'dumb': renderizza etichetta, controllo, hint ed errore, ed emette aggiornamenti di valore. Metti la logica cross-field, le regole condizionali e tutto ciò che dipende da altri valori nel form padre o nello strato di validazione, così il campo resta riutilizzabile.
Usa chiavi stabili che corrispondono per default al payload dell'API, come first_name o billing.address.zip. Questo semplifica validazione e mappatura degli errori server perché non devi tradurre continuamente i nomi tra gli strati.
Un default semplice è un oggetto di stato che contiene values, errors, touched, dirty e defaults. Quando tutto legge e scrive attraverso la stessa struttura, reset e submit diventano prevedibili ed eviti bug da “reset parziale”.
Imposta sia values sia defaults dai dati caricati. Poi reset() deve copiare defaults in values e pulire touched, dirty ed errors, così l'interfaccia torna pulita e corrisponde a ciò che il server ha restituito l'ultima volta.
Inizia con regole semplici come funzioni indicizzate dalle stesse chiavi dei campi nel tuo stato form. Restituisci un solo messaggio chiaro per campo (prima fallitura) così l'interfaccia resta calma e gli utenti sanno cosa correggere.
Valida la maggior parte dei campi su blur, poi valida tutto su submit come controllo finale. Usa la validazione on-change solo dove aiuta davvero (per esempio la forza della password), così gli utenti non vengono puniti con errori mentre scrivono.
Esegui i controlli async su blur o dopo un breve debounce e mostra uno stato esplicito “verifico...” (o simile). Cancella o ignora le richieste obsolete in modo che risposte lente non sovrascrivano input più recenti e non generino errori confusi.
Normalizza ogni formato backend in una forma interna come { fieldErrors: { key: [messaggi] }, formErrors: [messaggi] }. Usa uno stile unico per i path (la notazione a punti funziona bene) così un campo address.street può sempre leggere fieldErrors['address.street'] senza eccezioni.
Mostra gli errori a livello di form sopra il form e gli errori di campo accanto all'input preciso. Dopo un submit fallito, porta il focus sul primo campo con errore e cancella l'errore server di quel campo non appena l'utente lo modifica di nuovo.


