In questa lezione parleremo di due particolari proprietà dell’oggetto data
presenti in Vue 2 e Vue 3 il cui comportamento è stato modificato nell’ultima versione del framework. Stiamo parlando di $attrs
e $listeners
.
Le speciali proprietà $attr e $listeners in Vue 2 e Vue 3
In Vue 2, nel definire un componente tramite un oggetto di opzioni, specifichiamo quali sono le proprietà (props) che il componente aspetta di ricevere in ingresso.
export default {
name: 'CustomInput',
props: [
// elenco delle proprietà attese dal componente
]
}
Eventuali altri attributi (non presenti in props), che vengono applicati sul componente, sono accessibili (nel componente stesso) tramite la proprietà speciale $attrs
ad eccezione di eventuali attributi class
e style
usati per specificare delle classi CSS e degli stili.
Vediamo subito un semplice esempio ed immaginiamo di avere un componente CustomInput
definito come sotto.
<template>
<div>
<label for="input">Nome</label>
<input id="input" type="text"/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: []
}
</script>
Importiamo poi CustomInput
in un altro componente App
, passiamo una serie di attributi e registriamo esplicitamente due funzioni per gli eventi focus
e blur
.
<template>
<div id="app">
<CustomInput
placeholder="Inserisci il tuo username"
:name="name"
:label="label"
v-model="username"
@focus="onFocus"
@blur="onBlur"
class="custom-input"
style="font-size: 2rem"
/>
</div>
</template>
<script>
import CustomInput from "./components/CustomInput.vue";
export default {
name: "App",
components: {
CustomInput,
},
data() {
return {
name: 'username',
username: '',
label: 'Seleziona il tuo username'
}
},
methods: {
onFocus() {},
onBlur() {}
}
};
</script>
Nell’esempio riportato sopra, abbiamo anche impiegato la direttiva v-model
che in Vue 2 è equivalente alla combinazione di v-bind:value
e v-on:input
.
Se lanciamo l’applicazione ed ispezioniamo il componente CustomInput
tramite gli strumenti per sviluppatori ed in particolare l’estensione Vue Dev Tools, notiamo che i vari attributi ricevuti da CustomInput
sono disponibili nell’oggetto speciale this.$attrs
che contiene le proprietà riportate sotto.
{
placeholder: "Inserisci il tuo username",
name: "username",
label: "Seleziona il tuo username",
// 'value' è presente perché abbiamo usato v-model
value: ""
}
Dal momento che l’array props
di CustomInput
è vuoto, tutti gli attributi passati finiscono in $attrs
ad eccezione di class
e style
che sono invece accessibili in due omonime e distinte proprietà.
Al contrario per gli eventi focus
, blur
ed input
(ricordiamo che abbiamo usato v-model
) verrà creato un oggetto che all’interno di CustomInput
possiamo accedere tramite this.$listeners
.
{
blur() {
// ...
},
focus() {
// ...
},
input() {
// ...
}
}
Sempre negli strumenti per sviluppatori notiamo che gli attributi elencati sopra, così come style
e class
, vengono automaticamente applicati all’elemento <div>
radice del template di CustomInput
. Gli elementi HTML generati saranno simili a quelli riportati nel frammento di codice sottostante.
<div
placeholder="Inserisci il tuo username"
name="username"
label="Seleziona il tuo username"
value="" class="custom-input"
style="font-size: 2rem;">
<label for="input">Nome</label>
<input id="input" type="text">
</div>
Possiamo però cambiare questa strategia predefinita ed applicare poi gli attributi all’elemento del template più appropriato. Per far ciò andremo ad usare l’opzione inheritAttrs: false
.
Spostiamoci allora nella sezione script del componente CustomInput
e modifichiamolo come riportato di seguito.
<template>
<div>
<label for="input">Nome</label>
<input id="input" type="text"/>
</div>
</template>
<script>
export default {
// aggiungiamo una nuova opzione per non assegnare
// gli attributi automaticamente all'elemento base
inheritAttrs: false,
name: 'CustomInput',
props: []
}
</script>
Grazie a inheritAttrs: false
l’elemento <div>
radice non riceve più tutti gli attributi passati a CustomInput
, ma solo style
e class
. Questo comportamento vedremo a breve che è stato modificato in Vue 3.
<div
class="custom-input"
style="font-size: 2rem;"
>
<label for="input">Nome</label>
<input id="input" type="text">
</div>
Per applicare gli altri attributi dovremo invece prelevarli da $attrs
. Stesso discorso vale per le funzioni impiegate per la gestione degli eventi e presenti in $listeners
.
Dal momento che sia v-on
che v-bind
accettano come valore un oggetto, potremmo modificare il template di CustomInput
come riportato sotto.
<template>
<div>
<label for="input">{{ label }}</label>
<input
v-bind="$attrs"
v-on="{
...$listeners,
input: onInput
}"
id="input"
type="text" />
</div>
</template>
<script>
export default {
inheritAttrs: false,
name: "CustomInput",
props: [
'label',
],
methods: {
onInput(event) {
this.$emit('input', event.target.value);
}
}
};
</script>
Nel template del componente CustomInput
è presente un elemento <input>
sul quale applichiamo tutti gli attributi ricevuti assegnando l’oggetto $attrs
a v-bind
. Registriamo inoltre tutti i gestori di eventi presenti in $listeners
, ma sovrascriviamo il gestore per l’evento input (Abbiamo assegnato a v-on
un oggetto e usato l’operatore … – spread operator – per espandere le proprietà dell’oggetto $listeners
nel nuovo oggetto. Appendendo poi la proprietà input
, abbiamo sovrascritto il gestore di eventi omonimo già presente in $listeners
).
$attrs e $listeners in Vue 3
In Vue 3 $listeners
è stato deprecato, ora tutti gli attributi, compresi class
e style
, e i gestori di eventi, che non hanno una corrispondente proprietà nelle opzioni props
e events
di un componente, sono accessibili tramite la proprietà $attrs
.
Riformuliamo allora l’esempio visto sopra in Vue 2, usando questa volta Vue 3. Partiamo dal componente App
che resta invariato.
<template>
<div id="app">
<CustomInput
placeholder="Inserisci il tuo username"
:name="name"
:label="label"
@focus="onFocus"
@blur="onBlur"
v-model="username"
class="custom-input"
style="font-size: 2rem"
/>
<p v-if="username">
Hai scelto <strong>{{ username }}</strong> come username
</p>
</div>
</template>
<script>
import CustomInput from "./components/CustomInput.vue";
export default {
name: "App",
components: {
CustomInput,
},
data() {
return {
name: 'username',
username: '',
label: 'Seleziona il tuo username'
}
},
methods: {
onFocus() {
console.log('event: focus')
},
onBlur() {
console.log('event: blur')
}
}
};
</script>
Nel template del componente App
passiamo a CustomInput
le stesse proprietà e attributi di prima. Applichiamo la direttiva v-model
che però in Vue 3 ha subito delle modifiche e, come abbiamo visto nella precedente lezione, è equivalente all’uso contemporaneo di v-bind:modelValue
e v-on:update:modelValue
.
Il componente CustomInput
, che contiene solo label
fra le sue props
, riceverà un oggetto $attrs
come quello riportato sotto.
{
placeholder: "Inserisci il tuo username",
name: "username",
modelValue: "d",
class: "custom-input",
style: {
font-size: "2rem"
},
onFocus() {},
onBlur() {},
'onUpdate:modelValue': () => {}
}
Notiamo che per convenzione i nomi degli eventi sono ora nella forma onEventName
.
Visto che l’oggetto $attrs
in Vue 3 contiene tutti gli attributi e i gestori di eventi, possiamo applicarli all’elemento <input>
del template di CustomInput
tramite la direttiva v-bind
.
<template>
<div>
<label for="input">{{ label }}</label>
<input
v-bind="{
...$attrs,
onInput
}"
id="input"
type="text" />
</div>
</template>
<script>
export default {
inheritAttrs: false,
name: "CustomInput",
props: [
'label'
],
methods: {
onInput(event) {
this.$emit('update:modelValue', event.target.value);
}
}
};
</script>
Nell’esempio assegniamo un nuovo oggetto come valore di v-bind
. Tale oggetto contiene tutte le proprietà di $attrs
a parte onInput
che sostituiamo con il metodo omonimo del componente.
Abbiamo mantenuto l’opzione inheritAttrs: false
perché in caso contrario Vue applicherebbe tutti gli attributi all’elemento radice.
Componenti multi radice in Vue 3
A partire da Vue 3, il template di un componente può però avere più di un elemento radice e non è più necessario usare un elemento superfluo che racchiuda altri elementi discendenti, se non è strettamente necessario.
Possiamo quindi pensare di ristrutturare il template di CustomInput
rimuovendo l’elemento <div>
base. Così facendo otteniamo un altro vantaggio. Quando è presente più di un elemento radice, l’opzione inheritAttrs: false
non è più necessaria dato che Vue non potrà più applicare automaticamente ad un solo elemento radice. Dovremo però occuparci di gestire opportunamente l’oggetto $attrs
altrimenti riceveremo un messaggio d’errore.
<template>
<!-- in Vue 3 non è più necessario avere -->
<!-- un solo elemento radice -->
<label for="input">{{ label }}</label>
<input
v-bind="{
...$attrs,
onInput
}"
id="input"
type="text" />
</template>
<script>
export default {
// inheritAttrs: false, NON è più necessario
// nel caso di componenti il cui template presenta più di un
// elemento radice
name: "CustomInput",
props: [
'label'
],
methods: {
onInput(event) {
this.$emit('update:modelValue', event.target.value);
}
}
};
</script>
<style></style>
Riepilogo
In questa lezione abbiamo parlato degli oggetti speciali $attrs
e $listeners
usati in Vue 2 per accedere agli attributi e ai gestori di eventi passati ad un componente e non dichiarati tra le sue props. In Vue 3 $listeners
è stato deprecato e tutti gli attributi, compresi class
e style
, e i gestori di eventi non dichiarati nelle opzioni props
e emits
, sono accessibili tramite $attrs
.