# 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 `` components have been independent. In this tutorial, you’ll make them *talk* to their parent `` component via **custom events**, keeping everything under **TDD**. --- # 🧭 Tutorial 5 — Component Communication ### “Talking Components” — events between parent and child --- ## 🎯 Goal We’ll make each `` component **emit an event** when its name changes. The `` component will **listen to these events** and maintain a live **activity log**. The flow will look like this: ``` [User types in input] ↓ fires event "name-changed" ↓ 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(" (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 `` 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 `` 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`

Hello Dashboard

Total greetings: ${this.greeters.length}
${this.greeters.map( (name) => html`` )}
${this.logs.length === 0 ? html`

No activity yet

` : html`
    ${this.logs.map((l) => html`
  • ${l}
  • `)}
`}
`; } ``` Run tests again: ```bash npm test ``` ✅ The event test should now pass. --- ## 🌐 4. Step 4 — Try it live In `src/index.html`: ```html ``` 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? xxx