generated from coulomb/repo-seed
221 lines
5.2 KiB
Markdown
221 lines
5.2 KiB
Markdown
# 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**.
|
||
> The `name` property 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`
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```javascript
|
||
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
|
||
|
||
```bash
|
||
npm test
|
||
```
|
||
|
||
✅ All three tests should now pass.
|
||
|
||
---
|
||
|
||
## ⚡ 5. Step 4 — Try it live
|
||
|
||
Update `src/index.html` to:
|
||
|
||
```html
|
||
<hello-world></hello-world>
|
||
```
|
||
|
||
Run:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```javascript
|
||
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`
|
||
|
||
```javascript
|
||
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:
|
||
|
||
1. Static rendering (`Hello World!`)
|
||
2. Reactive property rendering (`Hello, ${name}!`)
|
||
3. 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 |