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
aggiornaNomeinvece diaggiorna - 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