Skip to content

Reactive Patterns and Advanced UI Communication

A computer program is a set of instructions for manipulating information.

In this lesson, we’re goint to add the ability to display a resource’s details in the details component when a result item is clicked.

This is intentionally a small, clear example of event-driven UI.

At the end of this class, you should be able to…

  • Explain the concept of reactive programming and its application in front-end development.
  • Demonstrate how to create and handle custom events for advanced UI communication.
  • Compare and apply the Observer and Pub-Sub patterns in event-driven applications.

Emphasize practical applications of reactive programming in building scalable front-end apps.

Ensure students understand the trade-offs between Observer and Pub-Sub before moving to asynchronous programming in Lesson 04.

  • The results list shows multiple resources
  • Clicking a result selects that resource
  • The details panel updates to show the clicked resource
  • <resource-results> will dispatch a custom event when a resource is selected
  • <resource-details> will listen for that event and render the selected resource
  • Event-driven programming (user action triggers code)
  • Custom events (CustomEvent)
  • Event bubbling and where to listen
  • One-way data flow between components
  • “Reactive” UI updates (re-render when data changes)

In this example, we will be explicit about the event contract between components.

An event contract defines:

  • Who emits the event
  • What the event is called
  • What data it carries
  • Who is allowed to listen and react

Making this explicit helps avoid hidden coupling and confusion as the UI grows.

Emitted by:

  • <resource-results>

When it fires:

  • A user clicks (or activates) a resource item in the results list

Purpose:

  • Notify the rest of the application that a specific resource has been selected

Event configuration:

  • bubbles: true
  • composed: true (recommended since Shadow DOM because implemented)

Event detail payload:

{
resource: {
id: string,
title: string,
category: string,
summary: string,
location: string,
hours: string,
contact: string,
virtual: boolean,
openNow: boolean,
}
}

Who listens:

  • main.js will be used to compose the connection between components.

What the listener does:

  • Extracts resource from event.detail
  • Passes it to <resource-details> via a setter such as details.resource = resource
  • The results component does not know who is listening
  • The details component does not know who emitted the event
  • Communication flows in one direction
  • Components remain reusable and testable

This same idea scales to filters, pagination, and other interactions that will be explored later in the course.

Lesson 02 focused on composition (assembling the UI from components).

Lesson 03 introduces the next problem:

  • Components need to react to user actions
  • Components need a way to communicate
  • The UI should update when data changes

In this example, we’ll use a simple reactive pattern built on events.

  1. Something happens (a user clicks)
  2. A component emits an event describing what happened
  3. Another component reacts by updating its own view

We are not using frameworks yet. We’re building the mental model that frameworks later formalize.

We’ll use the same data shape across components.

Example:

{
id: 'tutoring',
title: 'Peer Tutoring Centre',
category: 'Academic',
summary: 'Drop-in tutoring and study support.',
location: 'Building W, Room W101',
hours: 'Mon–Thu 10:00–16:00',
contact: 'tutoring@nait.ca',
virtual: false,
openNow: true,
}

In <resource-results>:

  • Store an array of resources (hard-coded for this lesson)
  • Render the list from that array
  • Each list item must identify which resource it represents
    • Put the resource id in a data-id attribute on each list item
    • Use event delegation on the list container to capture clicks

Step 3: Dispatch a custom event when a result is clicked

Section titled “Step 3: Dispatch a custom event when a result is clicked”

When a list item is clicked, dispatch a custom event from <resource-results>.

Suggested event name:

  • resource-selected

Suggested detail payload:

  • { resource } (include the full object so the receiver can render without having to look up)

Example:

this.dispatchEvent(
new CustomEvent('resource-selected', {
detail: { resource },
bubbles: true,
composed: true,
})
);

Why bubbles: true?

  • It allows a parent (or the document) to listen without wiring direct references between components.

Why composed: true?

  • Since we are using a Shadow DOM in our components, events will need to cross the Shadow DOM boundary.

Step 4: Listen for the event and update details

Section titled “Step 4: Listen for the event and update details”

In <resource-details>:

  • Create a set resource(resource) setter
  • Store the selected resource
  • Re-render the details view when it changes

Then, in a place that can see both components (choose one):

Option A (implemented in this example): Listen in main.js

  • Listen for resource-selected
  • Call details.resource = resource

Option B: Listen inside a parent component (explored in a later example)

After a click:

  • Visually highlight the selected item (use the Bootstrap active class)
  • Reinforces the idea that UI is reacting to data changes

Minimum requirements:

  • Results render from an array of resource objects
  • Clicking a result updates the details panel
  • Use a custom event (resource-selected) to communicate

Grab the starter kit code for your Workbook by running the following in the VS Code terminal at the root of your workbook.

Terminal window
pnpm dlx tiged --disable-cache --force DG-InClass/SDEV-2150-A01-Sep-2026/sk/day-03/example/lesson-03-starter ./src/lesson-03
  1. Add the following to the main.js.

    src/js/main.js
    // TODO: Pass data to resource-results component
    const resultsComponent = document.querySelector('resource-results');
    resultsComponent.results = resultData;
    // TODO: Listen for resourceSelected event and update resource-details component
    const detailsComponent = document.querySelector('resource-details');
    resultsComponent.addEventListener('resource-selected', (event) => {
    const { resource } = event.detail;
    detailsComponent.resource = resource;
    });
  2. Modify the template template string in resource-results.js to remove all the hard-coded buttons that contain the resource results.

    src/js/components/resource-results.js
    <div class="list-group list-group-flush">
    <!-- Results will be injected here -->
    <button type="button" class="list-group-item list-group-item-action active" aria-current="true">
    <div class="d-flex w-100 justify-content-between">
    <h2 class="h6 mb-1">Peer Tutoring Centre</h2>
    <small>Academic</small>
    </div>
    <p class="mb-1 small text-body-secondary">Drop-in tutoring and study support.</p>
    <small class="text-body-secondary">Building W, Room W101</small>
    </button>
    <button type="button" class="list-group-item list-group-item-action">
    <div class="d-flex w-100 justify-content-between">
    <h2 class="h6 mb-1">Counselling Services</h2>
    <small>Wellness</small>
    </div>
    <p class="mb-1 small text-body-secondary">Confidential mental health supports.</p>
    <small class="text-body-secondary">Virtual and in-person</small>
    </button>
    <button type="button" class="list-group-item list-group-item-action">
    <div class="d-flex w-100 justify-content-between">
    <h2 class="h6 mb-1">Student Awards and Bursaries</h2>
    <small>Financial</small>
    </div>
    <p class="mb-1 small text-body-secondary">Funding options and application help.</p>
    <small class="text-body-secondary">Student Services, Main Floor CAT</small>
    </button>
    <button type="button" class="list-group-item list-group-item-action">
    <div class="d-flex w-100 justify-content-between">
    <h2 class="h6 mb-1">IT Service Desk</h2>
    <small>Tech</small>
    </div>
    <p class="mb-1 small text-body-secondary">Account access, Wi-Fi, BYOD support.</p>
    <small class="text-body-secondary">Library</small>
    </button>
    </div>
  3. Then, modify the component class to dynamically accept the data.

    src/js/components/resource-results.js
    class ResourceResults extends HTMLElement {
    // TODO: Create a private field for results data
    #results = [];
    src/js/components/resource-results.js
    // TODO: Implement setter for results data, remember to render
    set results(data) {
    this.#results = data;
    this.render();
    }
  4. Update the render() function to build the component’s content from the supplied data. This is where we dynamically build each <button> that we removed from the template string.

    src/js/components/resource-results.js
    render() {
    // TODO: Update to render from the private results field, if it's empty, show "No results found" message
    const content = template.content.cloneNode(true);
    if (this.#results.length) {
    // Generate the list of results to display
    const resultsHtml = this.#results.map(result => `<button type="button" class="list-group-item list-group-item-action" data-id="${result.id}">
    <div class="d-flex w-100 justify-content-between">
    <h2 class="h6 mb-1">${result.title}</h2>
    <small>${result.category}</small>
    </div>
    <p class="mb-1 small text-body-secondary">${result.summary}</p>
    <small class="text-body-secondary">${result.location}</small>
    </button>`);
    const listGroup = content.querySelector('.list-group');
    listGroup.innerHTML = resultsHtml.join('');
    } else {
    // No results found message
    const listGroup = content.querySelector('.list-group');
    listGroup.innerHTML = `<div class="list-group-item">
    <p class="mb-0">No results found.</p>
    </div>`;
    }
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    // Clear existing content and append new content
    this.shadowRoot.innerHTML = '';
    this.shadowRoot.appendChild(content);
    }
    }
  5. Create an event handler for button clicks in the resource results.

    src/js/components/resource-results.js
    // TODO: Add an event handler method for result selection
    _handleResultClick(event) {
    const button = event.target.closest('button[data-id]');
    if (button) {
    const selectedId = button.getAttribute('data-id');
    // Mark the selected result as active
    // NOTE: can use and explain the optional chaining operator here (as below) OR just use an if statement
    this.shadowRoot.querySelector('button.active')?.classList.remove('active');
    button.classList.add('active');
    // Find the selected resource from the results
    const resource = this.#results.find(r => r.id === selectedId);
    // Dispatch a custom event with the selected resource details
    const selectedEvent = new CustomEvent('resource-selected', {
    detail: { resource },
    bubbles: true,
    composed: true,
    });
    this.dispatchEvent(selectedEvent);
    }
    }
  6. Make sure the event handler is hooked up in both the constructor and the connected callback.

    src/js/components/resource-results.js
    constructor() {
    super();
    // TODO: Bind the handleResultClick method to this instance
    this._handleResultClick = this._handleResultClick.bind(this);
    this.attachShadow({ mode: 'open' });
    }
    src/js/components/resource-results.js
    connectedCallback() {
    // TODO: Add a click event listener to handle result selection
    this.shadowRoot.addEventListener('click', this._handleResultClick);
    this.render();
    }
  7. Also make sure to detach the event handler in the disconnnected callback.

    src/js/components/resource-results.js
    // TODO: Clean up event listener in disconnectedCallback
    disconnectedCallback() {
    this.shadowRoot.removeEventListener('click', this._handleResultClick);
    }

We will need to do similar work with the resource-details.js custom element. This time, however, we’ll replace the hard-coded content with a <slot> where the details can be injected.

  1. First, modify the template to remove hard-coded information.

    src/js/components/resource-details.js
    template.innerHTML = `
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
    <section class="h-100">
    <div class="card h-100">
    <div class="card-header">
    <strong>Details</strong>
    </div>
    <!-- Details content will be injected here -->
    <slot></slot>
    <!-- Action buttons may be dealt with in a future example -->
    <div class="card-body">
    <h2 class="h5">Peer Tutoring Centre</h2>
    <p class="text-body-secondary mb-2">Drop-in tutoring and study support.</p>
    <dl class="row mb-0">
    <dt class="col-4">Category</dt>
    <dd class="col-8">Academic</dd>
    <dt class="col-4">Location</dt>
    <dd class="col-8">Building W, Room W101</dd>
    <dt class="col-4">Hours</dt>
    <dd class="col-8">Mon-Thu 10:00-16:00</dd>
    <dt class="col-4">Contact</dt>
    <dd class="col-8">tutoring@nait.ca</dd>
    </dl>
    </div>
    <div class="card-footer d-flex gap-2">
    <button class="btn btn-outline-secondary" type="button">Copy email</button>
    <button class="btn btn-outline-primary" type="button">Open map</button>
    </div>
    </div>
    </section>`;
  2. Next, we modify the component class to process the data dynamically. We’ll add a #resource private property to hold the data and a setter so its value can be set at runtime.

    src/js/components/resource-details.js
    class ResourceDetails extends HTMLElement {
    // TODO: Create private field for resource data
    #resource = null;
    constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    }
    connectedCallback() {
    this.render();
    }
    // TODO: Implement setter for resource data, remember to render
    set resource(data) {
    this.#resource = data;
    this.render();
    }
  3. Lastly, we’ll adjust the rendering to build up the content. This time, we’ll do it the “better” way by using document.createElement() instead of trying to build HTML from a template string.

    src/js/components/resource-details.js
    render() {
    // TODO: Render resource details if available
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    if (this.#resource) {
    const detailsContainer = document.createElement('div');
    detailsContainer.classList.add('card-body');
    detailsContainer.innerHTML = `
    <h2 class="h5">${this.#resource.title}</h2>
    <p class="text-body-secondary mb-2">${this.#resource.summary}</p>
    <dl class="row mb-0">
    <dt class="col-4">Category</dt>
    <dd class="col-8">${this.#resource.category}</dd>
    <dt class="col-4">Location</dt>
    <dd class="col-8">${this.#resource.location}</dd>
    <dt class="col-4">Hours</dt>
    <dd class="col-8">${this.#resource.hours}</dd>
    <dt class="col-4">Contact</dt>
    <dd class="col-8">${this.#resource.contact}</dd>
    </dl>
    `;
    this.shadowRoot.innerHTML = '';
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this.shadowRoot.querySelector('slot').appendChild(detailsContainer);
    } else {
    // If no resource is selected, just render the template
    this.shadowRoot.innerHTML = '';
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
    }

In the starter kit, you will find a homework.md that corresponds to the content in this lesson.

You should also try to stretch what you’ve reviewed so far by adding these features to today’s demo:

  • Add keyboard support (Enter/Space selects an item)
  • Add a “selected id” state inside results
  • Add a default state in details (“Select a resource to view details”)

Here be dragons

Lesson 04 will push the reactive pattern further.

We’ll connect:

  • <resource-filters> (form inputs)
  • to <resource-results> (rendered list)

The key idea will be:

  • Filters emit an event describing the new filter state
  • Results reacts by filtering the array and re-rendering