Components

DynamicFormListDialog

Dialog che combina lista di record esistenti + form di aggiunta inline. Gestione collezioni embedded senza data fetching — il parent passa items e gestisce add/remove via eventi.

Panoramica

DynamicFormListDialog è il dialog ideale per gestire collezioni embedded — liste di oggetti già presenti nei dati del parent, dove servono operazioni rapide di aggiunta e rimozione senza un endpoint CRUD dedicato.

A differenza di DynamicFormDrawerWithTable:

  • Non fa data fetching: il parent passa la lista items e riceve eventi add/remove
  • Non ha colonne configurabili: la lista è renderizzata con un template semplice (slot #item per personalizzare)
  • No endpoint backend: tutta la persistenza è responsabilità del parent (es. salvataggio in un ref locale + commit globale al submit del form padre)

Esempi tipici:

  • Lista di regole di concatenamento di un tipo attività
  • Lista di partecipanti a una riunione
  • Lista di assegnatari embedded in un record principale
  • Lista di tag/etichette personalizzati

Props

PropTipoDefaultDescrizione
visiblebooleanRichiesto. Visibilità dialog (v-model:visible)
itemsT[][]Record esistenti renderizzati come lista
schemaFormSchemaRichiesto. Schema del form di aggiunta
headerstringTitolo del dialog
keyFieldkeyof T'id'Chiave usata per :key del v-for
displayFieldkeyof T'nome'Campo usato per il testo della riga (fallback se non passi #item)
emptyMessagestring''Messaggio mostrato quando items è vuoto (override dello slot #empty)
addLabelstringEtichetta del pulsante "Aggiungi" — di default usa la traduzione ui.common.create
modalbooleantrueDialog modale (overlay)
dismissableMaskbooleanfalseChiude il dialog al click sull'overlay esterno. Default false per evitare perdita accidentale di dati compilati nel form di aggiunta
closeOnEscapebooleantrueChiude il dialog alla pressione di Escape
widthstring'500px'Larghezza CSS del dialog
showDeletebooleantrueMostra pulsante elimina su ogni riga
resetOnAddbooleantrueResetta il form dopo ogni aggiunta riuscita (bumpa la :key del DynamicFormLayouts interno)
Il componente è generico: DynamicFormListDialog<T extends Record<string, unknown>>. TypeScript inferisce T dal tipo passato a items, garantendo type safety in keyField/displayField/eventi.

Eventi

EventoPayloadDescrizione
update:visiblebooleanCambio visibilità (compatibile v-model:visible)
addRecord<string, unknown>Submit del form di aggiunta (solo se valid)
removeTClick sul pulsante elimina di una riga
Il componente non muta items direttamente. È responsabilità del parent gestire add/remove aggiornando l'array sorgente. Questo permette controlli pre-add (validazione duplicati, conferma utente, fetch lookup) e annullamenti.

Slots

SlotSlot propsDescrizione
item{ item: T }Override del rendering della singola riga
emptyOverride del messaggio "lista vuota" (override di emptyMessage)
field-{name}-{childSlot}(forwardati al DynamicFormLayouts interno)Espone lo slot {childSlot} del componente del campo {name} nel form di aggiunta. Vedi DynamicFormLayouts → Slot field-{name}-{childSlot}
section-before-{idx}{ values, valid, dirty, isEdit, submitting, sectionIndex }Forwardato al DynamicFormLayouts interno (form di aggiunta) — contenuto prima della sezione {idx}
section-after-{idx}{ values, valid, dirty, isEdit, submitting, sectionIndex }Forwardato al DynamicFormLayouts interno (form di aggiunta) — contenuto dopo la sezione {idx}
Slot riservati: item ed empty sono gestiti dal Dialog stesso. Tutti gli altri slot vengono forwardati al DynamicFormLayouts interno usato per il form di aggiunta — stesso pattern di DynamicFormDialog.

Esempio: lista regole di concatenamento

<script setup lang="ts">
import { ref } from 'vue'
import { DynamicFormListDialog, field, row, section, form } from '@pzeta/vue-form'

interface Concatenamento {
  id: number
  tipoSuccessivo: string
  intervalloGiorni: number
}

const tipo = ref({
  nome: 'Telefonata',
  concatenamenti: [
    { id: 1, tipoSuccessivo: 'Email', intervalloGiorni: 7 },
  ] as Concatenamento[],
})

const showDialog = ref(false)

const tipoOptions = [
  { label: 'Email', value: 'Email' },
  { label: 'Riunione', value: 'Riunione' },
  { label: 'Documento', value: 'Documento' },
]

const formSchema = form([
  section([
    row([
      field.required(field.select('tipoSuccessivo', 'Tipo successivo', tipoOptions, {
        span: 8,
        props: { optionLabel: 'label', optionValue: 'value' },
      })),
      field.required(field.number('intervalloGiorni', 'Giorni', {
        span: 4,
        validation: 'required|numeric|positive',
      })),
    ]),
  ]),
])

let nextId = 2

function handleAdd(data: Record<string, unknown>) {
  tipo.value.concatenamenti.push({
    id: nextId++,
    tipoSuccessivo: data.tipoSuccessivo as string,
    intervalloGiorni: data.intervalloGiorni as number,
  })
}

function handleRemove(item: Concatenamento) {
  tipo.value.concatenamenti = tipo.value.concatenamenti.filter(c => c.id !== item.id)
}
</script>

<template>
  <Button label="Gestisci concatenamenti" @click="showDialog = true" />

  <DynamicFormListDialog
    v-model:visible="showDialog"
    :items="tipo.concatenamenti"
    :schema="formSchema"
    header="Concatenamenti"
    key-field="id"
    display-field="tipoSuccessivo"
    @add="handleAdd"
    @remove="handleRemove"
  >
    <template #item="{ item }">
      <span>
        {{ item.tipoSuccessivo }}
        <small class="text-muted">+ {{ item.intervalloGiorni }} giorni</small>
      </span>
    </template>
  </DynamicFormListDialog>
</template>

Esempio: con conferma di rimozione

<script setup lang="ts">
import { useConfirm } from '@pzeta/vue-components'

const confirm = useConfirm()

function handleRemove(item: Concatenamento) {
  confirm.require({
    message: `Eliminare il concatenamento con ${item.tipoSuccessivo}?`,
    accept: () => {
      tipo.value.concatenamenti = tipo.value.concatenamenti.filter(c => c.id !== item.id)
    },
  })
}
</script>

Differenze vs altri Dialog/Drawer

Caso d'usoComponente
Form modale singolo (crea/modifica)DynamicFormDialog
Drawer laterale con auto-fetch del recordDynamicFormDrawer
CRUD completo con tabella + drawer + endpointDynamicFormDrawerWithTable
Collezione embedded già in memoriaDynamicFormListDialog

DynamicFormListDialog non sostituisce DynamicFormDrawerWithTable — coprono scenari diversi:

AspettoListDialogDrawerWithTable
Data sourceProp items (in memoria)fetchFn + endpoint backend
Form di aggiuntaInline nel dialogDrawer separato
Operazioniadd + remove (via evento)CRUD completo verso API
PersistenzaResponsabilità del parentGestita dal componente

TypeScript

import { DynamicFormListDialog } from '@pzeta/vue-form'
import type { DynamicFormListDialogProps } from '@pzeta/vue-form'

Il componente è generico su T: DynamicFormListDialog<T> con T extends Record<string, unknown>.