2025-11-03 21:46:15 +01:00
# Tutorial 5: Component Communication
Welcome to the **fifth tutorial** takes you into one of the most important patterns in UI development: **communication between components.**
Up to now, your `<hello-world>` components have been independent. In this tutorial, you’ ll make them *talk* to their parent `<hello-dashboard>` component via **custom events** , keeping everything under **TDD** .
---
# 🧭 Tutorial 5 — Component Communication
### “Talking Components” — events between parent and child
---
## 🎯 Goal
We’ ll make each `<hello-world>` component **emit an event** when its name changes.
The `<hello-dashboard>` component will **listen to these events** and maintain a live **activity log** .
The flow will look like this:
```
[User types in < hello-world > input]
↓
< hello-world > fires event "name-changed"
↓
< hello-dashboard > listens, updates log
↓
Log list displays latest updates
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/components/hello-dashboard/hello-dashboard.events.test.js` :
```javascript
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("< hello-dashboard > (events)", () => {
it("shows a log section that updates when a child emits 'name-changed'", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
// Initial render
let log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("No activity yet");
// Simulate child event
const firstChild = el.shadowRoot.querySelector("hello-world");
const event = new CustomEvent("name-changed", {
detail: { oldName: "World", newName: "Agent" },
bubbles: true,
composed: true
});
firstChild.dispatchEvent(event);
await el.updateComplete;
log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("World → Agent");
});
});
```
Run:
```bash
npm test
```
The test fails (expected).
---
## 🧩 2. Step 2 — Update `<hello-world>` to fire events
Open `src/components/hello-world/hello-world.js`
and update `_onInput()` to emit an event whenever the name changes:
```javascript
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
this.dispatchEvent(
new CustomEvent("name-changed", {
detail: { oldName, newName: this.name },
bubbles: true,
composed: true
})
);
}
```
That’ s it — now `hello-world` tells the world when its name changes.
---
## ⚙️ 3. Step 3 — Update `<hello-dashboard>` to handle events
Edit `src/components/hello-dashboard/hello-dashboard.js` :
Add a new property and event handler:
```javascript
constructor() {
super();
this.greeters = ["World"];
this.logs = [];
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("name-changed", this._onNameChanged);
}
_onNameChanged = (e) => {
const { oldName, newName } = e.detail;
this.logs = [`${oldName} → ${newName}` , ...this.logs];
};
```
Now render the log area under the button:
```javascript
render() {
return html`
< div class = "container" >
< h2 > Hello Dashboard< / h2 >
< div class = "counter" > Total greetings: ${this.greeters.length}< / div >
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
< button @click =${ this . _addGreeter } > Add Greeting</ button >
< div class = "log" >
${this.logs.length === 0
? html`<p>No activity yet</p>`
: html`< ul >
${this.logs.map((l) => html`<li>${l}</li>` )}
< / ul > `}
< / div >
< / div >
`;
}
```
Run tests again:
```bash
npm test
```
✅ The event test should now pass.
---
## 🌐 4. Step 4 — Try it live
In `src/index.html` :
```html
< hello-dashboard > < / hello-dashboard >
```
Then:
```bash
npm run dev
```
Open the page —
Type into a greeter input, and you’ ll see a live **activity log** appear below the button:
```
Hello Dashboard
Total greetings: 1
Hello, World!
[ Add Greeting ]
Activity Log:
World → Agent
```
---
## 🧠 5. Step 5 — Add a test for multiple events (optional)
Extend `hello-dashboard.events.test.js` :
```javascript
it("keeps a running list of multiple name changes", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const child = el.shadowRoot.querySelector("hello-world");
const events = [
new CustomEvent("name-changed", { detail: { oldName: "World", newName: "A" }, bubbles: true, composed: true }),
new CustomEvent("name-changed", { detail: { oldName: "A", newName: "B" }, bubbles: true, composed: true })
];
child.dispatchEvent(events[0]);
child.dispatchEvent(events[1]);
await el.updateComplete;
const items = Array.from(el.shadowRoot.querySelectorAll(".log li")).map(li => li.textContent);
expect(items).to.deep.equal(["A → B", "World → A"]);
});
```
✅ It should pass too.
---
## 🔩 6. Key Concepts
| Concept | Explanation |
| -------------------------- | ----------------------------------------------------------------- |
| **CustomEvent** | A way for child components to send structured messages to parents |
| **Bubbling + composed** | Ensures events cross shadow DOM boundaries |
| **Reactive logs** | Updating an array property triggers rerender |
| **Hierarchical testing** | TDD across parent/child relationships |
| **Single source of truth** | Dashboard owns state; children only emit events |
---
## ✅ 7. Summary
You’ ve now built:
1. Independent reactive components (`hello-world` )
2. Composed layouts (`hello-dashboard` )
3. Two-way UI interaction (input updates)
4. **Inter-component event communication**
Your **TestDrive-UI** framework now supports:
* Pure front-end logic
* Reactive re-rendering
* Nested component tests
* Event-driven state management
---
Next step (Tutorial 6) will cover:
> **State synchronization** — introducing a shared store or context (e.g. using Lit’ s reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
Would you like to continue with that direction next?
2025-11-03 21:36:45 +01:00
xxx