Skip to content

Component communication and State

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

  • Explain how data may be passed into or read from a web component using attributes and properties.
  • Emit custom events from a web component for listening.
  • Expose methods and properties for external JavaScript control.
  • Store and update simple internal component state and synchronize it with properties/attributes.

In-Class Demo Jan 2026 term

To run the coding demo, you need to have your Student Workbook open in Visual Studio Code.

  1. Open the terminal window and paste in the following.

    Run from the root of your repository
    pnpm dlx tiged --disable-cache --force DG-InClass/SDEV-1150-A04-Jan-2026/sk/lesson-21 ./src/lesson-21
  2. Walk through the steps in the ReadMe.md of the new lesson.


In user.card.js, update the template markup by adding a Follow <button>:

user-card.js
...
<slot name="name" class="name"></slot>
<slot name="description" class="description"></slot>
<button>Follow</button>
...

This button will be used to dispatch a custom event when clicked with user id in event.detail.

To track the current state of being “followed”, add a private property to the UserCard:

user-card.js
...
constructor() {
super();
// Added property to track follow state
this._followed = false;
...

Now, we need to add support for toggling the followed state for the component.

user-card.js
...
constructor() {
...
}
_setFollow(value) {
this._followed = value;
}
...

The last thing to do is connect the button to the followed state. Add an event listener to the button in the constructor:

user-card.js
...
constructor() {
super();
this._followed = false;
const shadow = this.attachShadow({ mode: 'open' });
const content = template.content.cloneNode(true);
const img = content.querySelector('img');
img.src = this.getAttribute('avatar') || 'https://placehold.co/80x80/0077ff/ffffff';
this._btn = content.querySelector('button');
this._btn.addEventListener('click', () => {
this._setFollow(!this._followed);
console.log('Follow button clicked');
});
shadow.appendChild(content);
}
...

Update the example so that the state change is reflected in the UI. Add a private method for updating the state (_onFollow()) and update the text in the Follow button:

user-card.js
...
constructor() {
super();
this._followed = false;
const shadow = this.attachShadow({ mode: 'open' });
const content = template.content.cloneNode(true);
const img = content.querySelector('img');
img.src = this.getAttribute('avatar') || 'https://placehold.co/80x80/0077ff/ffffff';
this._btn = content.querySelector('button');
this._btn.addEventListener('click', () => this._onFollow());
shadow.appendChild(content);
}
_setFollow(value) {
this._followed = value;
this._btn.textContent = this._followed ? 'Following' : 'Follow';
}
// Follow button handler
_onFollow() {
this._setFollow(!this._followed);
}

Expose follow() and unfollow() methods that toggles an internal follow state. This will allow external APIs to interact with the component programmatically.

user-card.js
...
constructor() {
...
}
follow() {
this._setFollow(true);
}
unfollow() {
this._setFollow(false);
}
...

We can now set the followed state from our JS. Select the first user-card and follow() and unfollow() it programmatically:

main.js
...
// Programmatically follow the first user card
document.querySelector('user-card').follow();

When the page loads, you should now see that the first <user-card> is followed (e.g., the button text shows “Following”).

Open the console and enter the following to see the effect of unfollowing a user:

document.querySelector('user-card').unfollow();

Dispatch a Custom Event and Property for External Communication

Section titled “Dispatch a Custom Event and Property for External Communication”

Finally, let’s dispatch a custom event (follow-change) in case any listeners want to be notified when a <user-card> is followed/unfollowed. Add a read-only property to read the state of _followed as well:

user-card.js
...
constructor() {
...
}
...
_setFollow(value) {
this._followed = value;
this._btn.textContent = this._followed ? 'Following' : 'Follow';
// emit event so parent can react
this.dispatchEvent(new CustomEvent('follow-change', {
detail: { id: this.getAttribute('user-id') || null, followed: this._followed },
bubbles: true,
composed: true
}));
}
// Property to read followed state
get followed() {
return this._followed;
}
...

Listen for and respond to the event in the main application:

main.js
document.querySelector('user-card').addEventListener('follow-change', (e) => {
const card = e.target;
const name = card.querySelector('[slot=name]').textContent;
console.log(`User ${name} is ${card.followed? 'followed' : 'not followed'}`);
});

Add styling for the added Follow button.

If you have completed the previous student task of adding a “Details” section to the card, add the ability to toggle the visiblity of the “Details” within the component (e.g., CSS display update). Trigger an event whenever the details display is toggled.

Once you’re done making your own custom updates to the project, stage your files, commit your work, and push to the remote repository.

  1. Open a terminal in VS Code
  2. Stage all updated and created files:
Terminal window
git add .
  1. Commit the changes:
Terminal window
git commit -m 'Lesson 21 Example'
  1. Push your changes to the remote workbook repository:
Terminal window
git push origin main
IssueCauseSolution
Parent doesn’t receive eventCustomEvent not bubbled/composedUse { bubbles: true, composed: true } when dispatching
Property updates not reflectedUsing attributes only or missing setterImplement a setter that updates internal state and the rendered UI
Can’t call method on elementQuery happened before element parsed/connectedQuery after DOMContentLoaded or place code after import that defines the custom element