back to top

Vue.js – le proprietà $attrs, $listeners e i componenti multi radice

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.

Pubblicitร