DynamicFormDialog
Panoramica
DynamicFormDialog apre un dialog modale (Dialog di @pzeta/vue-components) contenente un DynamicFormLayouts preconfigurato. Gestisce automaticamente il ciclo vita del form, i pulsanti azione (Crea/Salva/Annulla) e supporta le modalità "creazione" e "modifica".
Caratteristiche chiave:
- Modalità creazione/modifica automatica in base a
isEdit: cambia label del pulsante save (CreavsSalva), header (t('ui.common.new')vst('ui.common.edit')), e payload di@save(in edit invia solo i campi modificatidirtyValues) - Pulsante save intelligente: disabilitato finché il form non è valido; in modalità edit anche se non
dirty(nessun PATCH inutile) - Reset automatico dello stato al cambio di
visible: false - Header personalizzabile con icona + sottotitolo + severity (4 prop dedicate) o slot completo
- Azioni dichiarative
extraActions: pulsanti aggiuntivi nel footer con posizione, severity, predicates di visibilità/disable, handler async - Submitting state controllato dal parent: durante una save async il dialog non è chiudibile, i pulsanti sono disabilitati, il save mostra spinner
- API imperativa via
defineExpose:validate(),reset(),submit(),values - Slot completi:
header,icon,body-before,body-after,footer,footer-extra+ slotfield-{name}-{childSlot}esection-before-{idx}/section-after-{idx}forwardati alDynamicFormLayoutsinterno - Chiusura controllata:
dismissableMaskdefaultfalse(no chiusura su click overlay) ecloseOnEscapedefaulttrue(chiusura via tastiera) — entrambi disabilitati durantesubmitting
Casi d'uso tipici:
- Modifica record da una tabella senza navigare a una pagina dedicata
- Creazione rapida di entità di supporto (es. nuova categoria mentre si compila un form prodotto)
- Form di configurazione con validazione completa e azioni custom prima del submit
Anteprima live
Utilizzo
Props
Core
| Prop | Tipo | Default | Descrizione |
|---|---|---|---|
visible | boolean | — | Richiesto. Visibilità dialog (v-model:visible) |
schema | FormSchema | — | Richiesto. Schema del form (vedi DynamicFormLayouts) |
displayMode | 'normal' | 'tabs' | 'wizard' | 'accordion' | 'normal' | Modalità del DynamicFormLayouts interno |
isEdit | boolean | false | Modalità modifica vs creazione — cambia label save (Salva/Crea), header default e payload di @save |
initialData | Record<string, unknown> | {} | Dati iniziali per la modalità modifica (passati a DynamicFormLayouts.initialData) |
header | string | '' | Titolo header dialog. Se omesso, usa t('ui.common.edit') o t('ui.common.new') in base a isEdit |
width | string | '600px' | Larghezza dialog (CSS value) |
modal | boolean | true | Abilita overlay modale |
dismissableMask | boolean | false | Chiude il dialog al click sull'overlay esterno. Default false per evitare perdita accidentale di dati compilati. Imposta true per ripristinare il comportamento standard del Dialog di @pzeta/vue-components. Disabilitato quando submitting=true |
closeOnEscape | boolean | true | Chiude il dialog alla pressione di Escape. Default true (la tastiera è una scorciatoia attesa per la chiusura). Disabilitato quando submitting=true |
Header personalizzato
| Prop | Tipo | Default | Descrizione |
|---|---|---|---|
headerIcon | string | — | Icona PrimeIcons accanto al titolo (es. 'pi pi-pencil'). Ignorata se viene fornito lo slot #header o #icon |
headerSubtitle | string | — | Sottotitolo descrittivo sotto il titolo. Ignorato se viene fornito lo slot #header |
headerSeverity | ActionSeverity | 'primary' | Severity dell'icona del header (mappa a colori Tailwind: primary/secondary/success/info/warn/help/danger/contrast). Ignorata se non c'è headerIcon o se viene fornito lo slot #header |
Submitting / async control
| Prop | Tipo | Default | Descrizione |
|---|---|---|---|
submitting | boolean | false | Loading state controllato dal parent. Quando true: pulsante save mostra spinner, Cancel + tutti i pulsanti del footer disabilitati, dialog non chiudibile (X disabilitata, click overlay no-op, Escape disabilitato). Tipico uso: parent imposta submitting=true prima di await api.save() e a false al termine, indipendentemente dall'esito |
Azioni custom
| Prop | Tipo | Default | Descrizione |
|---|---|---|---|
extraActions | ActionDefinition[] | [] | Azioni aggiuntive nel footer (Button accanto a Cancel/Save). Ogni azione ha position, severity, visible/disabled predicate, handler async — vedi Azioni dichiarative |
submitting: il parent controlla esplicitamente il flusso di chiusura. Se imposti submitting=true sincronamente nel listener @save, il dialog non si chiude finché submitting torna false. Questo permette di gestire errori API e mantenere il form aperto in caso di fallimento, oppure mostrare un toast di successo prima di chiudere.Eventi
| Evento | Payload | Descrizione |
|---|---|---|
update:visible | boolean | Cambio visibilità (compatibile v-model:visible). Emesso a false su click X, click overlay (con modal=true) o click Cancel — a meno che submitting=true |
save | Record<string, unknown> | Submit valido — il payload è formData completo in creazione, dirtyValues (solo campi modificati) in modalità isEdit |
field-change | (fieldName: string, value: unknown, formData: Record<string, unknown>) | Forwardato dal DynamicFormLayouts interno. Emesso dopo il commit di un campo — utile per logiche globali (telemetria, autosave esterno). Per prefill locali a un campo preferire l'hook field.onChange |
cancel: la chiusura del dialog viene segnalata via update:visible(false). Se devi distinguere chiusura volontaria da save riuscito, usa un watcher su visible o gestisci la logica nel listener @save.Slot
| Slot | Slot props | Descrizione |
|---|---|---|
header | { resolvedHeader, isEdit, submitting } | Override completo del header (titolo + icona + sottotitolo). Disabilita headerIcon/headerSubtitle/headerSeverity |
icon | — | Solo l'icona accanto al titolo (manteniene il titolo testuale standard). Ha priorità su headerIcon |
body-before | { values, valid, dirty, isEdit, submitting } | Contenuto prima del form (es. avviso, riepilogo dati esterni) |
body-after | { values, valid, dirty, isEdit, submitting } | Contenuto dopo il form (es. legenda, link a documentazione) |
footer | { submit, cancel, values, valid, dirty, submitting } | Override completo del footer: nasconde i pulsanti Cancel/Save di default. Le funzioni submit/cancel sono passate come slot props |
footer-extra | { submit, cancel, values, valid, dirty, submitting } | Azioni inline aggiunte prima del Cancel, mantenendo Cancel/Save di default. Alternativa imperativa a extraActions |
field-{name}-{childSlot} | (forwardati al DynamicFormLayouts interno) | Espone lo slot {childSlot} del componente del campo {name} (es. field-tipoId-option per personalizzare il rendering delle opzioni di un Select). Vedi DynamicFormLayouts → Slot field-{name}-{childSlot} |
section-before-{idx} | { values, valid, dirty, isEdit, submitting, sectionIndex } | Forwardato a DynamicFormLayouts. Contenuto prima della sezione {idx} (0-based). isEdit/submitting propagati automaticamente dal Dialog |
section-after-{idx} | { values, valid, dirty, isEdit, submitting, sectionIndex } | Forwardato a DynamicFormLayouts. Contenuto dopo la sezione {idx}. Utile per panel data-aware che reagiscono a isEdit/submitting |
header, footer, footer-extra, body-before, body-after, icon — questi vengono gestiti dal Dialog stesso e non propagati a DynamicFormLayouts. Tutti gli altri slot (incluso field-{name}-{childSlot} e section-before-{idx} / section-after-{idx}) sono forwardati così come sono al form interno.isEdit / submitting automatici negli slot section: il Dialog passa al DynamicFormLayouts interno il proprio isEdit e submitting, quindi gli slot section-before-{idx} / section-after-{idx} ricevono automaticamente questi valori nello scope, senza configurazione aggiuntiva. Vedi esempio nella sezione Esempio: pannello data-aware in sezione.Esempio: header custom completo
<DynamicFormDialog v-model:visible="show" :schema="schema" :is-edit="true">
<template #header="{ resolvedHeader, isEdit, submitting }">
<div class="flex items-center justify-between w-full pr-6">
<div class="flex items-center gap-3">
<i class="pi pi-pencil text-blue-500 text-2xl" />
<div>
<h3 class="text-lg font-semibold">{{ resolvedHeader }}</h3>
<span class="text-xs text-gray-500">ID #{{ recordCorrente.id }}</span>
</div>
</div>
<ProgressSpinner v-if="submitting" style="width:24px; height:24px" />
</div>
</template>
</DynamicFormDialog>
Esempio: footer completamente custom
<DynamicFormDialog v-model:visible="show" :schema="schema">
<template #footer="{ submit, cancel, valid, submitting }">
<div class="flex justify-between w-full">
<Button label="Anteprima" icon="pi pi-eye" text @click="showPreview = true" />
<div class="flex gap-2">
<Button label="Annulla" severity="secondary" :disabled="submitting" @click="cancel" />
<Button label="Salva e nuovo" :disabled="!valid || submitting" @click="onSaveAndNew" />
<Button label="Salva" :disabled="!valid || submitting" :loading="submitting" @click="submit" />
</div>
</div>
</template>
</DynamicFormDialog>
Esempio: pannello data-aware in sezione
Lo slot section-after-{idx} permette di iniettare contenuto reattivo dopo una sezione del form, con accesso automatico a values, valid, dirty, isEdit, submitting. Tipico uso: pulsanti contestuali, anteprime, link a entità correlate visibili solo in modifica:
<DynamicFormDialog
v-model:visible="show"
:schema="schema"
:is-edit="isEdit"
:submitting="submitting"
@save="onSave"
>
<!-- Storico audit log dopo la prima sezione, solo in modifica -->
<template #section-after-0="{ values, isEdit, submitting }">
<Button
v-if="isEdit"
label="Storico modifiche"
icon="pi pi-history"
text
:disabled="submitting"
@click="showAuditLog(values.id)"
/>
</template>
<!-- Avviso prima della sezione fatturazione, basato su altri campi del form -->
<template #section-before-1="{ values }">
<div
v-if="values.tipoCliente === 'estero' && !values.partitaIva"
class="bg-blue-50 border border-blue-200 p-3 rounded mb-3 text-sm"
>
<i class="pi pi-info-circle text-blue-600 mr-2" />
I clienti esteri possono inserire la P.IVA o lasciare il campo vuoto.
</div>
</template>
</DynamicFormDialog>
Azioni dichiarative (extraActions)
Alternativa allo slot #footer-extra/#footer: invece di aggiungere markup custom, dichiari le azioni come dati. Più pulito quando hai 1-3 pulsanti aggiuntivi con logica semplice.
interface ActionDefinition {
label: string // testo del pulsante
icon?: string // PrimeIcons (es. 'pi pi-check')
severity?: ActionSeverity // 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'help' | 'danger' | 'contrast' (default: 'secondary')
position?: ActionPosition // 'before-cancel' (default) | 'after-cancel' | 'before-save'
visible?: ActionPredicate // true (default) | false | (values, valid, dirty) => boolean
disabled?: ActionPredicate // false (default) | true | (values, valid, dirty) => boolean
loading?: boolean // mostra spinner sul pulsante
text?: boolean // stile "text" senza background
handler: (values: Record<string, unknown>) => void | Promise<void>
}
Posizionamento
Lo slot footer di default ha questo layout (sinistra → destra):
[before-cancel] [footer-extra slot] [Cancel] [after-cancel] [before-save] [Save]
Esempio: "Segna come fatto" come azione extra
<script setup lang="ts">
import type { ActionDefinition } from '@pzeta/vue-form'
const extraActions: ActionDefinition[] = [
{
label: 'Segna come fatto',
icon: 'pi pi-check',
severity: 'success',
position: 'before-cancel',
// Visibile solo se è una creazione (no edit)
visible: (values, valid, dirty) => valid && !!values.idtipo,
// Disabilitata durante validazione di altri campi
disabled: (values, valid) => !valid,
handler: async (values) => {
await api.completaSubito(values)
},
},
]
</script>
<template>
<DynamicFormDialog
v-model:visible="show"
:schema="schema"
:extra-actions="extraActions"
@save="handleSave"
/>
</template>
extraActions vengono disabilitati automaticamente quando submitting=true, indipendentemente dal loro disabled predicate. Le predicate vengono valutate ad ogni cambio di formData/valid/dirty.Pattern submitting (save async)
Per gestire correttamente save asincroni con loading state, errori e chiusura controllata:
<script setup lang="ts">
import { ref } from 'vue'
import { DynamicFormDialog } from '@pzeta/vue-form'
import { useToast } from '@pzeta/vue-components'
const show = ref(false)
const submitting = ref(false)
const toast = useToast()
async function handleSave(values: Record<string, unknown>) {
submitting.value = true
try {
await api.create(values)
toast.add({ severity: 'success', summary: 'Salvato' })
show.value = false // chiudo solo a successo
} catch (e) {
toast.add({ severity: 'error', summary: 'Errore', detail: e.message })
// dialog rimane aperto, l'utente può correggere e riprovare
} finally {
submitting.value = false
}
}
</script>
<template>
<DynamicFormDialog
v-model:visible="show"
:schema="schema"
:submitting="submitting"
@save="handleSave"
/>
</template>
Metodi esposti (defineExpose)
Accessibili tramite un template ref:
<script setup lang="ts">
import { ref } from 'vue'
import { DynamicFormDialog } from '@pzeta/vue-form'
const dialogRef = ref<InstanceType<typeof DynamicFormDialog> | null>(null)
function tryAutoSubmit() {
// Forza validazione + save (rispetta il pattern submitting)
const submitted = dialogRef.value?.submit()
if (!submitted) {
console.log('Validazione fallita, dialog rimane aperto')
}
}
function checkValid() {
return dialogRef.value?.validate() ?? false
}
</script>
<template>
<DynamicFormDialog ref="dialogRef" v-model:visible="show" :schema="schema" @save="onSave" />
<button @click="tryAutoSubmit">Save automatico</button>
</template>
| Metodo / proprietà | Tipo | Descrizione |
|---|---|---|
validate() | () => boolean | Forza la validazione di tutti i campi del form interno. Ritorna true se valido. Equivale a layoutsRef.value.validate() |
reset() | () => void | Resetta il DynamicFormLayouts interno ai valori iniziali |
submit() | () => boolean | Tenta il submit: se validate() passa, emette @save e chiude il dialog (rispettando il pattern submitting). Ritorna true se la validazione è passata |
values | Ref<Record<string, unknown>> | Riferimento reattivo ai valori correnti del form |
Differenza con DynamicFormDrawerWithTable
| Aspetto | DynamicFormDialog | DynamicFormDrawerWithTable |
|---|---|---|
| UI | Dialog modale (al centro) | Drawer laterale + tabella |
| Lista record | ❌ — devi gestirla esternamente | ✅ — DataTable integrata |
| Operazioni CRUD | Solo create/update tramite event save | create/update/delete con fetchAllFn, createFn, updateFn, deleteFn |
| Quando usarlo | Form one-shot collegato a un record specifico | Pannello completo di gestione entità |
Pattern: integrazione con tabella esistente
Combinazione tipica: una tabella PrimeVue mostra i record, un pulsante "Modifica" apre il dialog precompilato.