Reactive Patterns and Advanced UI Communication
Lecture
Section titled “Lecture”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.
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.
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.
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.
Our Approach
Section titled “Our Approach”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
Activity
Section titled “Activity”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-A01-Sep-2026/sk/day-03/example/lesson-03-starter ./src/lesson-03pnpm dlx tiged --disable-cache --force DG-InClass/SDEV-2150-A02-Sep-2026/sk/day-03/example/lesson-03-starter ./src/lesson-03Code Walkthrough
Section titled “Code Walkthrough”-
Add the following to the
main.js.src/js/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 the template template string in
resource-results.jsto 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> -
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 renderset results(data) {this.#results = data;this.render();} -
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" messageconst content = template.content.cloneNode(true);if (this.#results.length) {// Generate the list of results to displayconst 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 messageconst 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 contentthis.shadowRoot.innerHTML = '';this.shadowRoot.appendChild(content);}} -
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 statementthis.shadowRoot.querySelector('button.active')?.classList.remove('active');button.classList.add('active');// Find the selected resource from the resultsconst resource = this.#results.find(r => r.id === selectedId);// Dispatch a custom event with the selected resource detailsconst selectedEvent = new CustomEvent('resource-selected', {detail: { resource },bubbles: true,composed: true,});this.dispatchEvent(selectedEvent);}} -
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 instancethis._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 selectionthis.shadowRoot.addEventListener('click', this._handleResultClick);this.render();} -
Also make sure to detach the event handler in the disconnnected callback.
src/js/components/resource-results.js // TODO: Clean up event listener in disconnectedCallbackdisconnectedCallback() {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.
-
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. We’ll add a
#resourceprivate 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 renderset resource(data) {this.#resource = data;this.render();} -
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 availablethis.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 templatethis.shadowRoot.innerHTML = '';this.shadowRoot.appendChild(template.content.cloneNode(true));}}
Practice
Section titled “Practice”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”)
OLD Content…
Section titled “OLD Content…”
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