Reactive Patterns and Advanced UI Communication
Learning Outcome Guide
Section titled “Learning Outcome Guide”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.
Starter Kit Code
Section titled “Starter Kit Code”Grab the starter kit code for your Workbook by running the following in the VS Code terminal at the root of your workbook.
pnpm dlx tiged --disable-cache --force DG-InClass/SDEV-2150-A03-Jan-2026/sk/day-03/example/lesson-03-starter ./src/lesson-03Walkthrough
Section titled “Walkthrough”- Commit the starter kit (so you know what the starting position of this lesson is).
- Install the necessary dependencies:
npm installor
npm i- Run the dev server with the
devscript:
npm run dev- Open the provided development server URL in your browser
- You should see the Campus Resource Directory composed from Web Components.
Lesson focus
Section titled “Lesson focus”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.
The reactive idea (simple version)
Section titled “The reactive idea (simple version)”- Something happens (a user clicks)
- A component emits an event describing what happened
- Another component reacts by updating its own view
We are not using frameworks yet. We’re building the mental model that frameworks later formalize.
Instructor demo
Section titled “Instructor demo”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.
Target behaviour
Section titled “Target behaviour”- The results list shows multiple resources
- Clicking a result selects that resource
- The details panel updates to show the clicked resource
What we will build
Section titled “What we will build”<resource-results>will dispatch a custom event when a resource is selected<resource-details>will listen for that event and render the selected resource
Concepts explored
Section titled “Concepts explored”- 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)
Event Contract
Section titled “Event Contract”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.
Event: resource-selected
Section titled “Event: resource-selected”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: truecomposed: 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.jswill be used to compose the connection between components.
What the listener does:
- Extracts
resourcefromevent.detail - Passes it to
<resource-details>via a setter such asdetails.resource = resource
Why this is a good contract
Section titled “Why this is a good contract”- 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
Instructor Guide
Section titled “Instructor Guide”Step 1: Agree on the data shape
Section titled “Step 1: Agree on the data shape”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,}Step 2: Render results from an array
Section titled “Step 2: Render results from an array”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-idattribute on each list item - Use event delegation on the list container to capture clicks
- Put the resource id in a
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)
Step 5: Add a selected state in results
Section titled “Step 5: Add a selected state in results”After a click:
- Visually highlight the selected item (use the Bootstrap
activeclass) - Reinforces the idea that UI is reacting to data changes
Assessing
Section titled “Assessing”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
Student exercise
Section titled “Student exercise”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
Lesson 03 Code
Section titled “Lesson 03 Code”Modify main.js
Section titled “Modify main.js”Add the following to the main.js.
// TODO: Pass data to resource-results componentconst resultsComponent = document.querySelector('resource-results');resultsComponent.results = resultData;
// TODO: Listen for resourceSelected event and update resource-details componentconst detailsComponent = document.querySelector('resource-details');resultsComponent.addEventListener('resource-selected', (event) => { const { resource } = event.detail; detailsComponent.resource = resource;});Modify resource-results.js
Section titled “Modify resource-results.js”First, modify the template to remove the hard-coded results.
<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.
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); }}Modify resource-details.js
Section titled “Modify resource-details.js”First, modify the template to remove hard-coded information.
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.
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)); } }}Looking ahead to Lesson 04
Section titled “Looking ahead to Lesson 04”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
Push to your GitHub workbook repo
Section titled “Push to your GitHub workbook repo”- Stage all changes:
git add .- Commit:
git commit -m 'Lesson 03 - Reactive pattern (resource selection)'- Push:
git push origin main