Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM

Custom Events i dispatchEvent w JavaScript

6 min czytania ·
W tej lekcji9

"Posłuchaj uważnie," mówi Dr. Alan Grant, pochylając się nad konsolą systemu monitoringu. "Dinozaury komunikują się ze sobą za pomocą dźwięków - od niskich pomruków po przerażliwe ryki. W naszym parku potrzebujemy podobnego systemu komunikacji między modułami. I tu właśnie wkraczają Custom Events!"

Czym są Custom Events?

Do tej pory poznaliśmy wbudowane zdarzenia DOM - click, keydown, submit czy scroll. Ale JavaScript pozwala nam tworzyć własne, niestandardowe zdarzenia. Dzięki nim możemy budować systemy komunikacji między różnymi częściami aplikacji, podobnie jak system alarmowy w Parku Jurajskim wysyła powiadomienia do różnych stref.

Custom Events to zdarzenia definiowane przez programistę, które mogą przenosić dowolne dane i być nasłuchiwane tak samo jak zwykłe zdarzenia DOM.

Konstruktor CustomEvent

Aby stworzyć własne zdarzenie, używamy konstruktora CustomEvent:

// Podstawowe tworzenie Custom Event
const alertEvent = new CustomEvent('dinoAlert');

// Custom Event z dodatkowymi danymi (detail)
const escapeEvent = new CustomEvent('dinoEscape', {
  detail: {
    dinosaur: 'Velociraptor',
    zone: 'B',
    severity: 'critical'
  }
});

// Dostęp do danych zdarzenia
console.log(escapeEvent.detail.dinosaur); // "Velociraptor"
console.log(escapeEvent.detail.severity); // "critical"
console.log(escapeEvent.type);            // "dinoEscape"

Obiekt detail to specjalne pole Custom Events, w którym możemy przekazać dowolne dane. To standardowe miejsce na dane w CustomEvent. Można też napisać własną klasę dziedziczącą po Event z dodatkowymi polami, ale zwykle detail w zupełności wystarcza.

Opcje CustomEvent

Konstruktor CustomEvent przyjmuje drugi argument - obiekt opcji:

const event = new CustomEvent('systemUpdate', {
  detail: { module: 'security', status: 'active' },  // Dane zdarzenia
  bubbles: true,     // Czy zdarzenie propaguje w górę DOM
  cancelable: true,  // Czy można wywołać preventDefault()
  composed: false    // Czy przechodzi przez Shadow DOM
});
  • detail - obiekt z danymi, które chcemy przekazać
  • bubbles - domyślnie false; ustaw na true, jeśli zdarzenie ma propagować w górę drzewa DOM (bubbling)
  • cancelable - domyślnie false; ustaw na true, jeśli chcesz móc anulować zdarzenie przez preventDefault()

Wysyłanie zdarzeń - dispatchEvent

Sam obiekt zdarzenia nic nie robi - musimy go wysłać (dispatch) na konkretnym elemencie DOM:

// Tworzymy element, który będzie emitował zdarzenia
const controlPanel = document.getElementById('controlPanel');

// Tworzymy zdarzenie
const fenceAlert = new CustomEvent('fenceBreak', {
  detail: {
    zone: 'A',
    voltage: 0,
    dinosaur: 'T-Rex'
  },
  bubbles: true
});

// Wysyłamy zdarzenie na elemencie
controlPanel.dispatchEvent(fenceAlert);

Metoda dispatchEvent() jest dostępna na każdym elemencie DOM. Po jej wywołaniu, zdarzenie zostaje natychmiast wysłane i wszystkie nasłuchujące handlery są wykonywane synchronicznie.

Nasłuchiwanie Custom Events

Custom Events nasłuchujemy dokładnie tak samo jak zwykłe zdarzenia - przez addEventListener:

const controlPanel = document.getElementById('controlPanel');

// Rejestrujemy listener dla naszego zdarzenia
controlPanel.addEventListener('fenceBreak', function(event) {
  const { zone, voltage, dinosaur } = event.detail;
  console.log('ALARM! Ogrodzenie strefy ' + zone + ' uszkodzone!');
  console.log('Dinozaur: ' + dinosaur);
  console.log('Napięcie: ' + voltage + 'V');
});

// Inny moduł też może nasłuchiwać tego samego zdarzenia
controlPanel.addEventListener('fenceBreak', function(event) {
  if (event.detail.dinosaur === 'T-Rex') {
    console.log('EWAKUACJA NATYCHMIAST!');
  }
});

// Wysyłamy zdarzenie - oba listenery zostaną wywołane
controlPanel.dispatchEvent(new CustomEvent('fenceBreak', {
  detail: { zone: 'A', voltage: 0, dinosaur: 'T-Rex' }
}));

Praktyczny przykład: System alarmowy parku

Zobaczmy kompletny przykład systemu komunikacji między modułami:

// System monitoringu Parku Jurajskiego
const parkSystem = document.getElementById('parkSystem');

// Moduł bezpieczeństwa nasłuchuje alarmów
parkSystem.addEventListener('securityAlert', function(event) {
  const { type, zone, message } = event.detail;
  const alertDiv = document.createElement('div');
  alertDiv.className = 'alert alert-' + type;
  alertDiv.textContent = '[' + zone + '] ' + message;
  document.getElementById('alerts').appendChild(alertDiv);
});

// Moduł monitoringu dinozaurów nasłuchuje statusów
parkSystem.addEventListener('dinoStatus', function(event) {
  const { name, heartRate, location } = event.detail;
  console.log(name + ': HR=' + heartRate + ', lokacja=' + location);
});

// Funkcja emitująca alert bezpieczeństwa
function emitSecurityAlert(type, zone, message) {
  parkSystem.dispatchEvent(new CustomEvent('securityAlert', {
    detail: { type, zone, message },
    bubbles: true
  }));
}

// Funkcja emitująca status dinozaura
function emitDinoStatus(name, heartRate, location) {
  parkSystem.dispatchEvent(new CustomEvent('dinoStatus', {
    detail: { name, heartRate, location },
    bubbles: true
  }));
}

// Użycie
emitSecurityAlert('warning', 'Zone B', 'Raptor aktywność podwyższona');
emitSecurityAlert('critical', 'Zone A', 'Ogrodzenie uszkodzone!');
emitDinoStatus('T-Rex', 85, 'Wybieg główny');

Bubbling w Custom Events

Domyślnie Custom Events nie propagują w górę drzewa DOM. Aby włączyć bubbling, musisz ustawić bubbles: true:

const child = document.getElementById('zoneA');
const parent = document.getElementById('parkSystem');

// Listener rejestrujemy PRZED wysłaniem, bo dispatchEvent() działa synchronicznie
parent.addEventListener('alert', (e) => {
  console.log('Rodzic złapał:', e.detail.msg);
});

// BEZ bubbles (domyślnie) - listener na rodzicu NIE złapie zdarzenia
child.dispatchEvent(new CustomEvent('alert', {
  detail: { msg: 'bez bubbles' }
  // bubbles domyślnie false
}));
// (w konsoli nic się nie pojawi)

// Z bubbles: true - zdarzenie propaguje do rodzica
child.dispatchEvent(new CustomEvent('alert', {
  detail: { msg: 'z bubbles' },
  bubbles: true  // Teraz zdarzenie będzie propagować!
}));
// Rodzic złapał: z bubbles

To jest szczególnie przydatne, gdy chcemy nasłuchiwać zdarzeń z wielu dzieci na jednym rodzicu (event delegation dla Custom Events).

Anulowanie Custom Events

Jeśli ustawisz cancelable: true, możesz anulować zdarzenie:

const event = new CustomEvent('transferDino', {
  detail: { dinosaur: 'T-Rex', from: 'A', to: 'B' },
  cancelable: true,
  bubbles: true
});

// Listener może anulować zdarzenie
parkSystem.addEventListener('transferDino', function(e) {
  if (e.detail.dinosaur === 'T-Rex') {
    e.preventDefault(); // Anuluj transfer T-Rexa!
    console.log('Transfer T-Rexa zablokowany ze względów bezpieczeństwa!');
  }
});

// Sprawdzamy, czy zdarzenie zostało anulowane
const wasNotCancelled = parkSystem.dispatchEvent(event);
console.log('Transfer dozwolony:', wasNotCancelled); // false (bo preventDefault)

Metoda dispatchEvent() zwraca false, jeśli zdarzenie zostało anulowane przez preventDefault(). Dzięki temu emitujący może sprawdzić, czy akcja powinna być kontynuowana.

Przypadki użycia Custom Events

Custom Events świetnie sprawdzają się w wielu scenariuszach:

  1. Komunikacja między komponentami - gdy dwa niezależne moduły muszą wymieniać informacje
  2. System powiadomień - centralny hub, na którym różne moduły emitują zdarzenia
  3. Decoupling kodu - moduł emitujący nie musi wiedzieć, kto nasłuchuje
  4. Plugin system - zewnętrzne moduły mogą reagować na zdarzenia aplikacji
  5. Śledzenie akcji użytkownika - emitowanie zdarzeń analitycznych
// Przykład: decoupling z Custom Events
// Moduł koszyka nie wie nic o module powiadomień
const cart = [];

function addToCart(product) {
  cart.push(product);

  // Emituje zdarzenie - ktokolwiek chce, może nasłuchiwać
  document.dispatchEvent(new CustomEvent('cartUpdated', {
    detail: { product, totalItems: cart.length },
    bubbles: true
  }));
}

// Moduł powiadomień nasłuchuje niezależnie
document.addEventListener('cartUpdated', (e) => {
  console.log('Dodano: ' + e.detail.product.name);
});

// Moduł badge'a koszyka też nasłuchuje niezależnie
document.addEventListener('cartUpdated', (e) => {
  console.log('Koszyk: ' + e.detail.totalItems + ' elementów');
});

Podsumowanie

Custom Events to potężne narzędzie do budowania luźno powiązanych (loosely coupled) systemów w JavaScript:

  • new CustomEvent(name, options) - tworzy niestandardowe zdarzenie
  • element.dispatchEvent(event) - wysyła zdarzenie na elemencie
  • detail - obiekt z danymi przekazywanymi w zdarzeniu
  • bubbles: true - włącza propagację zdarzenia w górę DOM
  • cancelable: true - pozwala anulować zdarzenie przez preventDefault()
  • Custom Events nasłuchujemy tak samo jak zwykłe zdarzenia DOM - przez addEventListener

"W Parku Jurajskim," podsumowuje Dr. Grant, "każdy system musi komunikować się z innymi. Custom Events to nasz wewnętrzny system radiowy - każdy moduł może nadawać i każdy może odbierać. Bez tego cały park pogrążyłby się w chaosie."

Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Custom Events - Park Jurajski</title>
    <style>
        body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
        h1 { color: #e94560; text-align: center; }
        h2 { color: #4ecdc4; margin-top: 25px; }
        .section { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 10px; margin: 15px 0; border-left: 3px solid #e94560; }
        .output { background: #000; padding: 12px; border-radius: 6px; margin: 10px 0; white-space: pre-wrap; font-size: 13px; color: #0f0; max-height: 200px; overflow-y: auto; }
        button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; margin: 5px; font-size: 14px; color: white; }
        .btn-alert { background: #e94560; }
        .btn-status { background: #0f3460; }
        .btn-transfer { background: #533483; }
        .btn-clear { background: #555; }
        button:hover { opacity: 0.85; }
        .zone { display: inline-block; padding: 5px 12px; margin: 3px; border-radius: 4px; background: rgba(255,255,255,0.1); }
        .zone.active { background: rgba(233,69,96,0.3); border: 1px solid #e94560; }
    </style>
</head>
<body>
    <h1>Custom Events - System Monitoringu Parku</h1>

    <div class="section">
        <h2>1. Emitowanie Custom Events</h2>
        <p>Kliknij przycisk, aby wysłać niestandardowe zdarzenie z danymi:</p>
        <button class="btn-alert" onclick="emitAlert('critical', 'A', 'Ogrodzenie uszkodzone!')">Alert Zone A</button>
        <button class="btn-alert" onclick="emitAlert('warning', 'B', 'Raptor aktywność podwyższona')">Alert Zone B</button>
        <button class="btn-status" onclick="emitStatus('T-Rex', 85, 'Wybieg główny')">Status T-Rex</button>
        <button class="btn-status" onclick="emitStatus('Raptor', 120, 'Zone B')">Status Raptor</button>
        <div class="output" id="event-log">Oczekiwanie na zdarzenia...</div>
        <button class="btn-clear" onclick="document.getElementById('event-log').textContent = ''">Wyczyść log</button>
    </div>

    <div class="section">
        <h2>2. Bubbling - propagacja zdarzeń</h2>
        <p>Zdarzenie z bubbles:true propaguje do rodzica:</p>
        <div id="parkSystem" style="padding: 10px; border: 1px dashed #4ecdc4; border-radius: 8px;">
            <span style="color: #4ecdc4;">parkSystem (rodzic)</span>
            <div style="display: flex; gap: 10px; margin-top: 10px;">
                <div class="zone" id="zoneA" onclick="emitZoneEvent('A')">Zone A (kliknij)</div>
                <div class="zone" id="zoneB" onclick="emitZoneEvent('B')">Zone B (kliknij)</div>
                <div class="zone" id="zoneC" onclick="emitZoneEvent('C')">Zone C (kliknij)</div>
            </div>
        </div>
        <div class="output" id="bubble-log">Kliknij strefę...</div>
    </div>

    <div class="section">
        <h2>3. Cancelable - anulowanie zdarzeń</h2>
        <p>Transfer T-Rexa jest blokowany przez preventDefault():</p>
        <button class="btn-transfer" onclick="tryTransfer('T-Rex', 'A', 'B')">Transfer T-Rex (A->B)</button>
        <button class="btn-transfer" onclick="tryTransfer('Triceratops', 'B', 'C')">Transfer Triceratops (B->C)</button>
        <div class="output" id="transfer-log">Spróbuj transferu...</div>
    </div>

    <script>
        const eventLog = document.getElementById('event-log');
        const bubbleLog = document.getElementById('bubble-log');
        const transferLog = document.getElementById('transfer-log');
        const parkSystemEl = document.getElementById('parkSystem');

        function log(target, msg) {
            const time = new Date().toLocaleTimeString();
            target.textContent += '\n[' + time + '] ' + msg;
            target.scrollTop = target.scrollHeight;
        }

        // === 1. Custom Events z detail ===
        document.addEventListener('securityAlert', function(e) {
            const { type, zone, message } = e.detail;
            log(eventLog, '[' + type.toUpperCase() + '] Zone ' + zone + ': ' + message);
        });

        document.addEventListener('dinoStatus', function(e) {
            const { name, heartRate, location } = e.detail;
            log(eventLog, '[STATUS] ' + name + ' - HR: ' + heartRate + ', loc: ' + location);
        });

        function emitAlert(type, zone, message) {
            document.dispatchEvent(new CustomEvent('securityAlert', {
                detail: { type, zone, message },
                bubbles: true
            }));
        }

        function emitStatus(name, heartRate, location) {
            document.dispatchEvent(new CustomEvent('dinoStatus', {
                detail: { name, heartRate, location },
                bubbles: true
            }));
        }

        // === 2. Bubbling demo ===
        parkSystemEl.addEventListener('zoneAlert', function(e) {
            log(bubbleLog, 'parkSystem złapał: strefa ' + e.detail.zone + ' (bubbling!)');
            document.querySelectorAll('.zone').forEach(z => z.classList.remove('active'));
            document.getElementById('zone' + e.detail.zone).classList.add('active');
        });

        function emitZoneEvent(zone) {
            const el = document.getElementById('zone' + zone);
            log(bubbleLog, 'Emituję ze strefy ' + zone + '...');
            el.dispatchEvent(new CustomEvent('zoneAlert', {
                detail: { zone: zone },
                bubbles: true
            }));
        }

        // === 3. Cancelable demo ===
        parkSystemEl.addEventListener('transferDino', function(e) {
            if (e.detail.dinosaur === 'T-Rex') {
                e.preventDefault();
                log(transferLog, 'ZABLOKOWANO transfer ' + e.detail.dinosaur + '!');
            }
        });

        function tryTransfer(dino, from, to) {
            const event = new CustomEvent('transferDino', {
                detail: { dinosaur: dino, from: from, to: to },
                cancelable: true,
                bubbles: true
            });
            const allowed = parkSystemEl.dispatchEvent(event);
            if (allowed) {
                log(transferLog, 'OK: ' + dino + ' przeniesiony z ' + from + ' do ' + to);
            } else {
                log(transferLog, 'Transfer ' + dino + ' anulowany (preventDefault)');
            }
        }

        console.log('Custom Events demo gotowe!');
    </script>
</body>
</html>

Hasła z dokumentacji w tej lekcji

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. Jak utworzyć niestandardowe zdarzenie z dodatkowymi danymi w JavaScript?

  2. 2. Która metoda służy do wysłania (wyemitowania) Custom Event na elemencie DOM?

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

Zadania praktyczne w grze

  • Edytor kodu

    W pliku index.js funkcja createMonitoring() buduje centrum kontroli z zagrodą w środku (zagroda jest dzieckiem centrum) i zwraca sendAlert(zone, level) oraz reportStatus(dino, status). Uzupełnij luki w sendAlert: ___BLANK1___ to konstruktor zdarzenia, które niesie dane w detail, ___BLANK2___ to opcja, dzięki której zdarzenie securityAlert dotrze z zagrody do centrum kontroli, a ___BLANK3___ to metoda, która wysyła zdarzenie z zagrody. Każdy alert ma trafić do dziennika centrum jako tekst "strefa: poziom", np. "A: wysoki".

  • Klikanie w kolejności

    Ułóż fragmenty kodu, aby utworzyć Custom Event 'dinoAlert' z obiektem detail:

Przydatne artykuły