Kurs Vue.js · Moduł 7: Props i zdarzenia

v-model na Komponentach

4 min czytania
W tej lekcji5

Formularz nadawczy stacji ma pole "Treść komunikatu". Rodzic trzyma treść w ref, a samo pole siedzi w komponencie MarsInput. Props przekażą wartość w dół, zdarzenie odeśle zmianę w górę - ale pisanie tej pary przy każdym polu formularza szybko się nudzi. Na szczęście v-model działa też na custom komponentach - to jak dwukierunkowa synchronizacja danych między Ziemią a Marsem!

Na natywnych elementach HTML v-model jest skrótem, który zależy od elementu: pole tekstowe <input> i <textarea> łączą value ze zdarzeniem input, pola wyboru i przyciski radiowe łączą checked ze zdarzeniem change, a <select> łączy value ze zdarzeniem change. Na komponencie reguła jest jedna i to Ty ją wypełniasz.

Jak działa v-model na komponencie?

Kompilator Vue rozwija v-model na komponencie do propsa modelValue i nasłuchiwacza zdarzenia update:modelValue. Oba zapisy poniżej robią dokładnie to samo:

1<!-- Rodzic używa v-model -->
2<MarsInput v-model="message" />
3
4<!-- Jest równoważne z: -->
5<MarsInput
6  :modelValue="message"
7  @update:modelValue="message = $event"
8/>

W drugiej wersji $event to pierwszy argument wysłany przez dziecko, czyli nowa treść pola. Rodzic przypisuje ją do message, więc właścicielem danych nadal pozostaje rodzic.

Implementacja w komponencie

Komponent musi:

  1. Przyjąć prop modelValue
  2. Emitować event update:modelValue

W praktyce oba punkty mieszczą się w kilku linijkach:

1<!-- MarsInput.vue -->
2<script setup>
3defineProps(['modelValue'])
4const emit = defineEmits(['update:modelValue'])
5</script>
6
7<template>
8  <input
9    :value="modelValue"
10    @input="emit('update:modelValue', $event.target.value)"
11  />
12</template>

Rodzic nie zmienił się ani o znak - wciąż pisze <MarsInput v-model="message" />. Całą pracę wykonuje dziecko: wyświetla modelValue i przy każdym wpisanym znaku wysyła zdarzenie z nową treścią. Samo nie nadpisuje propsa, zgodnie z zasadą jednokierunkowego przepływu - tylko prosi rodzica o zmianę.

defineModel - krótszy zapis (Vue 3.4+)

Od Vue 3.4 ten sam komponent napiszesz krócej dzięki makru defineModel. Zwraca ono ref: odczyt daje wartość propsa modelValue, a zapis emituje update:modelValue. Dlatego wewnątrz wystarczy zwykły v-model na polu <input>:

1<!-- MarsInput.vue -->
2<script setup>
3const model = defineModel()
4</script>
5
6<template>
7  <input v-model="model" />
8</template>

Pod spodem działa ten sam mechanizm - prop i zdarzenie - więc rodzic nie widzi żadnej różnicy. Dokumentacja Vue zaleca defineModel w nowym kodzie, a wersję ręczną warto znać, bo spotkasz ją w starszych projektach i bibliotekach.

Multiple v-model

Vue 3 pozwala na wiele v-model na jednym komponencie. Argument po dwukropku zmienia nazwę pary: v-model:first-name łączy się z propsem firstName i zdarzeniem update:firstName:

1<!-- Rodzic -->
2<template>
3  <CrewForm
4    v-model:first-name="firstName"
5    v-model:last-name="lastName"
6  />
7</template>
8
9<!-- CrewForm.vue -->
10<script setup>
11defineProps(['firstName', 'lastName'])
12const emit = defineEmits(['update:firstName', 'update:lastName'])
13</script>
14
15<template>
16  <input
17    :value="firstName"
18    @input="emit('update:firstName', $event.target.value)"
19  />
20  <input
21    :value="lastName"
22    @input="emit('update:lastName', $event.target.value)"
23  />
24</template>

W szablonie rodzica argument możesz zapisać w kebab-case albo w camelCase, jako v-model:firstName - w plikach .vue obie formy działają. Z defineModel wystarczy podać nazwę: const firstName = defineModel('firstName').

v-model Modifiers

Możesz tworzyć własne modyfikatory. Użyte przy v-model trafiają do dziecka w propsie modelModifiers jako obiekt, na przykład { uppercase: true }:

1<!-- Rodzic -->
2<MarsInput v-model.uppercase="text" />
3
4<!-- MarsInput.vue -->
5<script setup>
6const props = defineProps({
7  modelValue: String,
8  modelModifiers: { default: () => ({}) }
9})
10const emit = defineEmits(['update:modelValue'])
11
12function onInput(e) {
13  let value = e.target.value
14  if (props.modelModifiers.uppercase) {
15    value = value.toUpperCase()
16  }
17  emit('update:modelValue', value)
18}
19</script>
20
21<template>
22  <input :value="modelValue" @input="onInput" />
23</template>

Komponent sprawdza flagę i przekształca tekst przed wysłaniem, więc rodzic dostaje już wielkie litery. Dla nazwanego v-model:first-name prop z modyfikatorami nazywa się firstNameModifiers. Z defineModel modyfikatory odczytasz jako drugi element zwróconej pary, a przekształcenie wpiszesz w opcję set:

1<script setup>
2const [model, modifiers] = defineModel({
3  set(value) {
4    return modifiers.uppercase ? value.toUpperCase() : value
5  }
6})
7</script>

Funkcja set dostaje wartość zapisywaną do modelu i zwraca to, co ma trafić do rodzica. Rodzic niczego nie zmienia - nadal pisze <MarsInput v-model.uppercase="text" />.

Moja rada: w nowych komponentach sięgaj po defineModel, bo to mniej kodu i mniej miejsc na literówkę w nazwie zdarzenia. W edytorze poniżej MarsInput i CrewForm używają wersji ręcznej - spróbuj przepisać je na defineModel. Pamiętaj tylko, że v-model łączy dwa sąsiednie poziomy; w następnej lekcji prześlesz dane pięć pięter w dół bez przekazywania ich przez każde piętro.

Zapamiętaj: v-model na komponencie to para prop i zdarzenie, spięta w jeden kanał dupleksowy.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsInput from './MarsInput.vue'
4import CrewForm from './CrewForm.vue'
5
6const message = ref('Pozdrowienia z Marsa!')
7const firstName = ref('Nova')
8const lastName = ref('Komandor')
9</script>
10
11<template>
12  <div class="nova-lab">
13    <h1>v-model na komponentach</h1>
14
15    <h2>Pojedynczy v-model:</h2>
16    <MarsInput v-model="message" />
17    <p>Wartość w rodzicu: {{ message }}</p>
18
19    <h2>Wiele v-model:</h2>
20    <CrewForm
21      v-model:first-name="firstName"
22      v-model:last-name="lastName"
23    />
24    <p>Imię i nazwisko: {{ firstName }} {{ lastName }}</p>
25  </div>
26</template>
27
28<style scoped>
29.nova-lab {
30  background: #0a0e27;
31  color: #00ff88;
32  min-height: 100vh;
33  padding: 20px;
34  font-family: monospace;
35}
36h1 { color: #00ff88; margin-bottom: 20px; }
37h2 { color: #00b4d8; margin: 15px 0 10px; }
38p { margin: 8px 0; }
39</style>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym jest v-model na custom komponencie?

  2. 2. Co trzeba zrobić w komponencie, aby wspierał v-model?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz MarsInput z pełnym wsparciem v-model (modelValue + update:modelValue).

  • Układanie w pionie

    Ułóż kroki implementacji v-model w komponencie:

  • Edytor kodu

    Stwórz komponent z dwoma v-model: v-model:firstName i v-model:lastName.

Przydatne artykuły