We use cookies to enhance your experience on the site
CodeWorlds

Building Interactive Components

In Jurassic Park's control center, every panel on the screen is a separate component - a dinosaur card, an alert window, a report form. Each responds to operator actions: clicks, keys, mouse hover. Building such interactive components is one of the most common use cases for event listeners in real applications.

In this lesson, you'll learn to create self-contained, reusable UI components that manage their own events - from modal windows to panels with dynamic content.

Component: Modal Window

A modal is a window displayed on top of the page, blocking interaction with the rest of the interface. In our park, we'll use it to display dinosaur details after clicking its card.

Creating the Structure and Registering Events

1function createDinosaurModal(dinosaurData) {
2  // Creating the modal structure
3  const modal = document.createElement('div');
4  modal.className = 'dinosaur-modal';
5  modal.innerHTML = `
6    <div class="modal-content">
7      <div class="modal-header">
8        <h2>${dinosaurData.name}</h2>
9        <button class="close-btn">&times;</button>
10      </div>
11      <div class="modal-body">
12        <img src="${dinosaurData.image}" alt="${dinosaurData.name}">
13        <p><strong>Era:</strong> ${dinosaurData.era}</p>
14        <p><strong>Diet:</strong> ${dinosaurData.diet}</p>
15        <p><strong>Length:</strong> ${dinosaurData.length}m</p>
16        <p><strong>Weight:</strong> ${dinosaurData.weight}kg</p>
17        <p>${dinosaurData.description}</p>
18      </div>
19      <div class="modal-footer">
20        <button class="book-tour-btn">Book a Tour</button>
21        <button class="add-favorite-btn">Add to Favorites</button>
22      </div>
23    </div>
24  `;
25
26  // Get interactive elements from the modal
27  const closeBtn = modal.querySelector('.close-btn');
28  const bookTourBtn = modal.querySelector('.book-tour-btn');
29  const addFavoriteBtn = modal.querySelector('.add-favorite-btn');
30
31  // Close modal with X button
32  closeBtn.addEventListener('click', () => {
33    closeModal(modal);
34  });
35
36  // Close by clicking the background (overlay)
37  modal.addEventListener('click', (e) => {
38    if (e.target === modal) {
39      closeModal(modal);
40    }
41  });
42
43  // Book a tour
44  bookTourBtn.addEventListener('click', () => {
45    openBookingForm(dinosaurData.id);
46  });
47
48  // Add to favorites with button state change
49  addFavoriteBtn.addEventListener('click', () => {
50    addToFavorites(dinosaurData.id);
51    addFavoriteBtn.textContent = 'Added to favorites!';
52    addFavoriteBtn.disabled = true;
53  });
54
55  // Close with Escape key
56  const handleEscape = (e) => {
57    if (e.key === 'Escape') {
58      closeModal(modal);
59      document.removeEventListener('keydown', handleEscape);
60    }
61  };
62  document.addEventListener('keydown', handleEscape);
63
64  return modal;
65}
66
67function closeModal(modal) {
68  modal.classList.add('closing'); // Closing animation
69  setTimeout(() => modal.remove(), 300);
70}

Notice several key practices:

  • Closing on Escape - we add a listener on
    document
    , but remove it when the modal disappears (prevents memory leaks)
  • Clicking the background - we check
    e.target === modal
    to distinguish a click on the overlay from a click on the modal content
  • Button state change - after adding to favorites, we disable the button to prevent multiple clicks

Component: Toggle Switch

A simple component for turning park systems on and off:

1function createToggle(label, onChange) {
2  const container = document.createElement('div');
3  container.className = 'toggle-container';
4  container.innerHTML = `
5    <span class="toggle-label">${label}</span>
6    <button class="toggle-btn" data-active="false">OFF</button>
7  `;
8
9  const btn = container.querySelector('.toggle-btn');
10
11  btn.addEventListener('click', () => {
12    const isActive = btn.dataset.active === 'true';
13    const newState = !isActive;
14
15    btn.dataset.active = String(newState);
16    btn.textContent = newState ? 'ON' : 'OFF';
17    btn.classList.toggle('active', newState);
18
19    // Call callback with new state
20    onChange(newState);
21  });
22
23  return container;
24}
25
26// Usage - fence control panel
27const fenceToggle = createToggle('Electric Fence', (active) => {
28  console.log(`Fence ${active ? 'ACTIVE' : 'DISABLED'}`);
29});
30document.getElementById('controls').appendChild(fenceToggle);

Component: Tabs

Tabs are an essential element of the monitoring panel, allowing switching between views of different park zones:

1function createTabs(tabsData) {
2  const container = document.createElement('div');
3  container.className = 'tabs-container';
4
5  // Tab headers
6  const header = document.createElement('div');
7  header.className = 'tabs-header';
8
9  // Tab content
10  const body = document.createElement('div');
11  body.className = 'tabs-body';
12
13  tabsData.forEach((tab, index) => {
14    // Tab button
15    const tabBtn = document.createElement('button');
16    tabBtn.className = 'tab-btn' + (index === 0 ? ' active' : '');
17    tabBtn.textContent = tab.title;
18    tabBtn.dataset.index = index;
19
20    // Content panel
21    const tabPanel = document.createElement('div');
22    tabPanel.className = 'tab-panel' + (index === 0 ? ' active' : '');
23    tabPanel.innerHTML = tab.content;
24
25    // Click on tab
26    tabBtn.addEventListener('click', () => {
27      // Deactivate all
28      header.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
29      body.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
30
31      // Activate selected
32      tabBtn.classList.add('active');
33      tabPanel.classList.add('active');
34    });
35
36    header.appendChild(tabBtn);
37    body.appendChild(tabPanel);
38  });
39
40  container.appendChild(header);
41  container.appendChild(body);
42  return container;
43}
44
45// Usage
46const monitorTabs = createTabs([
47  { title: 'Zone A', content: '<p>T-Rex: status OK</p>' },
48  { title: 'Zone B', content: '<p>Raptors: active</p>' },
49  { title: 'Zone C', content: '<p>Brachiosaurus: calm</p>' },
50]);

Pattern: Component Cleanup

Every component should have a

destroy()
method that removes all event listeners and prevents memory leaks:

1class ParkWidget {
2  constructor(element) {
3    this.element = element;
4    this.handlers = [];
5    this.init();
6  }
7
8  addListener(target, event, handler) {
9    target.addEventListener(event, handler);
10    this.handlers.push({ target, event, handler });
11  }
12
13  destroy() {
14    // Remove all registered listeners
15    this.handlers.forEach(({ target, event, handler }) => {
16      target.removeEventListener(event, handler);
17    });
18    this.handlers = [];
19    this.element.remove();
20  }
21
22  init() {
23    this.addListener(this.element, 'click', (e) => {
24      console.log('Widget clicked');
25    });
26    this.addListener(document, 'keydown', (e) => {
27      if (e.key === 'Escape') this.destroy();
28    });
29  }
30}

Storing references to all listeners and collectively removing them in

destroy()
is the best practice when building components - especially important in dynamic applications where components are frequently created and destroyed.

Go to CodeWorlds