Components

DynamicFormDialog

Dialog modale con form dinamico per creazione/modifica record con DynamicFormLayouts integrato.

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 (Crea vs Salva), header (t('ui.common.new') vs t('ui.common.edit')), e payload di @save (in edit invia solo i campi modificati dirtyValues)
  • 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 + slot field-{name}-{childSlot} e section-before-{idx} / section-after-{idx} forwardati al DynamicFormLayouts interno
  • Chiusura controllata: dismissableMask default false (no chiusura su click overlay) e closeOnEscape default true (chiusura via tastiera) — entrambi disabilitati durante submitting

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

PropTipoDefaultDescrizione
visiblebooleanRichiesto. Visibilità dialog (v-model:visible)
schemaFormSchemaRichiesto. Schema del form (vedi DynamicFormLayouts)
displayMode'normal' | 'tabs' | 'wizard' | 'accordion''normal'Modalità del DynamicFormLayouts interno
isEditbooleanfalseModalità modifica vs creazione — cambia label save (Salva/Crea), header default e payload di @save
initialDataRecord<string, unknown>{}Dati iniziali per la modalità modifica (passati a DynamicFormLayouts.initialData)
headerstring''Titolo header dialog. Se omesso, usa t('ui.common.edit') o t('ui.common.new') in base a isEdit
widthstring'600px'Larghezza dialog (CSS value)
modalbooleantrueAbilita overlay modale
dismissableMaskbooleanfalseChiude 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
closeOnEscapebooleantrueChiude il dialog alla pressione di Escape. Default true (la tastiera è una scorciatoia attesa per la chiusura). Disabilitato quando submitting=true

Header personalizzato

PropTipoDefaultDescrizione
headerIconstringIcona PrimeIcons accanto al titolo (es. 'pi pi-pencil'). Ignorata se viene fornito lo slot #header o #icon
headerSubtitlestringSottotitolo descrittivo sotto il titolo. Ignorato se viene fornito lo slot #header
headerSeverityActionSeverity'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

PropTipoDefaultDescrizione
submittingbooleanfalseLoading 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

PropTipoDefaultDescrizione
extraActionsActionDefinition[][]Azioni aggiuntive nel footer (Button accanto a Cancel/Save). Ogni azione ha position, severity, visible/disabled predicate, handler async — vedi Azioni dichiarative
Pattern 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

EventoPayloadDescrizione
update:visiblebooleanCambio visibilità (compatibile v-model:visible). Emesso a false su click X, click overlay (con modal=true) o click Cancel — a meno che submitting=true
saveRecord<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
Niente evento 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

SlotSlot propsDescrizione
header{ resolvedHeader, isEdit, submitting }Override completo del header (titolo + icona + sottotitolo). Disabilita headerIcon/headerSubtitle/headerSeverity
iconSolo 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
Slot riservati: il componente filtra automaticamente gli slot 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>
<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>
Auto-disable durante submit: tutti i pulsanti delle 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àTipoDescrizione
validate()() => booleanForza la validazione di tutti i campi del form interno. Ritorna true se valido. Equivale a layoutsRef.value.validate()
reset()() => voidResetta il DynamicFormLayouts interno ai valori iniziali
submit()() => booleanTenta il submit: se validate() passa, emette @save e chiude il dialog (rispettando il pattern submitting). Ritorna true se la validazione è passata
valuesRef<Record<string, unknown>>Riferimento reattivo ai valori correnti del form

Differenza con DynamicFormDrawerWithTable

AspettoDynamicFormDialogDynamicFormDrawerWithTable
UIDialog modale (al centro)Drawer laterale + tabella
Lista record❌ — devi gestirla esternamente✅ — DataTable integrata
Operazioni CRUDSolo create/update tramite event savecreate/update/delete con fetchAllFn, createFn, updateFn, deleteFn
Quando usarloForm one-shot collegato a un record specificoPannello completo di gestione entità

Pattern: integrazione con tabella esistente

Combinazione tipica: una tabella PrimeVue mostra i record, un pulsante "Modifica" apre il dialog precompilato.