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

6.0 KiB
Raw Blame History

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:

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 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:

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

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