View Headless
title: View headless (presentational) description: Versioni "*View" dei componenti compositi. Riceve dati via prop, emette eventi. Nessun AttivitaPlugin richiesto.
Cosa sono
I componenti compositi sono esposti in due forme:
- Wrapper data-aware (es.
AttivitaListaEntita) — fetch automatico viaAttivitaPlugincontext. - View headless (es.
AttivitaListaEntitaView) — puramente presentazionale: riceveattivita,loading,errorcome prop ed emette eventi (attivita-click,month-change,submit, ...).
Le View non richiedono AttivitaPlugin. Possono essere usate in qualsiasi app Vue 3 con qualsiasi sorgente dati.
Quando usarle
| Scenario | Perché la View aiuta |
|---|---|
| Storybook / playground | Niente plugin da configurare, dati statici |
| Dati già in memoria | Cache Pinia, store esterno, prop dal padre — niente fetch duplicato |
| Datasource non-PostgREST | Backend GraphQL/REST custom — gestisci il fetch tu, usa solo la UI |
| SSR senza backend pre-render | Pre-componi i dati lato server, passa via prop |
| Test isolati | Snapshot delle View senza setup provide |
Per il caso "fetch dal backend PostgREST con form integrati" il wrapper data-aware resta la scelta giusta: già configurato, niente boilerplate.
Mappa wrapper → View
| Wrapper | View headless | Note |
|---|---|---|
AttivitaListaEntita | AttivitaListaEntitaView | Prop: attivita, riepilogo, loading, utenteOptions |
AttivitaBadge | AttivitaBadgeView | Prop: riepilogo, loading |
AttivitaConsolidata | AttivitaConsolidataView | Prop: attivita, loading |
AttivitaCalendario | AttivitaCalendarioView | Prop: attivita, loading; Eventi: month-change, attivita-click |
AttivitaTimeline | AttivitaTimelineView | Prop: attivita, loading, error |
RicorrenzaPanel | RicorrenzaPanelView | v-model su RicorrenzaForm |
PartecipantiPanel | PartecipantiPanelView | v-model su PartecipanteForm[] |
DocumentoPanel | DocumentoPanelView | Prop: iddocumento, uploading; Eventi: upload, remove |
FirmaPanel | FirmaPanelView | Prop iniziali, eventi submit |
Componenti senza variante View (perché già presentational o tightly-coupled all'context):
AttivitaForm— usa internamenteuseAttivitaForm, richiede contextAttivitaListItem— atomico, già presentationalAttivitaKanbanTipo— wrapper composto (non ha senso decoupled)CompletaDialog/AnnullaDialog— già presentational (riceveattivitavia prop)
Esempio: calendario standalone
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { AttivitaCalendarioView } from '@pzeta/vue-attivita'
import type { AttivitaView } from '@pzeta/vue-attivita'
const attivita = ref<AttivitaView[]>([])
const loading = ref(false)
async function loadMonth(month: number, year: number) {
loading.value = true
try {
const res = await fetch(`/api/attivita/calendario?month=${month}&year=${year}`)
attivita.value = await res.json()
} finally {
loading.value = false
}
}
onMounted(() => {
const now = new Date()
loadMonth(now.getMonth() + 1, now.getFullYear())
})
</script>
<template>
<AttivitaCalendarioView
:attivita="attivita"
:loading="loading"
@month-change="({ month, year }) => loadMonth(month, year)"
@attivita-click="(a) => router.push(`/attivita/${a.idattivita}`)"
/>
</template>
Nessun AttivitaPlugin, nessun provide — la View è autosufficiente.
Esempio: badge da Pinia store
<script setup lang="ts">
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { AttivitaBadgeView } from '@pzeta/vue-attivita'
import { useAnagraficaStore } from '@/stores/anagrafica'
const props = defineProps<{ idanagrafica: number }>()
const store = useAnagraficaStore()
const { riepilogoMap, loadingMap } = storeToRefs(store)
const riepilogo = computed(() => riepilogoMap.value.get(props.idanagrafica) ?? null)
const loading = computed(() => loadingMap.value.has(props.idanagrafica))
</script>
<template>
<AttivitaBadgeView :riepilogo="riepilogo" :loading="loading" />
</template>
Lo store gestisce il fetch + cache; la View renderizza il badge senza accoppiarsi al backend.
Esempio: timeline in pagina pre-renderizzata SSR
<script setup lang="ts">
import { AttivitaTimelineView } from '@pzeta/vue-attivita'
const props = defineProps<{
attivita: AttivitaView[] // pre-fetch da loader SSR
}>()
</script>
<template>
<aside class="w-64 border-l p-4">
<h3 class="text-sm font-semibold mb-3">Attività recenti</h3>
<AttivitaTimelineView :attivita="attivita.slice(0, 5)" :loading="false" />
</aside>
</template>
Niente client-side fetch, niente flicker iniziale.
Pattern di composizione
Il wrapper data-aware è essenzialmente "View + composable":
<!-- Pseudocodice di AttivitaListaEntita -->
<script setup lang="ts">
import { useAttivita } from '../composables/useAttivita'
import AttivitaListaEntitaView from './AttivitaListaEntitaView.vue'
const props = defineProps<{ entitasorgente: string; identitasorgente: number }>()
const { attivita, riepilogo, loading } = useAttivita(props)
</script>
<template>
<AttivitaListaEntitaView
:attivita="attivita"
:riepilogo="riepilogo"
:loading="loading"
v-bind="$attrs"
/>
</template>
Se vuoi una variante "wrapper su un altro datasource", puoi replicare questo pattern usando direttamente la View.