Skip to content

Reactive Patterns and Advanced UI Communication

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.


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-A03-Jan-2026/sk/day-03/example/lesson-03-starter ./src/lesson-03
  1. Commit the starter kit (so you know what the starting position of this lesson is).
  2. Install the necessary dependencies:
Terminal window
npm install

or

Terminal window
npm i
  1. Run the dev server with the dev script:
Terminal window
npm run dev
  1. Open the provided development server URL in your browser
  2. You should see the Campus Resource Directory composed from Web Components.

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.

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.

  • 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.

Open for Instructor Guide

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

Stretch goals:

  • 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”)
Open for Code Changes

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;
});

First, modify the template to remove the hard-coded 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>

Then, modify the component class to dynamically process the data.

src/js/components/resource-results.js
class ResourceResults extends HTMLElement {
// TODO: Create a private field for results data
#results = [];
constructor() {
super();
// TODO: Bind the handleResultClick method to this instance
this._handleResultClick = this._handleResultClick.bind(this);
this.attachShadow({ mode: 'open' });
}
// TODO: Implement setter for results data, remember to render
set results(data) {
this.#results = data;
this.render();
}
// 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);
}
}
connectedCallback() {
// TODO: Add a click event listener to handle result selection
this.shadowRoot.addEventListener('click', this._handleResultClick);
this.render();
}
// TODO: Clean up event listener in disconnectedCallback
disconnectedCallback() {
this.shadowRoot.removeEventListener('click', this._handleResultClick);
}
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);
}
}

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>`;

Next, we modify the component class to process the data dynamically.

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();
}
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));
}
}
}

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
  1. Stage all changes:
Terminal window
git add .
  1. Commit:
Terminal window
git commit -m 'Lesson 03 - Reactive pattern (resource selection)'
  1. Push:
Terminal window
git push origin main