JavaScript and TypeScript course ยท Module 8: Events and the DOM

Custom Events and dispatchEvent in JavaScript

7 min read ยท
In this lesson9

"Listen carefully," says Dr. Alan Grant, leaning over the monitoring system console. "Dinosaurs communicate with each other using sounds - from low rumbles to terrifying roars. In our park, we need a similar communication system between modules. And that's where Custom Events come in!"

What Are Custom Events?

So far, we've learned about built-in DOM events - click, keydown, submit, and scroll. But JavaScript allows us to create our own, non-standard events. Thanks to them, we can build communication systems between different parts of an application, just like the alarm system in Jurassic Park sends notifications to different zones.

Custom Events are events defined by the programmer that can carry any data and be listened to just like regular DOM events.

The CustomEvent Constructor

To create a custom event, we use the CustomEvent constructor:

// Basic Custom Event creation
const alertEvent = new CustomEvent('dinoAlert');

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

// Accessing event data
console.log(escapeEvent.detail.dinosaur); // "Velociraptor"
console.log(escapeEvent.detail.severity); // "critical"
console.log(escapeEvent.type);            // "dinoEscape"

The detail object is a special field of Custom Events where we can pass any data. It is the standard place for data in a CustomEvent. You can also write your own class that extends Event with extra fields, but detail is usually all you need.

CustomEvent Options

The CustomEvent constructor accepts a second argument - an options object:

const event = new CustomEvent('systemUpdate', {
  detail: { module: 'security', status: 'active' },  // Event data
  bubbles: true,     // Whether the event propagates up the DOM
  cancelable: true,  // Whether preventDefault() can be called
  composed: false    // Whether it crosses Shadow DOM boundaries
});
  • detail - an object with data we want to pass
  • bubbles - defaults to false; set to true if the event should propagate up the DOM tree (bubbling)
  • cancelable - defaults to false; set to true if you want to be able to cancel the event via preventDefault()

Dispatching Events - dispatchEvent

The event object itself does nothing - we must dispatch it on a specific DOM element:

// Create an element that will emit events
const controlPanel = document.getElementById('controlPanel');

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

// Dispatch the event on the element
controlPanel.dispatchEvent(fenceAlert);

The dispatchEvent() method is available on every DOM element. After calling it, the event is immediately dispatched and all listening handlers are executed synchronously.

Listening for Custom Events

We listen for Custom Events exactly the same way as regular events - via addEventListener:

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

// Register a listener for our event
controlPanel.addEventListener('fenceBreak', function(event) {
  const { zone, voltage, dinosaur } = event.detail;
  console.log('ALARM! Zone ' + zone + ' fence damaged!');
  console.log('Dinosaur: ' + dinosaur);
  console.log('Voltage: ' + voltage + 'V');
});

// Another module can also listen to the same event
controlPanel.addEventListener('fenceBreak', function(event) {
  if (event.detail.dinosaur === 'T-Rex') {
    console.log('EVACUATE IMMEDIATELY!');
  }
});

// Dispatch the event - both listeners will be called
controlPanel.dispatchEvent(new CustomEvent('fenceBreak', {
  detail: { zone: 'A', voltage: 0, dinosaur: 'T-Rex' }
}));

Practical Example: Park Alarm System

Let's see a complete example of inter-module communication:

// Jurassic Park monitoring system
const parkSystem = document.getElementById('parkSystem');

// Security module listens for alerts
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);
});

// Dinosaur monitoring module listens for statuses
parkSystem.addEventListener('dinoStatus', function(event) {
  const { name, heartRate, location } = event.detail;
  console.log(name + ': HR=' + heartRate + ', location=' + location);
});

// Function that emits a security alert
function emitSecurityAlert(type, zone, message) {
  parkSystem.dispatchEvent(new CustomEvent('securityAlert', {
    detail: { type, zone, message },
    bubbles: true
  }));
}

// Function that emits a dinosaur status
function emitDinoStatus(name, heartRate, location) {
  parkSystem.dispatchEvent(new CustomEvent('dinoStatus', {
    detail: { name, heartRate, location },
    bubbles: true
  }));
}

// Usage
emitSecurityAlert('warning', 'Zone B', 'Raptor activity elevated');
emitSecurityAlert('critical', 'Zone A', 'Fence damaged!');
emitDinoStatus('T-Rex', 85, 'Main enclosure');

Bubbling in Custom Events

By default, Custom Events do not propagate up the DOM tree. To enable bubbling, you must set bubbles: true:

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

// Register the listener BEFORE dispatching, because dispatchEvent() is synchronous
parent.addEventListener('alert', (e) => {
  console.log('Parent caught:', e.detail.msg);
});

// WITHOUT bubbles (default) - listener on parent WON'T catch the event
child.dispatchEvent(new CustomEvent('alert', {
  detail: { msg: 'without bubbles' }
  // bubbles defaults to false
}));
// (nothing appears in the console)

// WITH bubbles: true - event propagates to parent
child.dispatchEvent(new CustomEvent('alert', {
  detail: { msg: 'with bubbles' },
  bubbles: true  // Now the event will propagate!
}));
// Parent caught: with bubbles

This is especially useful when you want to listen for events from many children on a single parent (event delegation for Custom Events).

Canceling Custom Events

If you set cancelable: true, you can cancel the event:

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

// A listener can cancel the event
parkSystem.addEventListener('transferDino', function(e) {
  if (e.detail.dinosaur === 'T-Rex') {
    e.preventDefault(); // Cancel T-Rex transfer!
    console.log('T-Rex transfer blocked for safety reasons!');
  }
});

// Check if the event was canceled
const wasNotCancelled = parkSystem.dispatchEvent(event);
console.log('Transfer allowed:', wasNotCancelled); // false (because preventDefault)

The dispatchEvent() method returns false if the event was canceled by preventDefault(). This allows the emitter to check whether the action should continue.

Use Cases for Custom Events

Custom Events work great in many scenarios:

  1. Communication between components - when two independent modules need to exchange information
  2. Notification system - a central hub where different modules emit events
  3. Code decoupling - the emitting module doesn't need to know who's listening
  4. Plugin system - external modules can react to application events
  5. User action tracking - emitting analytics events
// Example: decoupling with Custom Events
// The cart module knows nothing about the notification module
const cart = [];

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

  // Emits an event - whoever wants to can listen
  document.dispatchEvent(new CustomEvent('cartUpdated', {
    detail: { product, totalItems: cart.length },
    bubbles: true
  }));
}

// Notification module listens independently
document.addEventListener('cartUpdated', (e) => {
  console.log('Added: ' + e.detail.product.name);
});

// Cart badge module also listens independently
document.addEventListener('cartUpdated', (e) => {
  console.log('Cart: ' + e.detail.totalItems + ' items');
});

Summary

Custom Events are a powerful tool for building loosely coupled systems in JavaScript:

  • new CustomEvent(name, options) - creates a non-standard event
  • element.dispatchEvent(event) - dispatches the event on an element
  • detail - object with data passed in the event
  • bubbles: true - enables event propagation up the DOM
  • cancelable: true - allows canceling the event via preventDefault()
  • Custom Events are listened to the same way as regular DOM events - via addEventListener

"In Jurassic Park," Dr. Grant summarizes, "every system must communicate with others. Custom Events are our internal radio system - every module can broadcast and every module can receive. Without it, the entire park would descend into chaos."

Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Events - Jurassic Park</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 - Park Monitoring System</h1>

    <div class="section">
        <h2>1. Emitting Custom Events</h2>
        <p>Click a button to send a custom event with data:</p>
        <button class="btn-alert" onclick="emitAlert('critical', 'A', 'Fence damaged!')">Alert Zone A</button>
        <button class="btn-alert" onclick="emitAlert('warning', 'B', 'Raptor activity increased')">Alert Zone B</button>
        <button class="btn-status" onclick="emitStatus('T-Rex', 85, 'Main paddock')">Status T-Rex</button>
        <button class="btn-status" onclick="emitStatus('Raptor', 120, 'Zone B')">Status Raptor</button>
        <div class="output" id="event-log">Waiting for events...</div>
        <button class="btn-clear" onclick="document.getElementById('event-log').textContent = ''">Clear log</button>
    </div>

    <div class="section">
        <h2>2. Bubbling - event propagation</h2>
        <p>An event with bubbles:true propagates to the parent:</p>
        <div id="parkSystem" style="padding: 10px; border: 1px dashed #4ecdc4; border-radius: 8px;">
            <span style="color: #4ecdc4;">parkSystem (parent)</span>
            <div style="display: flex; gap: 10px; margin-top: 10px;">
                <div class="zone" id="zoneA" onclick="emitZoneEvent('A')">Zone A (click)</div>
                <div class="zone" id="zoneB" onclick="emitZoneEvent('B')">Zone B (click)</div>
                <div class="zone" id="zoneC" onclick="emitZoneEvent('C')">Zone C (click)</div>
            </div>
        </div>
        <div class="output" id="bubble-log">Click a zone...</div>
    </div>

    <div class="section">
        <h2>3. Cancelable - canceling events</h2>
        <p>The T-Rex transfer is blocked by 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">Try a transfer...</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 with 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 CAUGHT: Zone ' + 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, 'Emitting from zone' + 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, 'BLOCKED the transfer of ' + 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 + ' moved from ' + from + ' to ' + to);
            } else {
                log(transferLog, 'Transfer ' + dino + ' cancelled (preventDefault)');
            }
        }

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

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you create a custom event with additional data in JavaScript?

  2. 2. Which method is used to dispatch (emit) a Custom Event on a DOM element?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    In the index.js file the createMonitoring() function builds a control center with a paddock inside (the paddock is a child of the center) and returns sendAlert(zone, level) and reportStatus(dino, status). Fill in the blanks in sendAlert: ___BLANK1___ is the constructor of an event that carries data in detail, ___BLANK2___ is the option that lets the securityAlert event travel from the paddock up to the control center, and ___BLANK3___ is the method that sends the event from the paddock. Every alert should reach the center log as the text "zone: level", e.g. "A: high".

  • Click in order

    Arrange the code fragments to create a Custom Event 'dinoAlert' with a detail object:

Useful articles