Kurs Vue.js · Moduł 7: Props i zdarzenia
Custom Events
W tej lekcji4
W Stacji Komunikacji sygnał nie idzie tylko w jedną stronę! Gdy Mars odbierze dane, wysyła potwierdzenie z powrotem na Ziemię. W Vue dziecko komunikuje się z rodzicem przez custom events - sygnały emitowane w górę drzewa komponentów.
Props płyną wyłącznie w dół, więc przycisk w module nadawczym nie może sam zmienić stanu centrali. Operator klika "Wyślij" i co dalej? Przycisk nie jest właścicielem dziennika łączności, trzyma go rodzic. Potrzebny jest kanał zwrotny: dziecko ogłasza, że coś się stało, a rodzic decyduje, co z tym zrobić.
defineEmits
W <script setup> deklarujesz emitowane zdarzenia makrem defineEmits. Tak jak defineProps, nie wymaga ono importu. Zwraca funkcję emit: jej pierwszym argumentem jest nazwa zdarzenia, a kolejnymi dane, które chcesz wysłać, czyli payload:
1<!-- SignalButton.vue (dziecko) -->
2<script setup>
3const emit = defineEmits(['signal-sent', 'error'])
4
5const sendSignal = () => {
6 emit('signal-sent', { message: 'OK', time: Date.now() })
7}
8
9const reportError = (code) => {
10 emit('error', code)
11}
12</script>
13
14<template>
15 <button @click="sendSignal">Send</button>
16</template>Lista w defineEmits działa jak rejestr kanałów modułu: od razu widać, jakie sygnały komponent potrafi nadać. Jeśli wyemitujesz zdarzenie spoza listy, Vue w trybie deweloperskim ostrzeże Cię w konsoli. Samo wywołanie emit niczego nie zmienia w dziecku - tylko nadaje komunikat.
Nasłuchiwanie w rodzicu
Rodzic nasłuchuje zdarzeń przez @event-name lub v-on:event-name, dokładnie tak jak kliknięć w zwykłe elementy HTML:
1<!-- Parent.vue -->
2<template>
3 <SignalButton
4 @signal-sent="handleSignal"
5 @error="handleError"
6 />
7</template>
8
9<script setup>
10const handleSignal = (data) => {
11 console.log('Signal received:', data.message)
12}
13
14const handleError = (code) => {
15 console.error('Error:', code)
16}
17</script>Handler dostaje payload jako swoje argumenty, dlatego handleSignal może odczytać data.message. Vue dopasowuje przy tym zapis nazw: zdarzenie wyemitowane jako signalSent złapiesz przez @signal-sent. Dokumentacja poleca camelCase w emit i kebab-case w szablonie rodzica, ale kebab-case po obu stronach, jak w tym przykładzie, też działa. Najważniejsza różnica względem zdarzeń DOM: zdarzenia komponentów nie wędrują w górę drzewa. Słyszy je tylko bezpośredni rodzic, a komponent dwa piętra wyżej nie dostanie sygnału, chyba że rodzic przekaże go dalej.
Event Payload
Emit może przekazywać dowolne dane jako payload - pojedynczą wartość, kilka argumentów albo cały obiekt:
1<script setup>
2const emit = defineEmits(['submit'])
3
4const onSubmit = () => {
5 // Jeden argument
6 emit('submit', 'data')
7
8 // Wiele argumentów
9 emit('submit', name, email, role)
10
11 // Obiekt
12 emit('submit', { name, email, role })
13}
14</script>Wszystkie argumenty po nazwie zdarzenia trafiają do handlera w tej samej kolejności. Polecam trzecią formę, czyli jeden obiekt: łatwiej dopisać do niego nowe pole, a rodzic nie musi pamiętać, który argument był który.
Walidacja Eventów
Obiektowa składnia defineEmits pozwala sprawdzić payload przed wysłaniem. Kluczem jest nazwa zdarzenia, a wartością null (bez walidacji) albo funkcja, która dostaje argumenty emit i zwraca true lub false:
1<script setup>
2const emit = defineEmits({
3 // Bez walidacji
4 click: null,
5
6 // Z walidacją
7 submit: (payload) => {
8 if (!payload.email) return false
9 if (!payload.email.includes('@')) return false
10 return true
11 }
12})
13</script>Tak jak przy props, nieudana walidacja tylko ostrzega w konsoli w trybie deweloperskim - zdarzenie i tak zostanie wysłane, więc rodzic nadal powinien być gotowy na złe dane. W tym przykładzie kryje się też pułapka: linia click: null deklaruje nazwę zdarzenia natywnego. Od tej chwili nasłuchiwacz @click w rodzicu reaguje wyłącznie na zdarzenia wyemitowane przez komponent, a nie na zwykłe kliknięcia w jego główny element.
Moja rada: deklaruj każde zdarzenie, nawet gdy nie potrzebujesz walidacji. Rejestr kanałów to najkrótsza dokumentacja modułu, a ostrzeżenia o niezadeklarowanych sygnałach wyłapią literówki w nazwach. W edytorze poniżej dwa przyciski nadają ten sam sygnał, a rodzic zbiera je w jednym dzienniku. Props w dół i zdarzenia w górę tworzą razem pełny obieg danych - w następnej lekcji zobaczysz, jak Vue skleja te dwa kanały w jedną dyrektywę v-model.
Zapamiętaj: props to rozkaz z Ziemi, a emit to potwierdzenie z Marsa - dwa kanały, każdy w swoją stronę.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3import SignalButton from './SignalButton.vue'
4
5const messages = ref([])
6
7const handleSignal = (data) => {
8 messages.value.push(data)
9}
10</script>
11
12<template>
13 <div class="nova-lab">
14 <h1>Demo: custom events</h1>
15
16 <SignalButton
17 label="Wyślij sygnał Alfa"
18 @signal-sent="handleSignal"
19 />
20 <SignalButton
21 label="Wyślij sygnał Beta"
22 @signal-sent="handleSignal"
23 />
24
25 <div class="log">
26 <h3>Odebrane sygnały:</h3>
27 <p v-for="(msg, i) in messages" :key="i">
28 {{ msg.timestamp }} - {{ msg.label }}: {{ msg.message }}
29 </p>
30 <p v-if="!messages.length" class="empty">Nie odebrano jeszcze żadnego sygnału...</p>
31 </div>
32 </div>
33</template>
34
35<style scoped>
36.nova-lab {
37 background: #0a0e27;
38 color: #00ff88;
39 min-height: 100vh;
40 padding: 20px;
41 font-family: monospace;
42}
43h1 { color: #00ff88; margin-bottom: 20px; }
44.log {
45 margin-top: 20px;
46 padding: 15px;
47 background: rgba(0, 180, 216, 0.1);
48 border: 2px solid #00b4d8;
49}
50.log h3 { color: #00b4d8; margin-bottom: 10px; }
51.empty { color: #666; font-style: italic; }
52</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. Co to są custom events w Vue?
2. Jak wyemitować event w <script setup>?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz komponent emitujący event 'signal-received' z payload po kliknięciu przycisku.
- Układanie w pionie
Uporządkuj kroki przepływu custom event:
- Klikanie w kolejności
Ułóż kod emitowania eventu z danymi:
- Edytor kodu
Stwórz komponent z defineEmits z walidacją payloadu (obiektowa składnia).