Skillversitas

Custom Events

Custom events let us create our own events — not just the browser's built-in events like click or submit. With custom events, components in an application can communicate with each other without direct coupling.

Why Custom Events?

Imagine a form component for adding students and a table component listing students. They don't know about each other. When the form successfully adds data, the table needs to know so it can update itself. Custom events bridge this communication.

Benefits:
- Loose coupling — components don't need to know about each other
- Flexible — many components can listen to the same event
- Can carry data — via the detail property

Code Example

Creating a Basic Custom Event

// Create a new event
const event = new Event('dataSaved');
// Or with data:
const eventDetails = new CustomEvent('dataSaved', {
  detail: {
    studentId: '2024001',
    name: 'Mike Johnson',
    timestamp: new Date().toISOString()
  }
});

// Dispatch (send) the event
document.dispatchEvent(eventDetails);
// Listen for the custom event
document.addEventListener('dataSaved', function (event) {
  console.log('Data tersave:', event.detail);
  console.log('StudentID:', event.detail.studentId);
  console.log('Name:', event.detail.name);
});

Custom Events on a Specific Element

// The student table listens for a custom event
const table = document.querySelector('#student-table');

// The 'rowAdded' event — sent from another component
table.addEventListener('rowAdded', function (event) {
  const { studentId, name, major } = event.detail;
  const row = document.createElement('tr');
  row.innerHTML = `
    <td>${studentId}</td>
    <td>${name}</td>
    <td>${major}</td>
  `;
  table.querySelector('tbody').appendChild(row);
});

// The student form sends the event when data is added
const form = document.querySelector('#form-add-student');
form.addEventListener('submit', function (e) {
  e.preventDefault();
  const formData = new FormData(form);

  // Send the event to the table (rather than calling the table's method directly)
  const event = new CustomEvent('studentAdded', {
    detail: {
      studentId: formData.get('studentId'),
      name: formData.get('name'),
      major: formData.get('major')
    },
    bubbles: true // the event bubbles up to the parent
  });

  form.dispatchEvent(event);
  form.reset();
});

Code Explanation

new Event(name)

Creates a simple event without data. Suitable for signals/notifications.

const event = new Event('refresh');
document.dispatchEvent(event);

new CustomEvent(name, options)

Creates an event with additional data via the detail property.

const event = new CustomEvent('gradeUpdated', {
  detail: {
    studentId: '2024001',
    course: 'Algorithms',
    grade: 85
  },
  bubbles: true,     // allowed to bubble up to the parent
  cancelable: true   // can be cancelled with preventDefault()
});

bubbles: true

Without bubbles: true, the event can only be heard on the same element where the event is dispatched. With bubbles: true, the event bubbles up to the parent (like native events).

// Without bubbles — only the child can hear it
child.dispatchEvent(event); // the parent CANNOT hear it

// With bubbles — the parent can hear it
const event = new CustomEvent('update', { bubbles: true });
child.dispatchEvent(event); // the parent CAN hear it

cancelable: true

Allows a listener to cancel the event with preventDefault().

const event = new CustomEvent('beforeDelete', {
  detail: { id: 42 },
  cancelable: true
});

// A listener can cancel it
document.addEventListener('beforeDelete', function (e) {
  const confirmed = confirm('Yakin delete?');
  if (!confirmed) {
    e.preventDefault(); // cancel the delete
  }
});

Real-World Examples

A Campus Notification System

Different components send notifications to a toast/popup without needing direct access to the notification element.

// Toast notification service
const toastContainer = document.querySelector('#toast-container');

document.addEventListener('showToast', function (event) {
  const { message, type = 'info' } = event.detail;

  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.textContent = message;
  toastContainer.appendChild(toast);

  setTimeout(() => {
    toast.classList.add('fade-out');
    setTimeout(() => toast.remove(), 300);
  }, 3000);
});

// Other parts of the app can send notifications without knowing how the toast works
document.dispatchEvent(new CustomEvent('showToast', {
  detail: { message: 'Grade successful disave!', type: 'success' }
}));

document.dispatchEvent(new CustomEvent('showToast', {
  detail: { message: 'Failed to connect to server.', type: 'error' }
}));

A Cross-Component Data Refresh System

// When student data is changed on one page,
// other components need to know so they can refresh

// Dashboard card — listen for refresh
document.addEventListener('dataStudentChanged', function () {
  updateDashboardStats();
});

// Table — listen for refresh
document.addEventListener('dataStudentChanged', function () {
  refreshTable();
});

// Graph — listen for refresh
document.addEventListener('dataStudentChanged', function () {
  refreshGraph();
});

// Edit form — dispatch the event after saving
function saveEdit(data) {
  fetch('/api/student/' + data.studentId, {
    method: 'PUT',
    body: JSON.stringify(data)
  }).then(res => res.json()).then(() => {
    // One event, all components refresh
    document.dispatchEvent(new CustomEvent('dataStudentChanged', {
      detail: { studentId: data.studentId, action: 'update' }
    }));
  });
}
const searchInput = document.querySelector('#search-student');
const filterButtons = document.querySelectorAll('.filter-btn');
const tableContainer = document.querySelector('#student-table-container');

// The search input dispatches an event
searchInput.addEventListener('input', function () {
  document.dispatchEvent(new CustomEvent('filterChanged', {
    detail: {
      search: this.value,
      major: getActiveFilter()
    }
  }));
});

// The filter buttons dispatch an event
filterButtons.forEach(btn => {
  btn.addEventListener('click', function () {
    filterButtons.forEach(b => b.classList.remove('active'));
    this.classList.add('active');

    document.dispatchEvent(new CustomEvent('filterChanged', {
      detail: {
        search: searchInput.value,
        major: this.dataset.major
      }
    }));
  });
});

// The table listens for filter changes
document.addEventListener('filterChanged', function (event) {
  const { search, major } = event.detail;
  tableContainer.querySelectorAll('tr[data-studentId]').forEach(row => {
    const name = row.querySelector('.col-name').textContent.toLowerCase();
    const rowMajor = row.dataset.major;

    const matchSearch = !search || name.includes(search.toLowerCase());
    const matchMajor = major === 'all' || rowMajor === major;

    row.style.display = matchSearch && matchMajor ? '' : 'none';
  });
});

Lifecycle Events for Components

// An event signaling that a component is ready/has loaded
class FormStudent {
  constructor(container) {
    this.container = container;
    this.init();
  }

  init() {
    this.render();
    this.container.dispatchEvent(new CustomEvent('componentReady', {
      detail: { name: 'FormStudent', element: this.container },
      bubbles: true
    }));
  }

  render() {
    this.container.innerHTML = `
      <form id="form-student">
        <input name="studentId" placeholder="StudentID" required />
        <input name="name" placeholder="Name" required />
        <button type="submit">Save</button>
      </form>
    `;
  }
}

// The parent can know when the component is ready
document.addEventListener('componentReady', function (e) {
  console.log(`${e.detail.name} is ready!`);
});

Handling Errors with Custom Events

// Dispatch an event with error handling
function addStudent(data) {
  try {
    validationData(data);
  } catch (error) {
    document.dispatchEvent(new CustomEvent('showToast', {
      detail: {
        message: `Error: ${error.message}`,
        type: 'error'
      }
    }));
    return;
  }

  // Save and dispatch success
  saveKeDatabase(data).then(() => {
    document.dispatchEvent(new CustomEvent('studentAdded', {
      detail: data
    }));
  });
}

Conclusion

Custom events are an elegant way to enable inter-component communication without coupling. Use new CustomEvent() with the detail property to carry data, bubbles: true so the event can be heard on the parent, and cancelable: true if you want to support cancellation. This pattern is ideal for campus applications with many components that need to synchronize data.