Kurs Vue.js · Moduł 7: Props i zdarzenia
v-model na Komponentach
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:
- Przyjąć prop
modelValue - 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. Czym jest v-model na custom komponencie?
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.