Composables

useDetailForm

Gestione stato form di dettaglio. Dirty tracking, loading guard, canSave, wrapper saving e reset.

Signature

function useDetailForm(options?: UseDetailFormOptions): UseDetailFormResult

Astrae lo stato base di un form di dettaglio: loading, isFormDirty, canSave derivato, wrapper saving() per operazioni async.

Opzioni UseDetailFormOptions

interface UseDetailFormOptions {
  initialLoading?: boolean
  canDelete?: boolean
}
OpzioneTipoDefaultDescrizione
initialLoadingbooleanfalseValore iniziale di loading (utile se il form parte caricando un record)
canDeletebooleantrueFlag statico — esposto come result.canDelete. Tipicamente derivato da permessi

Risultato UseDetailFormResult

interface UseDetailFormResult {
  loading: Ref<boolean>
  isFormDirty: Ref<boolean>
  canDelete: boolean
  canSave: ComputedRef<boolean>
  saving: (operation: () => Promise<void>) => Promise<void>
  markDirty: () => void
  markClean: () => void
  reset: () => void
}
CampoDescrizione
loadingRef<boolean> di stato caricamento/salvataggio
isFormDirtyRef<boolean>true se ci sono modifiche non salvate
canDeleteBoolean statico (passato in opzioni)
canSavecomputed(() => isFormDirty.value && !loading.value)
saving(op)Wrapper: imposta loading = true, esegue op(), in finally riporta loading = false. Non gestisce errori: catcha tu nel chiamante
markDirty()isFormDirty = true
markClean()isFormDirty = false (chiamare dopo save riuscito)
reset()Riporta loading a initialLoading e isFormDirty = false

Esempio completo

<script setup lang="ts">
import { reactive, watch } from 'vue'
import { useMfeContext, useDetailForm } from '@pzeta/mfe-contracts/composables'

const props = defineProps<{ clienteId: string }>()
const ctx = useMfeContext()

const form = reactive({
  ragioneSociale: '',
  piva: '',
  email: '',
})

const {
  loading,
  isFormDirty,
  canSave,
  saving,
  markDirty,
  markClean,
  reset,
} = useDetailForm({ initialLoading: true })

// Carica record iniziale
await saving(async () => {
  const { data } = await ctx.http.get<Cliente>(`/api/clienti/${props.clienteId}`)
  Object.assign(form, data)
  markClean() // reset dirty dopo load
})

// Marca dirty al primo cambio
watch(form, () => markDirty(), { deep: true })

async function onSave() {
  try {
    await saving(async () => {
      await ctx.http.put(`/api/clienti/${props.clienteId}`, form)
      markClean()
    })
  } catch (err) {
    console.error('Save failed:', err)
    // mostra toast errore...
  }
}
</script>

<template>
  <form @submit.prevent="onSave">
    <input v-model="form.ragioneSociale" placeholder="Ragione sociale" />
    <input v-model="form.piva" placeholder="P.IVA" />
    <input v-model="form.email" placeholder="Email" />

    <button type="submit" :disabled="!canSave">
      {{ loading ? 'Salvataggio...' : 'Salva' }}
    </button>
    <button type="button" @click="reset">Annulla modifiche</button>
  </form>
</template>

Pattern: prompt "Conferma uscita"

Combina con MFEModule.onBeforeUnmount o con un router guard per intercettare la navigazione mentre il form è dirty:

import { onBeforeRouteLeave } from 'vue-router'

const { isFormDirty } = useDetailForm()

onBeforeRouteLeave((to, from, next) => {
  if (isFormDirty.value && !confirm('Hai modifiche non salvate. Continuare?')) {
    next(false)
  } else {
    next()
  }
})

Pattern: disabilita Save mentre carica

canSave è già computed, quindi rispecchia automaticamente sia isFormDirty sia loading:

isFormDirtyloadingcanSave
falsefalsefalse (niente da salvare)
truefalsetrue
falsetruefalse
truetruefalse (operazione in corso)

Note

  • saving() non gestisce gli errori: l'eccezione si propaga al chiamante. Usa try/catch o un wrapper useErrorToast.
  • canDelete è statico (non Ref): se i permessi cambiano durante la vita del form, ricalcolalo passando un nuovo useDetailForm({ canDelete: hasPermission }) oppure gestiscilo separatamente con un computed esterno.

Tipi correlati

  • useDialog — gestione dialog (overlap parziale)
  • useCrudView — alternativa per liste con dialog modale