Schema builders
{ sections: [...] } è la forma raccomandata: è il formato canonico (lo stesso che ricevi da un backend, che salvi in un file JSON, che leggi da un editor visuale), è diff-friendly nelle code review, non richiede import aggiuntivi e funziona identico a runtime. I builder fluenti documentati in questa pagina sono un'alternativa opzionale, utile in casi specifici elencati nella sezione Quando usare i builder — non sono il default.Perché esistono
Lo schema di DynamicFormLayouts è una struttura sections > rows > fields annidata. La forma canonica è l'oggetto literal: è il formato in cui lo schema viaggia su backend, file JSON ed editor visuali. Scrivere lo schema come literal è la scelta predefinita.
I builder esistono per ridurre il boilerplate in scenari specifici (factorizzazione di campi base, varianti riusabili, select/multiselect/listbox/radio ripetuti con la stessa shape props.options). Restituiscono lo stesso oggetto literal con tipi corretti.
// Equivalente — entrambe forme producono lo stesso oggetto
import { field, row, section, form } from '@pzeta/vue-form'
// Forma builder
const schemaA = form([
section([
row([
field.required(field.text('codice', 'Codice', { span: 6 })),
field.required(field.text('nome', 'Nome', { span: 6 })),
]),
], { title: 'Identificativi', icon: 'pi pi-tag' }),
])
// Forma oggetto literal equivalente
const schemaB = {
sections: [
{
title: 'Identificativi',
icon: 'pi pi-tag',
rows: [
{
fields: [
{ name: 'codice', type: 'text', label: 'Codice', span: 6, required: true, validation: 'required' },
{ name: 'nome', type: 'text', label: 'Nome', span: 6, required: true, validation: 'required' },
],
},
],
},
],
}
Field helpers
field espone una factory per ogni InputType. Tutti accettano la stessa firma di base:
field.text(name, label?, opts?)
| Parametro | Tipo | Descrizione |
|---|---|---|
name | string | Chiave nel formData |
label | string (opzionale) | Etichetta visibile |
opts | FieldHelperOptions (opzionale) | Tutto ciò che serve oltre name/type/label |
FieldHelperOptions = Omit<FieldDefinition, 'name' | 'type' | 'label'> — quindi validation, required, span, props, defaultValue, disabled, visible, class, exclude, ecc.
Helpers semplici
field.text('nome', 'Nome', { validation: 'required|min:2', span: 6 })
field.email('email', 'Email', { validation: 'required|email' })
field.password('password', 'Password', { validation: 'required|password' })
field.textarea('note', 'Note', { span: 12, props: { rows: 4 } })
field.number('eta', 'Età', { validation: 'numeric|positive' })
field.date('datacreazione', 'Data creazione')
field.color('colore', 'Colore')
field.rating('voto', 'Voto')
field.slider('priorita', 'Priorità')
field.switch('attivo', 'Attivo')
field.toggle('confermato', 'Confermato')
field.checkbox('accettoTermini', 'Accetto i termini')
field.autocomplete('citta', 'Città')
field.mask('telefono', 'Telefono', { props: { mask: '+39 999 9999999' } })
field.treeselect('categoria', 'Categoria')
field.tiptap('descrizione', 'Descrizione')
field.icon('icona', 'Icona')
field.signature('firma', 'Firma')
Helpers con opzioni inline
select, multiselect, listbox e radio iniettano automaticamente le options nei props:
field.select('paese', 'Paese',
[{ label: 'Italia', value: 'IT' }, { label: 'Francia', value: 'FR' }],
{ required: true, props: { optionLabel: 'label', optionValue: 'value' } }
)
field.multiselect('lingue', 'Lingue parlate',
[
{ label: 'Italiano', value: 'it' },
{ label: 'English', value: 'en' },
{ label: 'Français', value: 'fr' },
],
{ props: { optionLabel: 'label', optionValue: 'value' } }
)
field.listbox('preferenza', 'Preferenza',
['Telefono', 'Email', 'SMS']
)
field.radio('genere', 'Genere',
[{ label: 'Maschio', value: 'M' }, { label: 'Femmina', value: 'F' }],
{ props: { optionLabel: 'label', optionValue: 'value' } }
)
Hidden field
field.hidden('idanagrafica', 42) // valore di default
field.hidden('source') // solo nome, nessun default
Modificatori
Gli helper field.required / field.disabled / field.visible trasformano un FieldDefinition esistente — utili per applicare condizioni in cascata o per riusare campi base con varianti:
const baseEmail = field.text('email', 'Email', { validation: 'email' })
// Aggiunge `required: true` e prepende 'required|' al validation esistente
const requiredEmail = field.required(baseEmail)
// → { name: 'email', type: 'text', label: 'Email', validation: 'required|email', required: true }
// Disabilita condizionalmente
const conditionalEmail = field.disabled(baseEmail, {
field: 'tipoCliente', operator: '==', value: 'anonimo',
})
// Visibile solo se condizione vera
const azionalEmail = field.visible(requiredEmail, {
field: 'tipoCliente', operator: '!=', value: 'anonimo',
})
required: chiamarlo due volte non duplica required nel validation — il check è basato su validation.includes('required').Row helper
row(fields, opts?)
| Parametro | Tipo | Descrizione |
|---|---|---|
fields | FieldDefinition[] | Campi della riga |
opts | RowHelperOptions | Omit<RowDefinition, 'fields'> (es. class) |
row([
field.text('via', 'Via', { span: 8 }),
field.text('cap', 'CAP', { span: 4, validation: 'cap' }),
])
// Con classe custom
row(
[field.text('note', 'Note', { span: 12 })],
{ class: 'border-t pt-4' }
)
Section helper
section(rows, opts?)
| Parametro | Tipo | Descrizione |
|---|---|---|
rows | RowDefinition[] | Righe della sezione |
opts | SectionHelperOptions | Omit<SectionDefinition, 'rows'> (title, subtitle, icon, layout, columns, collapsible, collapsed, visible, class) |
section([
row([field.text('nome', 'Nome'), field.text('cognome', 'Cognome')]),
row([field.email('email', 'Email')]),
], {
title: 'Anagrafica',
subtitle: 'Dati personali del contatto',
icon: 'pi pi-user',
collapsible: true,
})
Form helper
form(sections)
Wrappa un array di SectionDefinition in un FormSchema:
const schema = form([
section([
row([field.required(field.text('nome', 'Nome', { span: 6 }))]),
], { title: 'Anagrafica' }),
section([
row([field.email('email', 'Email')]),
], { title: 'Contatti', collapsed: true }),
])
Esempio completo
import { field, row, section, form } from '@pzeta/vue-form'
const tipoOptions = [
{ label: 'Privato', value: 'privato' },
{ label: 'Azienda', value: 'azienda' },
]
const schema = form([
section([
row([
field.required(field.text('codice', 'Codice', { span: 4 })),
field.required(field.text('ragioneSociale', 'Ragione sociale', { span: 8 })),
]),
row([
field.required(field.select('tipo', 'Tipo cliente', tipoOptions, {
span: 6,
props: { optionLabel: 'label', optionValue: 'value' },
})),
]),
row([
field.visible(
field.text('partitaIva', 'Partita IVA', { span: 6, validation: 'partitaiva' }),
{ field: 'tipo', operator: '==', value: 'azienda' },
),
field.visible(
field.text('codiceFiscale', 'Codice Fiscale', { span: 6, validation: 'codicefiscale' }),
{ field: 'tipo', operator: '==', value: 'privato' },
),
]),
], { title: 'Identificativi', icon: 'pi pi-id-card' }),
section([
row([
field.email('email', 'Email', { span: 6, validation: 'email' }),
field.text('telefono', 'Telefono', { span: 6 }),
]),
], { title: 'Contatti', icon: 'pi pi-phone', collapsible: true }),
])
<DynamicFormLayouts v-model="formData" :schema="schema" />
Quando usare i builder
Il default è l'oggetto literal. I builder hanno senso solo se uno di questi vincoli ti rallenta concretamente:
✅ Casi in cui i builder aiutano:
- Hai una libreria di campi base riusabili che applichi con varianti tramite
field.required(...)/field.visible(...)/field.disabled(...)— l'idempotenza direquirede la composizione esplicita evitano la duplicazione manuale. - Lo schema contiene molte select/listbox/radio inline con la stessa struttura
props: { options: [...] }ripetuta: gli helperfield.select/multiselect/listbox/radioaccettano l'array come terzo argomento e iniettanoprops.optionsautomaticamente.
❌ Casi in cui l'oggetto literal è chiaramente meglio (default):
- Schema serializzati (caricati da backend, file JSON, configurazione DB, editor visuale): sono già nel formato corretto, non serve trasformarli.
- Schema piccoli o medi (fino a una dozzina di campi): l'oggetto literal resta leggibile senza import.
- Code review e diff comprensibili: un literal mostra la struttura completa nello stesso punto, i builder spezzano la struttura in più chiamate.
- Onboarding di nuovi sviluppatori: l'oggetto literal mappa 1:1 sui tipi
FormSchema/SectionDefinition/RowDefinition/FieldDefinitionmostrati nella documentazione — nessuna API parallela da imparare.
TypeScript
Tutti gli helper sono tipizzati e ritornano i tipi canonici di DynamicFormLayouts:
import type {
FieldHelperOptions,
RowHelperOptions,
SectionHelperOptions,
FieldDefinition,
RowDefinition,
SectionDefinition,
FormSchema,
} from '@pzeta/vue-form'