5.4 KiB
Tutorial 3: Bi-Directional Data Binding
This third tutorial builds directly on your hello-world component and introduces two-way interaction (bi-directional data binding).
You’ll learn how to let the user type into an input field and see the greeting update live — while continuing to follow a TDD-first approach.
🧭 1. Goal
The
<hello-world>component should display a greeting and an input box. Typing into the input should update the greeting in real time. Thenameproperty should still be readable/writable programmatically.
🧪 2. Step 1 — Write the failing test first
Create a new test file:
src/components/hello-world/hello-world.input.test.js
import "./hello-world.js";
describe("<hello-world> (interactive input)", () => {
it("renders an input element and shows default greeting", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
const greeting = el.shadowRoot.querySelector(".greeting");
expect(input).to.exist;
expect(greeting.textContent.trim()).to.equal("Hello, World!");
});
it("updates greeting when user types into input", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
it("reflects property changes in input value", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Nova";
await el.updateComplete;
const input = el.shadowRoot.querySelector("input");
expect(input.value).to.equal("Nova");
});
});
Run tests:
npm test
They will all fail — as expected.
🧩 3. Step 2 — Implement the feature
Edit src/components/hello-world/hello-world.js and replace the render logic:
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = "World";
}
static styles = css`
.container {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
}
input {
font-size: 1rem;
margin-top: 1rem;
padding: 0.3rem 0.6rem;
border: 1px solid #ccc;
border-radius: 6px;
width: 60%;
text-align: center;
}
`;
render() {
return html`
<div class="container">
<div class="greeting">Hello, ${this.name}!</div>
<input
type="text"
.value=${this.name}
@input=${this._onInput}
aria-label="Name input"
/>
</div>
`;
}
_onInput(event) {
this.name = event.target.value;
}
}
customElements.define("hello-world", HelloWorld);
🧪 4. Step 3 — Run the tests again
npm test
✅ All three tests should now pass.
⚡ 5. Step 4 — Try it live
Update src/index.html to:
<hello-world></hello-world>
Run:
npm run dev
In your browser:
- You’ll see “Hello, World!”
- Type “Coulomb” in the input box.
- The greeting updates instantly: Hello, Coulomb!
🧠 6. Step 5 — Explore two-way binding manually
Open DevTools Console and run:
document.querySelector("hello-world").name = "Bernd";
The input value updates automatically, and the greeting reflects the change too. That’s the power of Lit’s reactive updates combined with the native DOM event loop.
📚 7. Step 6 — Optional Storybook story
src/components/hello-world/hello-world.interactive.stories.js
import "./hello-world.js";
export default {
title: "UI/Hello World (Interactive)"
};
export const Interactive = () => `<hello-world></hello-world>`;
When you add Storybook later, this story will provide a live, interactive playground.
🔍 8. Key TDD lessons learned
| Concept | Explanation |
|---|---|
| Event testing | Simulate user input with dispatchEvent(new Event('input')) |
| Reactive updates | Use await el.updateComplete to wait for re-render |
| Two-way binding | Property changes update DOM; DOM events update property |
| Isolation | jsdom tests confirm behavior without running a browser |
✅ 9. Summary
You’ve now completed:
- Static rendering (
Hello World!) - Reactive property rendering (
Hello, ${name}!) - Bi-directional interaction (live input → UI → property → UI)
You can now test-drive any UI component using the same methodology.
Continue to the fourth tutorial on component composition — i.e., building a small dashboard that uses multiple custom components together, still under test Control.
xxx