chore: Fixed line endings i tutorials and provided Introduction

This commit is contained in:
2025-11-03 21:46:15 +01:00
parent 03656a1f19
commit 7ef23c2905
10 changed files with 2275 additions and 2076 deletions

View File

@@ -1,322 +1,322 @@
# Tutorial 6: Shared State
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
Youll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
---
# ⚡ Tutorial 6 — Shared State
### “The Store Pattern” — Synchronizing Multiple Components
---
## 🎯 Goal
Well create a **store** that holds the global list of greeter names.
All `<hello-world>` components will automatically update when the store changes.
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
**Architecture overview:**
```
+------------------------+
| HelloDashboard |
| (manages store ops) |
| |
| ┌────────────┐ |
| | HelloWorld | <── store.subscribe()
| └────────────┘ |
| ┌────────────┐ |
| | HelloWorld | |
| └────────────┘ |
+------------------------+
Shared Store
(holds array of greeters)
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/store/hello-store.test.js`:
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore", () => {
it("starts with one default greeter", () => {
const state = helloStore.getState();
expect(state.greeters).to.deep.equal(["World"]);
});
it("can add a new greeter", () => {
helloStore.addGreeter("Alice");
const state = helloStore.getState();
expect(state.greeters).to.include("Alice");
});
it("notifies subscribers on change", (done) => {
const unsubscribe = helloStore.subscribe((state) => {
expect(state.greeters).to.include("Bob");
unsubscribe();
done();
});
helloStore.addGreeter("Bob");
});
});
```
Run:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement the store
Create a new file:
`src/store/hello-store.js`
```javascript
class HelloStore {
constructor() {
this.state = { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state); // immediate call with current state
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = {
...this.state,
greeters: [...this.state.greeters, name]
};
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
```
Re-run:
```bash
npm test
```
✅ All tests should now pass.
---
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
Modify `src/components/hello-dashboard/hello-dashboard.js`:
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
import { helloStore } from "../../store/hello-store.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;
}
`;
constructor() {
super();
this.greeters = [];
}
connectedCallback() {
super.connectedCallback();
this.unsubscribe = helloStore.subscribe((state) => {
this.greeters = state.greeters;
});
}
disconnectedCallback() {
this.unsubscribe?.();
super.disconnectedCallback();
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
helloStore.addGreeter(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);
```
---
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
```javascript
import { helloStore } from "../../store/hello-store.js";
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
helloStore.renameGreeter(oldName, this.name);
}
```
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
---
## 🧪 5. Step 5 — Integration test (optional)
Add `src/integration/dashboard-store.test.js`:
```javascript
import "../components/hello-dashboard/hello-dashboard.js";
import { helloStore } from "../store/hello-store.js";
describe("HelloDashboard and HelloStore integration", () => {
it("renders as many greeters as store entries", async () => {
helloStore.addGreeter("Eve");
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
});
});
```
Run:
```bash
npm test
```
✅ All pass again.
---
## 🌐 6. Step 6 — Try it live
Add to your `index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Run:
```bash
npm run dev
```
* Click “Add Greeting” → new greeters appear instantly.
* Type into any greeter → all components referencing that name update automatically.
Congratulations — youve just implemented **a reactive state system** entirely in vanilla JS + Lit!
---
## 📘 7. Key Concepts
| Concept | Description |
| -------------------------- | ----------------------------------------------------- |
| **Centralized state** | One store manages the data for all components |
| **Observer pattern** | Components subscribe to updates, re-render on change |
| **Reactive sync** | Changes propagate automatically without prop drilling |
| **TDD-first store design** | Ensures predictable, testable state behavior |
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
---
## ✅ 8. Summary
At this point, your **TestDrive-UI** ecosystem includes:
| Layer | Responsibility |
| ----------------- | ---------------------------- |
| `hello-world` | Simple reactive component |
| `hello-dashboard` | Container and coordinator |
| `hello-store` | Shared, observable state |
| Tests | Guard behavior at each level |
---
## 🔮 9. Next Tutorial (optional direction)
You can now evolve in one of two directions:
1. **Tutorial 7 — Persistence**
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
2. **Tutorial 8 — Agent Automation**
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
---
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
xxx
# Tutorial 6: Shared State
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
Youll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
---
# ⚡ Tutorial 6 — Shared State
### “The Store Pattern” — Synchronizing Multiple Components
---
## 🎯 Goal
Well create a **store** that holds the global list of greeter names.
All `<hello-world>` components will automatically update when the store changes.
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
**Architecture overview:**
```
+------------------------+
| HelloDashboard |
| (manages store ops) |
| |
| ┌────────────┐ |
| | HelloWorld | <── store.subscribe()
| └────────────┘ |
| ┌────────────┐ |
| | HelloWorld | |
| └────────────┘ |
+------------------------+
Shared Store
(holds array of greeters)
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/store/hello-store.test.js`:
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore", () => {
it("starts with one default greeter", () => {
const state = helloStore.getState();
expect(state.greeters).to.deep.equal(["World"]);
});
it("can add a new greeter", () => {
helloStore.addGreeter("Alice");
const state = helloStore.getState();
expect(state.greeters).to.include("Alice");
});
it("notifies subscribers on change", (done) => {
const unsubscribe = helloStore.subscribe((state) => {
expect(state.greeters).to.include("Bob");
unsubscribe();
done();
});
helloStore.addGreeter("Bob");
});
});
```
Run:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement the store
Create a new file:
`src/store/hello-store.js`
```javascript
class HelloStore {
constructor() {
this.state = { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state); // immediate call with current state
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = {
...this.state,
greeters: [...this.state.greeters, name]
};
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
```
Re-run:
```bash
npm test
```
✅ All tests should now pass.
---
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
Modify `src/components/hello-dashboard/hello-dashboard.js`:
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
import { helloStore } from "../../store/hello-store.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;
}
`;
constructor() {
super();
this.greeters = [];
}
connectedCallback() {
super.connectedCallback();
this.unsubscribe = helloStore.subscribe((state) => {
this.greeters = state.greeters;
});
}
disconnectedCallback() {
this.unsubscribe?.();
super.disconnectedCallback();
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
helloStore.addGreeter(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);
```
---
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
```javascript
import { helloStore } from "../../store/hello-store.js";
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
helloStore.renameGreeter(oldName, this.name);
}
```
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
---
## 🧪 5. Step 5 — Integration test (optional)
Add `src/integration/dashboard-store.test.js`:
```javascript
import "../components/hello-dashboard/hello-dashboard.js";
import { helloStore } from "../store/hello-store.js";
describe("HelloDashboard and HelloStore integration", () => {
it("renders as many greeters as store entries", async () => {
helloStore.addGreeter("Eve");
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
});
});
```
Run:
```bash
npm test
```
✅ All pass again.
---
## 🌐 6. Step 6 — Try it live
Add to your `index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Run:
```bash
npm run dev
```
* Click “Add Greeting” → new greeters appear instantly.
* Type into any greeter → all components referencing that name update automatically.
Congratulations — youve just implemented **a reactive state system** entirely in vanilla JS + Lit!
---
## 📘 7. Key Concepts
| Concept | Description |
| -------------------------- | ----------------------------------------------------- |
| **Centralized state** | One store manages the data for all components |
| **Observer pattern** | Components subscribe to updates, re-render on change |
| **Reactive sync** | Changes propagate automatically without prop drilling |
| **TDD-first store design** | Ensures predictable, testable state behavior |
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
---
## ✅ 8. Summary
At this point, your **TestDrive-UI** ecosystem includes:
| Layer | Responsibility |
| ----------------- | ---------------------------- |
| `hello-world` | Simple reactive component |
| `hello-dashboard` | Container and coordinator |
| `hello-store` | Shared, observable state |
| Tests | Guard behavior at each level |
---
## 🔮 9. Next Tutorial (optional direction)
You can now evolve in one of two directions:
1. **Tutorial 7 — Persistence**
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
2. **Tutorial 8 — Agent Automation**
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
---
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
xxx