00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Props ed Eventi

Props: passare dati ai componenti

Le props sono il meccanismo principale per passare dati da un componente genitore a un componente figlio. In Vue 3 con <script setup>, si utilizza defineProps.

defineProps base

<!-- CartaProdotto.vue -->
<script setup lang="ts">
// Sintassi runtime
const props = defineProps({
  titolo: String,
  prezzo: Number,
  disponibile: Boolean
})

console.log(props.titolo, props.prezzo)
</script>

<template>
  <div class="carta">
    <h3>{{ titolo }}</h3>
    <p>{{ prezzo }} EUR</p>
    <span v-if="disponibile">In stock</span>
  </div>
</template>

defineProps con TypeScript

La sintassi type-based offre una tipizzazione completa e chiara.

<script setup lang="ts">
// Sintassi type-based (consigliata con TypeScript)
interface Props {
  titolo: string
  prezzo: number
  disponibile?: boolean
  categorie?: string[]
  immagine?: string
}

const props = defineProps<Props>()

// Oppure inline
const props2 = defineProps<{
  nome: string
  eta: number
}>()
</script>

Valori predefiniti con withDefaults

<script setup lang="ts">
interface Props {
  titolo: string
  prezzo: number
  disponibile?: boolean
  categorie?: string[]
  variante?: 'primario' | 'secondario' | 'terziario'
}

const props = withDefaults(defineProps<Props>(), {
  disponibile: true,
  categorie: () => ['generale'],
  variante: 'primario'
})
</script>

Validazione delle props

Con la sintassi runtime puoi aggiungere validatori dettagliati.

<script setup lang="ts">
const props = defineProps({
  // Tipo base
  nome: String,

  // Tipo richiesto
  id: {
    type: Number,
    required: true
  },

  // Con valore predefinito
  stato: {
    type: String,
    default: 'attivo'
  },

  // Tipo multiplo
  identificativo: {
    type: [String, Number],
    required: true
  },

  // Validatore custom
  eta: {
    type: Number,
    validator(valore: number) {
      return valore >= 0 && valore <= 150
    }
  },

  // Oggetto con default factory
  configurazione: {
    type: Object,
    default() {
      return { tema: 'chiaro', lingua: 'it' }
    }
  },

  // Array con default factory
  tags: {
    type: Array as () => string[],
    default: () => []
  }
})
</script>

Passare le props

<!-- Componente genitore -->
<script setup lang="ts">
import CartaProdotto from './CartaProdotto.vue'
import { ref } from 'vue'

const prodotto = ref({
  titolo: 'Laptop Pro',
  prezzo: 1299,
  disponibile: true,
  categorie: ['elettronica', 'computer']
})
</script>

<template>
  <!-- Props individuali -->
  <CartaProdotto
    titolo="Laptop Pro"
    :prezzo="1299"
    :disponibile="true"
  />

  <!-- Props dinamiche con binding -->
  <CartaProdotto
    :titolo="prodotto.titolo"
    :prezzo="prodotto.prezzo"
    :disponibile="prodotto.disponibile"
  />

  <!-- Spread di tutte le props da un oggetto -->
  <CartaProdotto v-bind="prodotto" />

  <!-- Boolean shorthand: presenza = true -->
  <CartaProdotto titolo="Test" :prezzo="99" disponibile />
</template>

Props e reattivita

Le props sono di sola lettura. Non puoi modificarle nel componente figlio.

<script setup lang="ts">
import { computed, ref } from 'vue'

const props = defineProps<{
  contaIniziale: number
  testo: string
}>()

// SBAGLIATO: non modificare le props direttamente
// props.contaIniziale++ // Errore!

// CORRETTO: crea una copia locale se hai bisogno di modificarla
const contatoreLocale = ref(props.contaIniziale)

// CORRETTO: usa computed per valori derivati dalle props
const testoFormattato = computed(() => props.testo.toUpperCase())
</script>

Eventi: comunicare dal figlio al genitore

Gli eventi permettono ai componenti figli di comunicare con i genitori. Si usa defineEmits per dichiarare gli eventi emessi.

defineEmits base

<!-- BottoneContatore.vue -->
<script setup lang="ts">
// Sintassi array
const emit = defineEmits(['incrementa', 'decrementa', 'reset'])

function gestisciIncremento() {
  emit('incrementa')
}

function gestisciDecremento() {
  emit('decrementa')
}
</script>

<template>
  <div>
    <button @click="gestisciIncremento">+1</button>
    <button @click="emit('decrementa')">-1</button>
    <button @click="emit('reset')">Reset</button>
  </div>
</template>

defineEmits con TypeScript

<script setup lang="ts">
// Sintassi type-based con payload tipizzato
const emit = defineEmits<{
  incrementa: [valore: number]
  decrementa: [valore: number]
  reset: []
  aggiorna: [nome: string, valore: any]
}>()

function incrementaDi(n: number) {
  emit('incrementa', n)
}

function aggiornaCampo(nome: string, valore: any) {
  emit('aggiorna', nome, valore)
}
</script>

Ascoltare gli eventi nel genitore

<!-- Componente genitore -->
<script setup lang="ts">
import { ref } from 'vue'
import BottoneContatore from './BottoneContatore.vue'

const contatore = ref(0)

function gestisciIncremento(valore: number) {
  contatore.value += valore
}

function gestisciReset() {
  contatore.value = 0
}
</script>

<template>
  <p>Contatore: {{ contatore }}</p>

  <BottoneContatore
    @incrementa="gestisciIncremento"
    @decrementa="(v) => contatore -= v"
    @reset="gestisciReset"
  />
</template>

Validazione degli eventi

<script setup lang="ts">
const emit = defineEmits({
  // Validazione con funzione
  invia: (payload: { nome: string; email: string }) => {
    // Restituisci true se valido
    if (!payload.email.includes('@')) {
      console.warn('Email non valida')
      return false
    }
    return true
  },
  // Senza validazione
  annulla: null
})

function inviaForm() {
  emit('invia', { nome: 'Mario', email: 'mario@example.com' })
}
</script>

v-model su componenti

v-model su un componente crea un binding bidirezionale tra genitore e figlio. In Vue 3, v-model usa modelValue come prop e update:modelValue come evento.

v-model base

<!-- InputPersonalizzato.vue -->
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [valore: string]
}>()
</script>

<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
    class="input-custom"
  />
</template>
<!-- Utilizzo nel genitore -->
<script setup>
import { ref } from 'vue'
import InputPersonalizzato from './InputPersonalizzato.vue'

const nome = ref('')
</script>

<template>
  <!-- v-model crea il binding bidirezionale -->
  <InputPersonalizzato v-model="nome" />
  <p>Hai scritto: {{ nome }}</p>
</template>

v-model con defineModel (Vue 3.4+)

A partire da Vue 3.4, defineModel semplifica enormemente v-model.

<!-- InputModerno.vue -->
<script setup lang="ts">
// defineModel crea automaticamente prop + emit
const modello = defineModel<string>()
// modello e un Ref che puoi leggere e scrivere
</script>

<template>
  <input
    :value="modello"
    @input="modello = ($event.target as HTMLInputElement).value"
  />
</template>
<!-- Ancora piu conciso con v-model diretto -->
<script setup lang="ts">
const modello = defineModel<string>()
</script>

<template>
  <input v-model="modello" />
</template>

v-model multipli con nome

Un componente puo supportare piu v-model con nomi diversi.

<!-- FormUtente.vue -->
<script setup lang="ts">
const nome = defineModel<string>('nome')
const cognome = defineModel<string>('cognome')
const email = defineModel<string>('email')
</script>

<template>
  <div class="form">
    <input v-model="nome" placeholder="Nome" />
    <input v-model="cognome" placeholder="Cognome" />
    <input v-model="email" placeholder="Email" type="email" />
  </div>
</template>
<!-- Utilizzo con v-model multipli -->
<script setup>
import { ref } from 'vue'
import FormUtente from './FormUtente.vue'

const nome = ref('Mario')
const cognome = ref('Rossi')
const email = ref('mario@example.com')
</script>

<template>
  <FormUtente
    v-model:nome="nome"
    v-model:cognome="cognome"
    v-model:email="email"
  />
</template>

Modificatori personalizzati di v-model

<!-- InputMaiuscolo.vue -->
<script setup lang="ts">
const [modello, modificatori] = defineModel<string>({
  set(valore) {
    // Applica il modificatore 'maiuscolo'
    if (modificatori.maiuscolo) {
      return valore?.toUpperCase()
    }
    return valore
  }
})
</script>

<template>
  <input v-model="modello" />
</template>
<!-- Utilizzo con modificatore custom -->
<template>
  <InputMaiuscolo v-model.maiuscolo="testo" />
</template>

Pattern pratici

Componente Select personalizzato

<!-- SelectPersonalizzato.vue -->
<script setup lang="ts">
interface Opzione {
  valore: string | number
  etichetta: string
}

defineProps<{
  opzioni: Opzione[]
  placeholder?: string
}>()

const modello = defineModel<string | number | null>()
</script>

<template>
  <select v-model="modello" class="select-custom">
    <option v-if="placeholder" :value="null" disabled>
      {{ placeholder }}
    </option>
    <option
      v-for="opzione in opzioni"
      :key="opzione.valore"
      :value="opzione.valore"
    >
      {{ opzione.etichetta }}
    </option>
  </select>
</template>

Componente con emit asincrono

<!-- FormConferma.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const emit = defineEmits<{
  conferma: [dati: { nome: string; email: string }]
  annulla: []
}>()

const isInvio = ref(false)
const nome = ref('')
const email = ref('')

async function inviaForm() {
  isInvio.value = true
  try {
    // Simula validazione asincrona
    await new Promise(resolve => setTimeout(resolve, 500))
    emit('conferma', { nome: nome.value, email: email.value })
  } finally {
    isInvio.value = false
  }
}
</script>

<template>
  <form @submit.prevent="inviaForm">
    <input v-model="nome" placeholder="Nome" required />
    <input v-model="email" placeholder="Email" type="email" required />
    <button type="submit" :disabled="isInvio">
      {{ isInvio ? 'Invio...' : 'Conferma' }}
    </button>
    <button type="button" @click="emit('annulla')">Annulla</button>
  </form>
</template>

Best practice

  • Usa la sintassi TypeScript per props e emits: Migliore tipo-sicurezza e autocompletamento
  • Le props sono di sola lettura: Non modificarle nel componente figlio
  • Preferisci eventi specifici: Usa nomi descrittivi come aggiornaNome invece di aggiorna
  • Usa defineModel (Vue 3.4+): Semplifica enormemente la gestione di v-model
  • Documenta le props con JSDoc: Aiuta gli altri sviluppatori a capire il componente
  • Valida sempre le props critiche: Usa validator per valori con vincoli specifici