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

Własne wydarzenia (Custom Events)

5 min czytania ·
W tej lekcji6

W Parku Jurajskim różne systemy muszą ze sobą współpracować - system monitoringu dinozaurów musi informować centrum bezpieczeństwa o zagrożeniach, a centrum bezpieczeństwa musi powiadomić ekipy ratunkowe. Standardowe wydarzenia przeglądarki (click, keydown, submit) obsługują interakcje użytkownika, ale co jeśli potrzebujemy własnych komunikatów między modułami aplikacji? Tutaj z pomocą przychodzą Custom Events - własne, niestandardowe wydarzenia.

Pomyśl o tym jak o radiostacji w parku. Czujnik przy ogrodzeniu nie musi znać numerów telefonów wszystkich ekip. Nadaje komunikat na swojej częstotliwości, a każdy, kto słucha, sam decyduje, co z nim zrobić.

Tworzenie CustomEvent

Konstruktor CustomEvent pozwala stworzyć wydarzenie o dowolnej nazwie i przekazać dodatkowe dane przez właściwość detail:

// Składnia: new CustomEvent(nazwaWydarzenia, opcje)
const alertEvent = new CustomEvent('dinoAlert', {
  detail: {
    species: 'Velociraptor',
    zone: 'B6',
    threatLevel: 9
  },
  bubbles: true,      // Czy wydarzenie bąbelkuje w górę drzewa DOM
  cancelable: true     // Czy można je anulować przez preventDefault()
});

Właściwość detail może zawierać dowolne dane - obiekty, tablice, stringi, liczby. To główna różnica między CustomEvent a zwykłym Event, który nie ma pola detail. Zwróć uwagę na obie flagi: domyślnie bubbles i cancelable mają wartość false, więc własne wydarzenie nie wędruje w górę drzewa i nie da się go anulować, dopóki sam tego nie włączysz. Samo utworzenie obiektu niczego jeszcze nie wysyła.

Wysyłanie wydarzeń z dispatchEvent()

Aby "wystrzelić" wydarzenie, używamy metody dispatchEvent() na dowolnym elemencie DOM:

// Wysłanie wydarzenia na konkretnym elemencie
const monitorPanel = document.getElementById('monitor-panel');
monitorPanel.dispatchEvent(alertEvent);

// Wysłanie wydarzenia na całym dokumencie (globalny broadcast)
document.dispatchEvent(alertEvent);

Gdy wydarzenie zostanie wysłane, wszystkie zarejestrowane na nie listenery zostaną wywołane synchronicznie - w kolejności ich rejestracji. To ważna różnica względem kliknięć użytkownika: kod zapisany pod dispatchEvent() wykona się dopiero wtedy, gdy wszystkie listenery skończą pracę. Element, na którym wysyłasz wydarzenie, staje się jego celem. Dzięki bubbles: true sygnał z panelu dotrze też do listenerów na przodkach, aż po document.

Nasłuchiwanie na własne wydarzenia

Nasłuchiwanie na Custom Events działa identycznie jak na standardowe wydarzenia - za pomocą addEventListener():

// Nasłuchiwanie na dowolnym elemencie
document.addEventListener('dinoAlert', function(event) {
  const { species, zone, threatLevel } = event.detail;

  console.log(`ALERT: ${species} wykryty w strefie ${zone}!`);
  console.log(`Poziom zagrożenia: ${threatLevel}/10`);

  if (threatLevel >= 8) {
    activateEmergencyProtocol(zone);
  }
});

Destrukturyzacja wyciąga z event.detail dokładnie te pola, których potrzebujemy. Listener nie wie, który moduł nadał alarm, i nie musi tego wiedzieć, a dla addEventListener() własna nazwa niczym nie różni się od click.

Anulowanie: cancelable i wynik dispatchEvent()

Flaga cancelable pozwala słuchaczom zawetować akcję. Metoda dispatchEvent() zwraca false, gdy wydarzenie było anulowalne i któryś listener wywołał preventDefault(), a w każdym innym przypadku true.

const gateEvent = new CustomEvent('gateOpening', {
  detail: { gate: 'B6' },
  cancelable: true
});

document.addEventListener('gateOpening', (e) => {
  if (e.detail.gate === 'B6') {
    e.preventDefault(); // raptory przy bramie - wstrzymaj otwarcie
  }
});

const allowed = document.dispatchEvent(gateEvent);
console.log(allowed ? 'Brama otwarta' : 'Otwarcie zablokowane');

W konsoli zobaczysz "Otwarcie zablokowane". Moduł bramy nie zna modułu ochrony, a mimo to ochrona może wstrzymać otwarcie. Bez cancelable: true wywołanie preventDefault() zostałoby zignorowane.

Praktyczny system alertów parku

Połączmy wszystkie elementy w kompletny system komunikacji między modułami. Pierwszy moduł tylko tworzy i wysyła alerty.

// Moduł 1: Generator alertów
function createParkAlert(alertType, message, data = {}) {
  const alertEvent = new CustomEvent('parkAlert', {
    detail: {
      type: alertType,
      message: message,
      timestamp: new Date(),
      data: data
    },
    bubbles: true
  });

  document.dispatchEvent(alertEvent);
}

Funkcja createParkAlert() pakuje typ, treść, znacznik czasu i dodatkowe dane do jednego detail. Drugi moduł to centralny słuchacz, który rozdziela alerty według typu.

// Moduł 2: Centralny system nasłuchujący
document.addEventListener('parkAlert', (e) => {
  const { type, message, timestamp, data } = e.detail;

  console.log(`${timestamp.toLocaleTimeString()} - Alert ${type}: ${message}`);

  switch(type) {
    case 'security':
      showSecurityAlert(message, data);
      break;
    case 'maintenance':
      showMaintenanceNotification(message, data);
      break;
    case 'visitor':
      showVisitorInfo(message, data);
      break;
    default:
      showGeneralAlert(message);
  }
});

Instrukcja switch kieruje alarm bezpieczeństwa, konserwację i informacje dla gości do różnych funkcji. Trzeci moduł wykrywa ucieczki i korzysta z generatora.

// Moduł 3: Wykrywanie ucieczek dinozaurów
function handleDinosaurEscape(dinosaurId) {
  const dinosaur = getDinosaurById(dinosaurId);

  createParkAlert('security', `ALARM! ${dinosaur.name} uciekł z wybiegu!`, {
    dinosaurId: dinosaurId,
    location: dinosaur.enclosure,
    dangerLevel: dinosaur.dangerLevel
  });

  activateEmergencyProtocols();
}

Moduł ucieczek nie woła showSecurityAlert() bezpośrednio. Wysyła wydarzenie, a centralny słuchacz robi resztę, więc każdy moduł można zmienić albo przetestować osobno.

Wzorzec Event Bus (szyna zdarzeń)

Custom Events pozwalają zbudować prosty Event Bus - centralny punkt komunikacji między niezależnymi modułami aplikacji:

class ParkEventBus {
  constructor() {
    this.target = new EventTarget();
  }

  emit(eventName, data) {
    this.target.dispatchEvent(
      new CustomEvent(eventName, { detail: data })
    );
  }

  on(eventName, callback) {
    const handler = (e) => callback(e.detail);
    this.target.addEventListener(eventName, handler);
    // Zwracamy funkcję do wyrejestrowania
    return () => this.target.removeEventListener(eventName, handler);
  }
}

Klasa opiera się na EventTarget, czyli tym samym mechanizmie, którego używają elementy DOM, tyle że bez żadnego elementu na stronie. Metoda on() zwraca funkcję do wyrejestrowania, bo tylko ona zna referencję do handler. Tak wygląda użycie:

const bus = new ParkEventBus();

// Moduł monitoringu rejestruje się na alerty
const unsubscribe = bus.on('fenceBreak', (data) => {
  console.log(`Ogrodzenie w strefie ${data.zone} uszkodzone!`);
});

// Moduł sensorów wysyła alert
bus.emit('fenceBreak', { zone: 'C3', voltage: 0 });

// Gdy moduł jest niszczony - wyrejestrowanie
unsubscribe();

Dzięki Event Bus moduły nie muszą znać się nawzajem - komunikują się wyłącznie przez wydarzenia, co zapewnia luźne powiązanie (loose coupling) i łatwiejsze testowanie.

Moja rada: nazwy własnych wydarzeń wybieraj tak, żeby nie myliły się z natywnymi, na przykład z prefiksem modułu, jak dinoAlert czy parkAlert, i trzymaj się jednego stylu w całej aplikacji. W piaskownicy poniżej działa system alertów parku zbudowany na Custom Events. Do własnych wydarzeń wrócimy przy gestach dotykowych, a Event Bus rozbudujemy w osobnej lekcji.

Pamiętaj: własne wydarzenie to komunikat nadany przez radiostację parku, który usłyszy każdy, kto nastroi się na jego częstotliwość.

Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Events - System Alertów Parku</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
            margin: 0;
            padding: 20px;
            color: white;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        h1 {
            text-align: center;
            margin-bottom: 40px;
        }

        .control-panel {
            background: rgba(0, 0, 0, 0.8);
            border-radius: 15px;
            padding: 30px;
            margin-bottom: 30px;
        }

        h2 {
            color: #3498db;
            margin-bottom: 20px;
        }

        .button-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin-bottom: 30px;
        }

        .alert-btn {
            padding: 15px 20px;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }

        .alert-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
        }

        .btn-security {
            background: linear-gradient(45deg, #e74c3c, #c0392b);
            color: white;
        }

        .btn-maintenance {
            background: linear-gradient(45deg, #f39c12, #e67e22);
            color: white;
        }

        .btn-visitor {
            background: linear-gradient(45deg, #3498db, #2980b9);
            color: white;
        }

        .btn-emergency {
            background: linear-gradient(45deg, #e74c3c, #8b0000);
            color: white;
            animation: pulse 2s infinite;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.7; }
        }

        .alerts-display {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }

        .alert-listener {
            background: rgba(0, 0, 0, 0.6);
            border-radius: 10px;
            padding: 20px;
            border: 2px solid #34495e;
        }

        .listener-title {
            font-weight: bold;
            margin-bottom: 15px;
            color: #3498db;
        }

        .alert-box {
            background: rgba(255, 255, 255, 0.05);
            border-left: 4px solid;
            border-radius: 6px;
            padding: 15px;
            margin: 10px 0;
            animation: slideIn 0.5s ease;
        }

        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateX(-20px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }

        .alert-security {
            border-color: #e74c3c;
            background: rgba(231, 76, 60, 0.1);
        }

        .alert-maintenance {
            border-color: #f39c12;
            background: rgba(243, 156, 18, 0.1);
        }

        .alert-visitor {
            border-color: #3498db;
            background: rgba(52, 152, 219, 0.1);
        }

        .alert-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }

        .alert-type {
            font-weight: bold;
            text-transform: uppercase;
            font-size: 12px;
        }

        .alert-time {
            color: #95a5a6;
            font-size: 11px;
        }

        .alert-message {
            margin-bottom: 10px;
        }

        .alert-details {
            background: rgba(0, 0, 0, 0.3);
            border-radius: 4px;
            padding: 8px;
            font-size: 12px;
            font-family: 'Courier New', monospace;
            color: #bdc3c7;
        }

        .statistics {
            background: rgba(0, 0, 0, 0.8);
            border-radius: 15px;
            padding: 30px;
        }

        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 20px;
        }

        .stat-box {
            background: rgba(255, 255, 255, 0.05);
            border-radius: 8px;
            padding: 20px;
            text-align: center;
        }

        .stat-number {
            font-size: 36px;
            font-weight: bold;
            color: #3498db;
        }

        .stat-label {
            color: #bdc3c7;
            font-size: 13px;
            margin-top: 5px;
        }

        .clear-btn {
            padding: 10px 20px;
            background: #7f8c8d;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
            margin-top: 20px;
        }

        .clear-btn:hover {
            background: #95a5a6;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>System Custom Events - Park Jurajski</h1>

        <div class="control-panel">
            <h2>Panel Kontrolny - Wyślij Alert</h2>
            <div class="button-grid">
                <button class="alert-btn btn-security" onclick="sendSecurityAlert()">
                    Alert Bezpieczeństwa
                </button>
                <button class="alert-btn btn-maintenance" onclick="sendMaintenanceAlert()">
                    Zgłoszenie Konserwacji
                </button>
                <button class="alert-btn btn-visitor" onclick="sendVisitorInfo()">
                    Informacja dla Gości
                </button>
                <button class="alert-btn btn-emergency" onclick="sendEmergencyAlert()">
                    ALARM AWARYJNY
                </button>
            </div>
        </div>

        <div class="alerts-display">
            <div class="alert-listener">
                <div class="listener-title">Centrum Bezpieczeństwa</div>
                <div id="security-alerts"></div>
            </div>

            <div class="alert-listener">
                <div class="listener-title">Dział Konserwacji</div>
                <div id="maintenance-alerts"></div>
            </div>

            <div class="alert-listener">
                <div class="listener-title">Obsługa Gości</div>
                <div id="visitor-alerts"></div>
            </div>

            <div class="alert-listener">
                <div class="listener-title">Wszystkie Alerty</div>
                <div id="all-alerts"></div>
            </div>
        </div>

        <div class="statistics">
            <h2>Statystyki Alertów</h2>
            <div class="stats-grid">
                <div class="stat-box">
                    <div class="stat-number" id="stat-security">0</div>
                    <div class="stat-label">Alerty bezpieczeństwa</div>
                </div>
                <div class="stat-box">
                    <div class="stat-number" id="stat-maintenance">0</div>
                    <div class="stat-label">Zgłoszenia konserwacji</div>
                </div>
                <div class="stat-box">
                    <div class="stat-number" id="stat-visitor">0</div>
                    <div class="stat-label">Informacje dla gości</div>
                </div>
                <div class="stat-box">
                    <div class="stat-number" id="stat-total">0</div>
                    <div class="stat-label">Wszystkie alerty</div>
                </div>
            </div>
            <button class="clear-btn" onclick="clearAllAlerts()">Wyczyść wszystkie alerty</button>
        </div>
    </div>

    <script>
        console.log('System Custom Events - Inicjalizacja...');

        // Statystyki
        let stats = {
            security: 0,
            maintenance: 0,
            visitor: 0,
            total: 0
        };

        // Funkcja do tworzenia custom eventu
        function createParkAlert(alertType, message, data = {}) {
            const alertEvent = new CustomEvent('parkAlert', {
                detail: {
                    type: alertType,
                    message: message,
                    timestamp: new Date(),
                    data: data
                },
                bubbles: true,
                cancelable: true
            });

            document.dispatchEvent(alertEvent);
            console.log(`Custom Event wysłany: parkAlert [${alertType}]`, data);
        }

        // Funkcja do wyświetlania alertu
        function displayAlert(container, type, message, timestamp, data) {
            const alertBox = document.createElement('div');
            alertBox.className = `alert-box alert-${type}`;

            const typeLabels = {
                security: 'BEZPIECZEŃSTWO',
                maintenance: 'KONSERWACJA',
                visitor: 'GOŚCIE'
            };

            alertBox.innerHTML = `
                <div class="alert-header">
                    <span class="alert-type">${typeLabels[type]}</span>
                    <span class="alert-time">${timestamp.toLocaleTimeString()}</span>
                </div>
                <div class="alert-message">${message}</div>
                <div class="alert-details">
                    Szczegóły: ${JSON.stringify(data, null, 2)}
                </div>
            `;

            container.insertBefore(alertBox, container.firstChild);

            // Ograniczamy liczbę wyświetlanych alertów
            while (container.children.length > 3) {
                container.removeChild(container.lastChild);
            }
        }

        // Nasłuchiwacze dla różnych działów

        // 1. Centrum Bezpieczeństwa - tylko alerty security
        document.addEventListener('parkAlert', (e) => {
            if (e.detail.type === 'security') {
                const container = document.getElementById('security-alerts');
                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
                console.log('Centrum Bezpieczeństwa otrzymało alert:', e.detail);
            }
        });

        // 2. Dział Konserwacji - tylko maintenance
        document.addEventListener('parkAlert', (e) => {
            if (e.detail.type === 'maintenance') {
                const container = document.getElementById('maintenance-alerts');
                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
                console.log('Dział Konserwacji otrzymał zgłoszenie:', e.detail);
            }
        });

        // 3. Obsługa Gości - tylko visitor
        document.addEventListener('parkAlert', (e) => {
            if (e.detail.type === 'visitor') {
                const container = document.getElementById('visitor-alerts');
                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
                console.log('Obsługa Gości otrzymała informację:', e.detail);
            }
        });

        // 4. Wszystkie Alerty - wszystkie typy
        document.addEventListener('parkAlert', (e) => {
            const container = document.getElementById('all-alerts');
            displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);

            // Aktualizacja statystyk
            stats[e.detail.type]++;
            stats.total++;

            document.getElementById('stat-security').textContent = stats.security;
            document.getElementById('stat-maintenance').textContent = stats.maintenance;
            document.getElementById('stat-visitor').textContent = stats.visitor;
            document.getElementById('stat-total').textContent = stats.total;
        });

        // Funkcje wysyłania alertów

        function sendSecurityAlert() {
            const dinosaurs = ['T-Rex', 'Velociraptor', 'Dilophosaurus', 'Pteranodon'];
            const locations = ['Sektor A', 'Sektor B', 'Laboratorium', 'Główna Brama'];
            const randomDino = dinosaurs[Math.floor(Math.random() * dinosaurs.length)];
            const randomLocation = locations[Math.floor(Math.random() * locations.length)];

            createParkAlert('security', `UWAGA! ${randomDino} wykryty poza wyznaczonym obszarem!`, {
                dinosaur: randomDino,
                location: randomLocation,
                dangerLevel: Math.floor(Math.random() * 10) + 1,
                status: 'AKTYWNY'
            });
        }

        function sendMaintenanceAlert() {
            const systems = ['System Ogrodzenia', 'Generator Energii', 'System Klimatyzacji', 'Drzwi Zabezpieczające'];
            const randomSystem = systems[Math.floor(Math.random() * systems.length)];

            createParkAlert('maintenance', `Zgłoszenie usterki: ${randomSystem} wymaga przeglądu`, {
                system: randomSystem,
                priority: ['Wysoki', 'Średni', 'Niski'][Math.floor(Math.random() * 3)],
                estimatedTime: `${Math.floor(Math.random() * 4) + 1}h`
            });
        }

        function sendVisitorInfo() {
            const messages = [
                'Rozpoczyna się pokaz karmienia Triceratopsów',
                'Wycieczka VIP za 15 minut',
                'Nowa wystawa "Era Dinozaurów" już otwarta',
                'Przypominamy o zachowaniu bezpiecznej odległości'
            ];
            const randomMessage = messages[Math.floor(Math.random() * messages.length)];

            createParkAlert('visitor', randomMessage, {
                category: 'Informacja',
                targetGroup: 'Wszyscy goście',
                validUntil: new Date(Date.now() + 3600000).toLocaleTimeString()
            });
        }

        function sendEmergencyAlert() {
            createParkAlert('security', 'ALARM AWARYJNY! Ewakuacja natychmiastowa!', {
                emergency: true,
                evacuationRoute: 'Brama Główna',
                status: 'KRYTYCZNY',
                actionRequired: 'Natychmiastowa ewakuacja wszystkich gości i personelu'
            });

            // Dodatkowy alert dla wszystkich działów
            setTimeout(() => {
                createParkAlert('maintenance', 'Wszystkie systemy bezpieczeństwa w trybie awaryjnym', {
                    emergency: true
                });
            }, 500);

            setTimeout(() => {
                createParkAlert('visitor', 'Proszę niezwłocznie udać się do punktów ewakuacyjnych', {
                    emergency: true
                });
            }, 1000);
        }

        function clearAllAlerts() {
            document.getElementById('security-alerts').innerHTML = '';
            document.getElementById('maintenance-alerts').innerHTML = '';
            document.getElementById('visitor-alerts').innerHTML = '';
            document.getElementById('all-alerts').innerHTML = '';

            stats = { security: 0, maintenance: 0, visitor: 0, total: 0 };
            document.getElementById('stat-security').textContent = '0';
            document.getElementById('stat-maintenance').textContent = '0';
            document.getElementById('stat-visitor').textContent = '0';
            document.getElementById('stat-total').textContent = '0';

            console.log('Wszystkie alerty wyczyszczone');
        }

        console.log('System Custom Events gotowy do pracy');
        console.log('Zarejestrowano 4 nasłuchiwacze na event "parkAlert"');
        console.log('Park Jurajski - System Komunikacji ONLINE');
    </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. Do czego służy właściwość detail w CustomEvent?

  2. 2. Której metody używamy, aby „wystrzelić” własne wydarzenie na elemencie DOM?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż tworzenie własnego wydarzenia (jedna linia, od lewej):

Przydatne artykuły