Components

Schema builders

Helper fluenti field, row, section, form per costruire FormSchema. Alternativa opzionale all'oggetto literal — che resta la forma raccomandata.
Preferisci sempre l'oggetto literal. Lo schema scritto come { 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?)
ParametroTipoDescrizione
namestringChiave nel formData
labelstring (opzionale)Etichetta visibile
optsFieldHelperOptions (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',
})
Idempotenza di required: chiamarlo due volte non duplica required nel validation — il check è basato su validation.includes('required').

Row helper

row(fields, opts?)
ParametroTipoDescrizione
fieldsFieldDefinition[]Campi della riga
optsRowHelperOptionsOmit<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?)
ParametroTipoDescrizione
rowsRowDefinition[]Righe della sezione
optsSectionHelperOptionsOmit<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 di required e la composizione esplicita evitano la duplicazione manuale.
  • Lo schema contiene molte select/listbox/radio inline con la stessa struttura props: { options: [...] } ripetuta: gli helper field.select/multiselect/listbox/radio accettano l'array come terzo argomento e iniettano props.options automaticamente.

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 / FieldDefinition mostrati nella documentazione — nessuna API parallela da imparare.
Regola pratica: parti sempre dall'oggetto literal. Passa ai builder solo quando un pattern di ripetizione concreto (non ipotetico) emerge nel tuo schema. Convertire literal → builder è meccanico; convertire builder → literal è altrettanto facile.

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'