Files
testdrive-jsui/tutorials/Tutorial 5 Component Communication.md

252 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, youll 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
Well 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
})
);
}
```
Thats 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 youll 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
Youve 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 Lits reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
Would you like to continue with that direction next?
xxx