back to top

Novità per la direttiva v-model in Vue 3

Nelle precedenti lezioni abbiamo già incontrato la direttiva v-model quando abbiamo parlato dei form e degli elementi <input> predefiniti. Ricordiamo che v-model consente di implementare in modo estremamente semplice la tecnica che prende il nome di two way data binding e che consiste nella realizzazione di un legame bidirezionale fra il valore di un campo di <input> ed una proprietà di un componente o dello stato dell’intera applicazione.

Detto in altri termini, ogni volta che viene modificato il valore di un campo di input, viene automaticamente aggiornata la proprietà associata e viceversa.

Esempio di uso della direttiva v-model su elementi predefiniti

Rivediamo allora un semplice esempio per illustrare in pratica come utilizzare la direttiva v-model in caso di elementi HTML predefiniti.

<template>
  <input
    type="text"
    v-model="city"
    placeholder="Inserisci il nome di una città..."
  />
  <p>{{ city }}</p>
</template>

<script>
export default {
  data() {
    return {
      city: "",
    };
  },
};
</script>

Nel frammento di codice riportato sopra, ogni volta che digitiamo un carattere nel campo di input, viene immediatamente aggiornato il valore della proprietà city. Al contrario se cambiassimo quest’ultima, le modifiche si rifletterebbero automaticamente sul valore corrente del campo di testo.

La direttiva v-model semplifica questo tipo di associazione bidirezionale fra il valore di un campo di input ed una proprietà. Potremmo avere lo stesso risultato se usassimo la direttiva v-bind:value per associare in modo unidirezionale una proprietà al valore di un campo di input in combinazione con v-on:input per ottenere la sincronizzazione nell’altra direzione, ovvero dall’elemento alla proprietà.

Potremmo quindi riscrivere l’esempio precedente come mostrato sotto.

<template>
  <input
    type="text"
    v-bind:value="city"
    v-on:input="city=$event.target.value"
    placeholder="Inserisci il nome di una città..."
  />
  <p>{{ city }}</p>
</template>

<script>
export default {
  data() {
    return {
      city: "",
    };
  }
};
</script>

La direttiva v-model applicata ai componenti

La direttiva v-model può però essere usata anche sui componenti. Ed è in questo caso che sono state apportate delle modifiche significative in Vue 3 rispetto alla versione precedente.

Infatti, applicare v-model su un componente in Vue 2 è equivalente ad usare v-bind:value, per passare una proprietà value al componente, e gestire contemporaneamente un evento con v-on:input.

Creiamo quindi un nuovo progetto in una cartella separata e usiamo nuovamente Vue 2 al posto di Vue 3.

Realizziamo un nuovo componente che presenta un elemento <input> predefinito nel suo template. Ogni volta che si digita un carattere all’interno di quest’ultimo viene emesso un nuovo evento input passando come payload il valore corrente del campo. Il valore della casella di testo è inoltre associato alla proprietà value che il componente riceve in ingresso.

// esempio usando Vue 2
<template>
  <input
    type="text"
    v-bind:value="value"
    v-on:input="$emit(
      'input',
      $event.target.value
    )"
    placeholder="Inserisci il nome di una città..."
  />
</template>

<script>
export default {
  name: "CustomInputComponent",
  props: {
    value: {
      type: String,
      default: ''
    }
  }
};
</script>

Ricapitolando, nel componente riportato sopra associamo la prop value al valore del campo di input presente nel suo template ed emettiamo un nuovo evento 'input' ogni volta che viene digitato un nuovo carattere nella casella di testo.

Importiamo quindi il componente che chiamiamo CustomInputComponent all’interno di un altro componente, per esempio App e supponiamo di voler poi associare in modo bidirezionale una proprietà city del componente App al valore del campo di testo di CustomInputComponent in modo che siano sincronizzati e cambino simultaneamente.

Per far ciò potremmo usare una proprietà ed un evento personalizzati oppure possiamo sfruttare la direttiva v-model come mostrato nel frammento di codice riportato sotto. Questo perché il componente CustomInputComponent si aspetta di ricevere una proprietà value ed emette un evento input.

<template>
  <div id="app">
    <custom-input-component v-model="city"/>
    <p>
       {{ city }}
    </p>
  </div>
</template>

<script>
import CustomInputComponent from './components/CustomInputComponent.vue'

export default {
  name: 'App',
  data() {
    return {
      city: ''
    }
  },
  components: {
    CustomInputComponent
  }
}
</script>

Sul componente personalizzato abbiamo applicato v-model associando la proprietà city del componente App.

In base a quanto detto in precedenza, il comportamento predefinito che si ottiene applicando la direttiva v-model ad un componente personalizzato in Vue 2 è equivalente ad usare v-bind:value e v-on:input. Per cui avremmo potuto tranquillamente riscrivere il template di App in forma più estesa nel seguente modo.

<template>
  <div id="app">
    <custom-input-component 
      v-bind:value="city" 
      v-on:input="city = $event"
    />
    <p>
       {{ city }}
    </p>
  </div>
</template>

<script>
import CustomInputComponent from './components/CustomInputComponent.vue'

export default {
  name: 'App',
  data() {
    return {
      city: ''
    }
  },
  components: {
    CustomInputComponent
  }
}
</script>

Volendo cambiare il nome della proprietà e dell’evento predefiniti, in Vue 2 dovremmo usare l’opzione model nel componente su cui applichiamo v-model come mostrato sotto.

<template>
    <input
      type="text"
      v-bind:value="city"
      v-on:input="$emit(
        'update:city',
        $event.target.value
      )"
      placeholder="Inserisci il nome di una città..."
    />
</template>

<script>
export default {
  name: 'CustomInputComponent'
  model: {
    prop: 'city',
    event: 'update:city'
  },
  props: {
    city: {
      type: String,
      default: ''
    }
  }
};
</script>

Notiamo che ora CustomInputComponent si aspetta di ricevere in ingresso una proprietà city ed emette un evento 'update:city'.

Il componente App resta invece invariato e il suo template è sempre il seguente.

<template>
  <div id="app">
    <custom-input-component v-model="city"/>
    <p>
       {{ city }}
    </p>
  </div>
</template>

Se volessimo tuttavia sostituire v-model con una combinazione di v-bind e v-on dovremmo ora usare il nome della proprietà e dell’evento personalizzato emesso da CustomInputComponent. Per cui potremmo riscrivere il template di App come segue.

<template>
  <div id="app">
    <custom-input-element 
      v-bind:city="city" 
      v-on:update:city="city = $event"/>
    <p>
       {{ city }}
    </p>
  </div>
</template>

Direttiva v-model sui componenti in Vue 3

In Vue 3, è stato cambiato il comportamento predefinito di v-model. Adesso, applicare v-model ad un componente personalizzato è equivalente a passare la prop modelValue e gestire l’evento update:modelValue con la direttiva v-on.

Possiamo allora riscrivere l’esempio visto in precedenza come segue.

Se usiamo la direttiva v-model, il componente App resta invariato.

// Esempio in Vue 3
  <template>
    <div id="app">
      <custom-input-component v-model="city"/>
      <p>
        {{ city }}
      </p>
    </div>
  </template>

  <script>
  import CustomInputComponent from './components/CustomInputComponent.vue'

  export default {
    name: 'App',
    data() {
      return {
        city: ''
      }
    },
    components: {
      CustomInputComponent
    }
  }
  </script>

E come possiamo notare il template del componente App è uguale a quello visto sopra nell’esempio in cui abbiamo usato v-model in Vue 2.

Ciò che cambia è che adesso v-model è equivalente ad usare v-bind:modelValue insieme a v-on:update:modelValue. Possiamo quindi riscrivere il template del componente App come segue.

<template>
    <div id="app">
      <custom-input-component 
        v-bind:modelValue="city" 
        v-on:update:modelValue="city = $event"/>
      <p>
        {{ city }}
      </p>
    </div>
  </template>

Di conseguenza il componente CustomInputComponent deve essere riscritto in modo da accettare una prop modelValue ed emettere un evento update:modelValue.

<template>
    <input
      type="text"
      v-bind:value="modelValue"
      v-on:input="$emit(
        'update:modelValue',
        $event.target.value
      )"
      placeholder="Inserisci il nome di una città..."
    />
  </template>

  <script>
  export default {
    props: {
      name: "CustomInputComponent"
      modelValue: {
        type: String,
        default: ''
      }
    }
  };
  </script>

In particolare, CustomInputComponent riceve una proprietà modelValue che passa poi all’elemento predefinito <input> tramite v-bind ed emette un evento personalizzato update:modelValue ogni volta che viene inserito un nuovo carattere nel campo di testo.

Anche in Vue 3 è possibile cambiare il nome predefinito della prop che riceve un componente quando viene applicata la direttiva v-model e per farlo non dovremo più usare l’opzione model vista in Vue 2.

Al contrario, v-model può finalmente ricevere un argomento in Vue 3.

La sintassi da applicare è v-model:nomeProp="propDaAssociare".

In cui con l’argomento nomeProp indichiamo il nome della proprietà attesa dal componente al posto di modelValue, mentre propDaAssociare è sempre il valore passato a v-model e si riferisce alla proprietà del componente padre per la quale vogliamo creare un collegamento bidirezionale con una certa proprietà del componente figlio.

Per esempio, volendo replicare l’esempio visto in Vue 2 in cui passiamo al componente CustomInputComponent una prop city, basterà spostarci nel file App.vue e aggiornare il template del componente come segue.

<template>
  <div id="app">
    <custom-input-component v-model:city="city"/>
    <p>
      {{ city }}
    </p>
  </div>
</template>

Notate che alla direttiva v-model indichiamo come argomento (v-model:city) il nome della prop da passare a CustomInputComponent mentre continuiamo a passare fra doppie virgolette il nome della proprietà del componente App che vogliamo sincronizzare in modo bidirezionale.

Possiamo riscrivere il template di App in modo del tutto equivalente nel seguente modo.

<template>
  <div id="app">
    <custom-input-component 
      v-bind:city="city" 
      v-on:update:city="city = $event"
    />
    <p>
      {{ city }}
    </p>
  </div>
</template>

Prima di proseguire e vedere come modificare il componente CustomInputComponent, è bene fare due precisazioni.

Tramite argomento (v-model:propName) specifichiamo il nome della prop che verrà passata al componente su cui è applicata v-model. A tale proprietà viene automaticamente associato l’evento update:propName. Nel nostro esempio abbiamo usato come argomento city (v-model:city), così facendo il componente aspetterà una prop di nome 'city' e dovrà emettere un evento 'update:city'.

È inoltre importante evidenziare che è solo un caso se l’argomento di v-model e il suo valore hanno lo stesso nome, ma si riferiscono a due entità diverse. L’argomento di v-model (v-model:city) indica quale prop riceve in ingresso il componente CustomInputComponent, il valore di v-model, specificato fra virgolette, indica quale proprietà di App deve essere associata in modo bidirezionale.

Volendo, per non fare confusione, potremmo riscrivere il componente App come segue, rinominando il nome della proprietà del componente App.

<template>
  <div id="app">
    <custom-input-component v-model:city="bestCity"/>
    <p>
      {{ bestCity }}
    </p>
  </div>
</template>

<script>
import CustomInputComponent from './components/CustomInputComponent.vue'

export default {
  name: 'App',
  data() {
    return {
      bestCity: ''
    }
  },
  components: {
    CustomInputComponent
  }
}
</script>

Non ci resta quindi che aggiornare il componente CustomInputComponent in modo da assegnare il valore della prop city ricevuta in ingresso al valore del campo di testo. Faremo quindi in modo di emettere un evento personalizzato update:city ogni volta che viene immesso un nuovo carattere.

<template>
  <input
    type="text"
    v-bind:value="city"
    v-on:input="$emit('update:city', $event.target.value)"
    placeholder="Inserisci il nome di una città..."
  />
</template>

<script>
export default {
  name: "CustomInputComponent",
  props: {
    city: {
      type: String,
      default: "",
    },
  },
  emits: ['update:city']
};
</script>

Abbiamo anche usato l’opzione emits introdotta in Vue 3 per segnalare che il componente emette un evento personalizzato.

Associazioni multiple con v-model in Vue 3

L’introduzione degli argomenti per la direttiva v-model apre le porte a nuove opportunità. Ora è infatti possibile avere associazioni multiple, applicando più volte v-model ed usando degli argomenti diversi.

Supponiamo per esempio di aggiungere un nuovo campo di testo al componente CustomInputComponent come mostrato sotto.

<template>
    <input
      type="text"
      v-bind:value="country"
      v-on:input="$emit('update:country', $event.target.value)"
      placeholder="Inserisci il nome di una nazione..."
    />
    <input
      type="text"
      v-bind:value="city"
      v-on:input="$emit('update:city', $event.target.value)"
      placeholder="Inserisci il nome di una città..."
    />
  </template>

  <script>
  export default {
    name: "CustomInputComponent",
    props: {
      city: {
        type: String,
        default: "",
      },
      country: {
        type: String,
        default: "",
      },
    },
    emits: ['update:city', 'update:country']
  };
  </script>

CustomInputComponent aspetta ora di ricevere in ingresso un’altra prop country ed emette un evento personalizzato update:country.

Nel componente App possiamo ora utilizzare la direttiva v-model due volte usando due diversi argomenti ed associando due distinte proprietà del componente App.

<template>
  <div id="app">
    <custom-input-component
      v-model:country="bestCountry"
      v-model:city="bestCity"
    />
    <p>{{ bestCountry }} - {{ bestCity }}</p>
  </div>
</template>

<script>
import CustomInputComponent from "./components/CustomInputComponent.vue";

export default {
  name: "App",
  data() {
    return {
      bestCountry: "",
      bestCity: "",
    };
  },
  components: {
    CustomInputComponent,
  },
};
</script>

Così facendo, ogni volta che viene digitato un carattere in uno dei due campi, viene aggiornata automaticamente la rispettiva proprietà di App che abbiamo associato al valore del campo stesso.

Modificatori personalizzati per la direttiva v-model in Vue 3

Per concludere questa lezione, illustriamo un’altra utile funzione aggiunta in Vue 3 per la direttiva v-model.

Ricordiamo infatti che a tale direttiva è possibile applicare dei modificatori predefiniti come .trim che ne cambiano il comportamento. Nel caso di .trim vengono rimossi eventuali spazi bianchi iniziali e finali.

Per applicare uno o più modificatori basta usare la sintassi v-model.modificatore1.modificatore2.

In Vue 3 è ora possibile creare dei modificatori personalizzati.

I modificatori aggiunti a v-model sono resi disponibili nel componente su cui è stata applicata la direttiva tramite la prop modelModifiers la quale è un oggetto.

Se a v-model passiamo un argomento (v-model:argomento), i modificatori saranno invece disponibili all’interno di un oggetto "argomentoModifiers" al posto di modelModifiers.

Riprendiamo allora il nostro esempio e supponiamo di applicare un modificatore personalizzato .uppercase alla direttiva v-model:city assegnata al componente CustomInputComponent nel template di App.

<template>
    <div id="app">
      <custom-input-component
        v-model:city.uppercase="bestCity"
      />
      <p>{{ bestCity }}</p>
    </div>
  </template>

  <script>
  import CustomInputComponent from "./components/CustomInputComponent.vue";

  export default {
    name: "App",
    data() {
      return {
        bestCity: "",
      };
    },
    components: {
      CustomInputComponent,
    },
  };
  </script>

A questo punto CustomInputComponent dovrà avere almeno le due props city e cityModifiers. La prima è necessaria in quanto abbiamo passato ‘city’ come argomento a v-model, la seconda è un oggetto che permette di verificare se è stato usato un modificatore. In tal caso cityModifiers sarà un oggetto in cui il nome delle proprietà coincide con quello dei modificatori e il loro valore è pari a true.

Nel nostro esempio cityModifiers avrà la seguente struttura.

{ uppercase: true }

Vediamo allora come modificare CustomInputComponent e fare in modo che, in presenza del modificatore .uppercase, venga sempre emessa una stringa costituita da sole lettere maiuscole.

<template>
    <input
      type="text"
      v-bind:value="city"
      v-on:input="onInput"
      placeholder="Inserisci il nome di una città..."
    />
  </template>

  <script>
  export default {
    name: "CustomInputComponent",
    props: {
      city: {
        type: String,
        default: "",
      },
      cityModifiers: {
        type: Object,
        default: () => ({})
      }
    },
    emits: ['update:city'],
    methods: {
      onInput(event) {
        let value = event.target.value;
        if (this.cityModifiers.uppercase) {
          value = value.toUpperCase(); 
        }
        this.$emit('update:city', value);
      }
    },
    created() {
      console.log(this.cityModifiers) // { uppercase: true }
    }
  };
  </script>

CustomInputComponent presenta un campo di input il cui valore è associato in modo bidirezionale con il valore della prop city. Ogni volta che viene digitato un nuovo carattere al suo interno, viene invocato il metodo onInput(event) a cui viene passato un oggetto che rappresenta l’evento input. In questo metodo estraiamo il valore corrente del campo di testo e verifichiamo se è stato applicato il modificatore .uppercase alla direttiva v-model. In tal caso trasformiamo i caratteri del campo di testo in sole lettere maiuscole. Infine emettiamo un evento 'update:city' (perché è stato usato ‘city’ come argomento di v-model) con il valore aggiornato.

Nel definire il componente CustomInputComponent abbiamo indicato quali props devono essere passate e qual è il loro valore predefinito. Nel caso di cityModifiers si tratta di un oggetto e per questo motivo abbiamo usato come valore di default una funzione che restituisce l’oggetto predefinito, come indicato nella documentazione ufficiale. Nel caso specifico abbiamo usato una funzione a freccia introdotta in ES2015 (la funzione () => ({}) è equivalente a function() { return {};}. Le parentesi tonde che racchiudono le graffe sono necessarie e servono ad indicare che si sta restituendo un oggetto vuoto in forma letterale. In questo modo le parentesi graffe non vengono interpretate come delimitatori di un blocco di istruzioni).

Riepilogo

In questa lezione abbiamo illustrato come cambia la direttiva v-model in Vue 3 quando viene applicata a componenti personalizzati. Abbiamo inoltre visto che nell’ultima versione di Vue è ora possibile applicare più volte la direttiva v-model ad un componente consentendo associazioni multiple. Abbiamo infine mostrato in che modo creare dei modificatori personalizzati per la direttiva v-model.

Pubblicitร