Files
testdrive-jsui/tutorials/Tutorial 4 Component Composition.md

258 lines
6.0 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 4: Component Composition
Welcome to the **fourth tutorial** for TestDriveUi building on the first three and introduces **component composition**.
Youll 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
Well create a small **dashboard** component called `<hello-dashboard>` that:
1. Renders multiple `<hello-world>` components.
2. Tracks how many greetings are currently visible.
3. Updates a counter when a new greeter is added.
4. 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`:
```javascript
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:
```bash
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`
```javascript
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:
```bash
npm test
```
✅ All should now pass.
---
## 🌐 3. Step 3 — Try it live
Add this to `src/index.html`:
```html
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
<hello-dashboard></hello-dashboard>
```
Then start the dev server:
```bash
npm run dev
```
Open [http://localhost:5173](http://localhost:5173)
Youll 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`:
```javascript
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`:
```javascript
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
Youve now implemented:
1. A simple reactive component (`hello-world`)
2. User interaction (input updates)
3. Property-based rendering
4. 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