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 via AttivitaPlugin context.
  • View headless (es. AttivitaListaEntitaView) — puramente presentazionale: riceve attivita, loading, error come 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

ScenarioPerché la View aiuta
Storybook / playgroundNiente plugin da configurare, dati statici
Dati già in memoriaCache Pinia, store esterno, prop dal padre — niente fetch duplicato
Datasource non-PostgRESTBackend GraphQL/REST custom — gestisci il fetch tu, usa solo la UI
SSR senza backend pre-renderPre-componi i dati lato server, passa via prop
Test isolatiSnapshot 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

WrapperView headlessNote
AttivitaListaEntitaAttivitaListaEntitaViewProp: attivita, riepilogo, loading, utenteOptions
AttivitaBadgeAttivitaBadgeViewProp: riepilogo, loading
AttivitaConsolidataAttivitaConsolidataViewProp: attivita, loading
AttivitaCalendarioAttivitaCalendarioViewProp: attivita, loading; Eventi: month-change, attivita-click
AttivitaTimelineAttivitaTimelineViewProp: attivita, loading, error
RicorrenzaPanelRicorrenzaPanelViewv-model su RicorrenzaForm
PartecipantiPanelPartecipantiPanelViewv-model su PartecipanteForm[]
DocumentoPanelDocumentoPanelViewProp: iddocumento, uploading; Eventi: upload, remove
FirmaPanelFirmaPanelViewProp iniziali, eventi submit

Componenti senza variante View (perché già presentational o tightly-coupled all'context):

  • AttivitaForm — usa internamente useAttivitaForm, richiede context
  • AttivitaListItem — atomico, già presentational
  • AttivitaKanbanTipo — wrapper composto (non ha senso decoupled)
  • CompletaDialog / AnnullaDialog — già presentational (riceve attivita via 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.