6.3 KiB
Tutorial 4: Component Composition
Welcome to the fourth tutorial for TestDriveUi building on the first three and introduces component composition.
You’ll learn how to make multiple Lit components interact while keeping the development test-driven and agent-friendly.
🧩 Tutorial 4 — Composing Components
“Hello Dashboard” — combining reusable components
🎯 Goal
We’ll create a small dashboard component called <hello-dashboard> that:
- Renders multiple
<hello-world>components. - Tracks how many greetings are currently visible.
- Updates a counter when a new greeter is added.
- Uses TDD for structure and behavior.
In short:
“A dashboard that shows several personalized greetings and a live counter.”
🧪 1. Step 1 — Write the failing test first
Create src/components/hello-dashboard/hello-dashboard.test.js:
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("<hello-dashboard>", () => {
it("renders a header and a counter", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const header = el.shadowRoot.querySelector("h2");
const counter = el.shadowRoot.querySelector(".counter");
expect(header.textContent).to.include("Hello Dashboard");
expect(counter.textContent).to.match(/Total greetings:/);
});
it("renders at least one <hello-world> component", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.be.greaterThan(0);
});
it("adds a new greeter when the Add button is clicked", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const addButton = el.shadowRoot.querySelector("button");
addButton.click();
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(2);
const counter = el.shadowRoot.querySelector(".counter").textContent;
expect(counter).to.include("2");
});
});
Run it:
npm test
All tests fail (expected).
⚙️ 2. Step 2 — Implement <hello-dashboard>
Create a new folder:
src/components/hello-dashboard/
and add this file:
src/components/hello-dashboard/hello-dashboard.js
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
export class HelloDashboard extends LitElement {
static styles = css`
.container {
font-family: system-ui, sans-serif;
padding: 1rem;
text-align: center;
}
.counter {
margin: 0.5rem 0 1rem 0;
font-weight: bold;
color: #007acc;
}
button {
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
button:hover {
background: #005fa3;
}
`;
constructor() {
super();
this.greeters = ["World"];
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
this.greeters = [...this.greeters, newName];
}
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>
`;
}
}
customElements.define("hello-dashboard", HelloDashboard);
Run tests again:
npm test
✅ All should now pass.
🌐 3. Step 3 — Try it live
Add this to src/index.html:
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
<hello-dashboard></hello-dashboard>
Then start the dev server:
npm run dev
Open http://localhost:5173 You’ll see:
Hello Dashboard
Total greetings: 1
Hello, World!
[Add Greeting]
Click Add Greeting — new greeters appear, and the counter updates automatically.
🧠 4. Step 4 — Extend your test coverage (optional)
Add to hello-dashboard.test.js:
it("propagates custom names correctly", () => {
const el = document.createElement("hello-dashboard");
el.greeters = ["Alpha", "Beta", "Gamma"];
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
const names = Array.from(greeters).map((g) => g.getAttribute("name"));
expect(names).to.deep.equal(["Alpha", "Beta", "Gamma"]);
});
📚 5. Step 5 — Optional Storybook story
src/components/hello-dashboard/hello-dashboard.stories.js:
import "./hello-dashboard.js";
export default {
title: "UI/Hello Dashboard"
};
export const Default = () => `<hello-dashboard></hello-dashboard>`;
🧩 6. Lessons learned
| Concept | What you practiced |
|---|---|
| Composition | One Lit component rendering others |
| Reactive arrays | Using property updates to trigger re-render |
| Dynamic templates | Rendering lists with .map() |
| Event testing | Simulating clicks and rechecking the DOM |
| Incremental TDD | Adding small features one test at a time |
✅ 7. Summary
You’ve now implemented:
- A simple reactive component (
hello-world) - User interaction (input updates)
- Property-based rendering
- Component composition (
hello-dashboard)
Your TestDrive-UI foundation now supports:
- Isolated unit tests (
Mocha + jsdom) - Composed component testing
- Interactive browser previews (
Vite)
Continue to Event communication between components — e.g., the dashboard listening to events fired by each greeter when they change their name.
xxx