221 lines
5.4 KiB
Markdown
221 lines
5.4 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
|