@fun-land/fun-web 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +515 -0
- package/coverage/clover.xml +136 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/dom.ts.html +961 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/mount.ts.html +202 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/state.ts.html +91 -0
- package/coverage/lcov.info +260 -0
- package/dist/esm/src/dom.d.ts +85 -0
- package/dist/esm/src/dom.js +207 -0
- package/dist/esm/src/dom.js.map +1 -0
- package/dist/esm/src/index.d.ts +7 -0
- package/dist/esm/src/index.js +5 -0
- package/dist/esm/src/index.js.map +1 -0
- package/dist/esm/src/mount.d.ts +21 -0
- package/dist/esm/src/mount.js +27 -0
- package/dist/esm/src/mount.js.map +1 -0
- package/dist/esm/src/state.d.ts +2 -0
- package/dist/esm/src/state.js +3 -0
- package/dist/esm/src/state.js.map +1 -0
- package/dist/esm/src/types.d.ts +3 -0
- package/dist/esm/src/types.js +3 -0
- package/dist/esm/src/types.js.map +1 -0
- package/dist/esm/tsconfig.publish.tsbuildinfo +1 -0
- package/dist/src/dom.d.ts +85 -0
- package/dist/src/dom.js +224 -0
- package/dist/src/dom.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mount.d.ts +21 -0
- package/dist/src/mount.js +31 -0
- package/dist/src/mount.js.map +1 -0
- package/dist/src/state.d.ts +2 -0
- package/dist/src/state.js +7 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/types.d.ts +3 -0
- package/dist/src/types.js +4 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/eslint.config.js +54 -0
- package/examples/README.md +67 -0
- package/examples/counter/bundle.js +219 -0
- package/examples/counter/counter.ts +112 -0
- package/examples/counter/index.html +44 -0
- package/examples/todo-app/Todo.ts +79 -0
- package/examples/todo-app/index.html +142 -0
- package/examples/todo-app/todo-app.ts +120 -0
- package/examples/todo-app/todo-bundle.js +410 -0
- package/jest.config.js +5 -0
- package/package.json +49 -0
- package/src/dom.test.ts +768 -0
- package/src/dom.ts +296 -0
- package/src/index.ts +25 -0
- package/src/mount.test.ts +220 -0
- package/src/mount.ts +39 -0
- package/src/state.test.ts +225 -0
- package/src/state.ts +2 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +16 -0
- package/tsconfig.publish.json +6 -0
- package/wip/hx-magic-properties-plan.md +575 -0
- package/wip/next.md +22 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# @fun-land/fun-web
|
|
2
|
+
|
|
3
|
+
A lightweight web component library for building reactive UIs with native DOM and compositional state management.
|
|
4
|
+
|
|
5
|
+
## Why fun-web?
|
|
6
|
+
|
|
7
|
+
Build web UIs without a framework using:
|
|
8
|
+
- **Run-once components** that never re-render (sidesteps stale closures, memoization, render cycles)
|
|
9
|
+
- **Reactive state** following the FunState compositional pattern
|
|
10
|
+
- **Native DOM** elements (no virtual DOM)
|
|
11
|
+
- **TypeScript-first** APIs with full type inference
|
|
12
|
+
- **AbortSignal** for automatic cleanup
|
|
13
|
+
|
|
14
|
+
Perfect for embedding interactive components in static sites, building lightweight tools, or avoiding framework lock-in.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Subscription-based reactivity** - State changes automatically update DOM
|
|
19
|
+
- **Keyed list rendering** - Efficient reconciliation without virtual DOM
|
|
20
|
+
- **Type-safe utilities** - Element types and events inferred automatically
|
|
21
|
+
- **Memory-safe by default** - AbortSignal prevents leaks
|
|
22
|
+
- **Framework-agnostic** - Just functions and elements
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @fun-land/fun-web @fun-land/accessor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import {
|
|
34
|
+
h,
|
|
35
|
+
funState,
|
|
36
|
+
mount,
|
|
37
|
+
bindProperty,
|
|
38
|
+
on,
|
|
39
|
+
type Component,
|
|
40
|
+
type FunState,
|
|
41
|
+
} from "@fun-land/fun-web";
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const Counter: Component<{state: FunState<number>}> = (signal, {state}) => {
|
|
45
|
+
// Component runs once - no re-rendering on state changes
|
|
46
|
+
const button = h("button", {}, `Count: ${state.get()}`);
|
|
47
|
+
|
|
48
|
+
// bindProperty subscribes to the past state and updates named property
|
|
49
|
+
bindProperty(button, "textContent", state, signal);
|
|
50
|
+
|
|
51
|
+
// Event handlers never go stale (component doesn't re-run)
|
|
52
|
+
on(button, "click", () => state.mod((n) => n + 1), signal);
|
|
53
|
+
|
|
54
|
+
return button;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Create reactive state and mount
|
|
58
|
+
const mounted = mount(Counter, { state: funState(0) }, document.body);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Core Concepts
|
|
62
|
+
|
|
63
|
+
### Components Run Once
|
|
64
|
+
|
|
65
|
+
**The most important difference from React/Vue/Svelte:** fun-web components execute once when mounted, set up subscriptions, and never re-run.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const Counter: Component<{ count: FunState<number> }> = (signal, props) => {
|
|
69
|
+
console.log("Component runs once");
|
|
70
|
+
|
|
71
|
+
const display = h("div");
|
|
72
|
+
const button = h("button", {}, "Increment");
|
|
73
|
+
|
|
74
|
+
// This subscription handles updates, not re-rendering
|
|
75
|
+
bindProperty(display, "textContent", props.count, signal);
|
|
76
|
+
on(button, "click", () => props.count.mod(n => n + 1), signal);
|
|
77
|
+
|
|
78
|
+
return h("div", {}, [display, button]);
|
|
79
|
+
// Component function exits, but subscriptions keep working
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
When `count` changes, the component **doesn't re-run**. Instead, the `bindProperty` subscription updates `display.textContent` directly.
|
|
84
|
+
|
|
85
|
+
**Problems this sidesteps:**
|
|
86
|
+
|
|
87
|
+
| Framework components | fun-web components |
|
|
88
|
+
|---------------------|-------------------|
|
|
89
|
+
| Re-execute on every state change | Execute once, subscriptions handle updates |
|
|
90
|
+
| Need memoization (`useMemo`, `useCallback`) | No re-execution = no stale closures to worry about |
|
|
91
|
+
| Virtual DOM diffing overhead | Direct DOM updates via subscriptions |
|
|
92
|
+
| Rules about when you can update state | Update state whenever you want |
|
|
93
|
+
| "Render cycles" and batching complexity | No render cycles, updates are just function calls |
|
|
94
|
+
|
|
95
|
+
**What this means:**
|
|
96
|
+
- Component functions are just **constructors** - they build UI once and set up reactive bindings
|
|
97
|
+
- State changes trigger **targeted DOM updates**, not full re-renders
|
|
98
|
+
- No "rendering" concept - just imperative DOM manipulation driven by reactive state
|
|
99
|
+
- Simpler mental model: "When X changes, update this specific DOM property"
|
|
100
|
+
|
|
101
|
+
**Concrete example:**
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// In React - component re-executes on every count change
|
|
105
|
+
function ReactCounter() {
|
|
106
|
+
const [count, setCount] = useState(0);
|
|
107
|
+
console.log("Re-rendering!"); // Logs on every state change
|
|
108
|
+
|
|
109
|
+
// Need useCallback to prevent infinite re-renders
|
|
110
|
+
const increment = useCallback(() => setCount(c => c + 1), []);
|
|
111
|
+
|
|
112
|
+
return <div>{count} <button onClick={increment}>+</button></div>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// In fun-web - component executes once
|
|
116
|
+
const FunWebCounter: Component<{ count: FunState<number> }> = (signal, props) => {
|
|
117
|
+
console.log("Mounting!"); // Logs once, never again
|
|
118
|
+
|
|
119
|
+
const display = h("div");
|
|
120
|
+
const button = h("button", {}, "+");
|
|
121
|
+
|
|
122
|
+
// No useCallback needed - this closure never goes stale
|
|
123
|
+
on(button, "click", () => props.count.mod(n => n + 1), signal);
|
|
124
|
+
bindProperty(display, "textContent", props.count, signal);
|
|
125
|
+
|
|
126
|
+
return h("div", {}, [display, button]);
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
In React, clicking the button causes the entire component to re-execute. In fun-web, clicking the button just calls `count.mod()`, which triggers the `bindProperty` subscription to update `display.textContent`. The component function never runs again.
|
|
131
|
+
|
|
132
|
+
### FunState - Reactive State with Subscriptions
|
|
133
|
+
|
|
134
|
+
`FunState<T>` provides reactive compositional state [@fun-land/fun-state](../fun-state):
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const userState = funState({ name: "Alice", age: 30 });
|
|
138
|
+
|
|
139
|
+
// Get current value
|
|
140
|
+
userState.prop("name").get(); // "Alice"
|
|
141
|
+
|
|
142
|
+
// Update state
|
|
143
|
+
userState.prop("age").set(31);
|
|
144
|
+
|
|
145
|
+
// Subscribe to changes (cleaned up when signal aborts)
|
|
146
|
+
userState.prop("name").subscribe(signal, (name) => {
|
|
147
|
+
element.textContent = name; // runs when name changes
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Component Signature
|
|
152
|
+
|
|
153
|
+
Components are functions that receive an AbortSignal and props:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
type Component<Props = {}> = (
|
|
157
|
+
signal: AbortSignal, // For cleanup
|
|
158
|
+
props: Props // Data (static or reactive)
|
|
159
|
+
) => Element // Returns plain DOM element
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Props can contain anything:** Static values, callbacks, reactive state, or any combination. There's no distinction between "props" and "state" - state is just data that happens to have `.get()` and `.set()` methods.
|
|
163
|
+
|
|
164
|
+
**Examples:**
|
|
165
|
+
```typescript
|
|
166
|
+
// Static props only
|
|
167
|
+
const Static: Component<{ title: string }> = (signal, props) =>
|
|
168
|
+
h("h1", {}, props.title);
|
|
169
|
+
|
|
170
|
+
// Reactive state in props
|
|
171
|
+
const Counter: Component<{ count: FunState<number> }> = (signal, props) =>
|
|
172
|
+
h("div", {}, String(props.count.get()));
|
|
173
|
+
|
|
174
|
+
// Mix of static and reactive
|
|
175
|
+
const Dashboard: Component<{
|
|
176
|
+
onLogout: () => void;
|
|
177
|
+
user: FunState<User>;
|
|
178
|
+
settings: FunState<Settings>;
|
|
179
|
+
}> = (signal, props) => {
|
|
180
|
+
// props.onLogout is a static callback
|
|
181
|
+
// props.user is reactive state
|
|
182
|
+
// props.settings is reactive state
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Reactivity Patterns
|
|
187
|
+
|
|
188
|
+
**Use `bindProperty` for simple one-way bindings:**
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const nameEl = h("div");
|
|
192
|
+
const nameState: FunState<string> = state.prop("user").prop("name");
|
|
193
|
+
|
|
194
|
+
bindProperty(nameEl, "textContent", nameState, signal);
|
|
195
|
+
// nameEl.textContent stays in sync with nameState
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Use `on` for events:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const button = h("button", {}, "Click me");
|
|
202
|
+
on(button, "click", (e: MouseEvent & { currentTarget: HTMLButtonElement }) => {
|
|
203
|
+
console.log(e.currentTarget.textContent);
|
|
204
|
+
}, signal);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Chain for two-way bindings:**
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const input = on(
|
|
211
|
+
bindProperty(
|
|
212
|
+
h("input", { type: "text" }),
|
|
213
|
+
"value",
|
|
214
|
+
state.prop("name"),
|
|
215
|
+
signal
|
|
216
|
+
),
|
|
217
|
+
"input",
|
|
218
|
+
(e) => state.prop("name").set(e.currentTarget.value),
|
|
219
|
+
signal
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Use `.subscribe()` for complex logic:**
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
state.subscribe(signal, (s) => {
|
|
227
|
+
element.textContent = s.count > 100 ? "Max!" : String(s.count);
|
|
228
|
+
element.className = s.count > 100 ? "maxed" : "normal";
|
|
229
|
+
element.setAttribute("aria-label", `Count: ${s.count}`);
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Cleanup with AbortSignal
|
|
234
|
+
|
|
235
|
+
All subscriptions and event listeners require an AbortSignal. When the signal aborts, everything cleans up automatically:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const MyComponent: Component<{ state: FunState<State> }> = (signal, props) => {
|
|
239
|
+
const display = h("div");
|
|
240
|
+
const button = h("button");
|
|
241
|
+
|
|
242
|
+
// All three clean up when signal aborts
|
|
243
|
+
bindProperty(display, "textContent", props.state.prop("count"), signal);
|
|
244
|
+
on(button, "click", () => props.state.mod(increment), signal);
|
|
245
|
+
props.state.subscribe(signal, (s) => console.log("Changed:", s));
|
|
246
|
+
|
|
247
|
+
return h("div", {}, [display, button]);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const mounted = mount(MyComponent, { state }, container);
|
|
251
|
+
mounted.unmount(); // Aborts signal → everything cleans up
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For the most part you won't have to worry about the abort signal if you use the helpers provided.
|
|
255
|
+
|
|
256
|
+
## Best Practices
|
|
257
|
+
|
|
258
|
+
**Prefer helpers over manual subscriptions:**
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// ✅ Good
|
|
262
|
+
bindProperty(element, "textContent", state.prop("count"), signal);
|
|
263
|
+
|
|
264
|
+
// ❌ Avoid (when bindProperty works)
|
|
265
|
+
state.prop("count").subscribe(signal, (count) => {
|
|
266
|
+
element.textContent = String(count);
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Use `on()` for type safety:**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ✅ Good - types inferred
|
|
274
|
+
on(button, "click", (e) => {
|
|
275
|
+
e.currentTarget.disabled = true; // TypeScript knows it's HTMLButtonElement
|
|
276
|
+
}, signal);
|
|
277
|
+
|
|
278
|
+
// ❌ Avoid - loses type information
|
|
279
|
+
button.addEventListener("click", (e) => {
|
|
280
|
+
(e.currentTarget as HTMLButtonElement).disabled = true;
|
|
281
|
+
}, { signal });
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Manual subscriptions for complex updates:**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ✅ complex updates may need subscribe
|
|
288
|
+
state.subscribe(signal, (s) => {
|
|
289
|
+
// Multiple DOM updates based on complex conditions
|
|
290
|
+
if (s.status === "loading") {
|
|
291
|
+
spinner.style.display = "block";
|
|
292
|
+
button.disabled = true;
|
|
293
|
+
} else {
|
|
294
|
+
spinner.style.display = "none";
|
|
295
|
+
button.disabled = false;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## API Reference
|
|
301
|
+
|
|
302
|
+
### DOM Utilities
|
|
303
|
+
|
|
304
|
+
#### `h`
|
|
305
|
+
|
|
306
|
+
Declaratively create an HTML Element with properties and children.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const div = h("div", { id: "app" }, [
|
|
310
|
+
h("h1", null, "Hello"),
|
|
311
|
+
h("input", { type: "text", value: "foo" })
|
|
312
|
+
]);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Attribute conventions:**
|
|
316
|
+
- Dashed properties (`data-*`, `aria-*`) → `setAttribute()`
|
|
317
|
+
- Don't event bind with properties, use `on()`
|
|
318
|
+
- Everything else → property assignment
|
|
319
|
+
|
|
320
|
+
#### bindProperty
|
|
321
|
+
```ts
|
|
322
|
+
<E extends Element, K extends keyof E>(
|
|
323
|
+
el: E,
|
|
324
|
+
key: K,
|
|
325
|
+
fs: FunState<E[K]>,
|
|
326
|
+
signal: AbortSignal
|
|
327
|
+
): E
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Bind element property to state. Returns element for chaining.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const input: HTMLInputElement = h("input");
|
|
334
|
+
bindProperty(input, "value", state.prop("name"), signal);
|
|
335
|
+
// input.value syncs with state.name
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### on
|
|
339
|
+
```ts
|
|
340
|
+
<E extends Element, K extends keyof HTMLElementEventMap>(
|
|
341
|
+
el: E,
|
|
342
|
+
type: K,
|
|
343
|
+
handler: (ev: HTMLElementEventMap[K] & { currentTarget: E }) => void,
|
|
344
|
+
signal: AbortSignal
|
|
345
|
+
): E
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Add type-safe event listener. Returns element for chaining.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
on(h("button"), "click", (e) => {
|
|
352
|
+
// e.currentTarget is typed as HTMLButtonElement
|
|
353
|
+
e.currentTarget.disabled = true;
|
|
354
|
+
}, signal);
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### keyedChildren
|
|
358
|
+
```ts
|
|
359
|
+
<T extends { key: string }>(
|
|
360
|
+
parent: Element,
|
|
361
|
+
signal: AbortSignal,
|
|
362
|
+
list: FunState<T[]>,
|
|
363
|
+
renderRow: (row: {
|
|
364
|
+
signal: AbortSignal;
|
|
365
|
+
state: FunState<T>;
|
|
366
|
+
remove: () => void;
|
|
367
|
+
}) => Element
|
|
368
|
+
): KeyedChildren<T>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Render and reconcile keyed lists efficiently. Each row gets its own AbortSignal for cleanup and a focused state.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
interface Todo {
|
|
375
|
+
key: string;
|
|
376
|
+
label: string;
|
|
377
|
+
done: boolean;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const todos: FunState<Todo[]> = funState([
|
|
381
|
+
{ key: "a", label: "First", done: false }
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
keyedChildren(h("ul"), signal, todos, (row) => {
|
|
385
|
+
const li = h("li");
|
|
386
|
+
|
|
387
|
+
// row.state is a focused FunState<Todo> for this item
|
|
388
|
+
bindProperty(li, "textContent", row.state.prop("label"), row.signal);
|
|
389
|
+
|
|
390
|
+
// row.remove() removes this item from the list
|
|
391
|
+
const deleteBtn = h("button", { textContent: "Delete" });
|
|
392
|
+
on(deleteBtn, "click", row.remove, row.signal);
|
|
393
|
+
|
|
394
|
+
li.appendChild(deleteBtn);
|
|
395
|
+
return li;
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### $ and $$ - DOM Query Utilities
|
|
400
|
+
|
|
401
|
+
Convenient shortcuts for `querySelector` and `querySelectorAll` with better TypeScript support.
|
|
402
|
+
|
|
403
|
+
**`$<T extends Element>(selector: string): T | undefined`**
|
|
404
|
+
|
|
405
|
+
Query a single element. Returns `undefined` instead of `null` if not found.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
const button = $<HTMLButtonElement>("#submit-btn");
|
|
409
|
+
if (button) {
|
|
410
|
+
button.disabled = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const input = $<HTMLInputElement>(".name-input");
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**`$$<T extends Element>(selector: string): T[]`**
|
|
417
|
+
|
|
418
|
+
Query multiple elements. Returns a real Array (not NodeList) for better ergonomics.
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
const items = $$<HTMLDivElement>(".item");
|
|
422
|
+
items.forEach(item => item.classList.add("active"));
|
|
423
|
+
|
|
424
|
+
// Array methods work directly
|
|
425
|
+
const texts = $$(".label").map(el => el.textContent);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Other utilities
|
|
429
|
+
|
|
430
|
+
All return the element for chaining:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
text: (content: string | number) => (el: Element) => Element
|
|
434
|
+
attr: (name: string, value: string) => (el: Element) => Element
|
|
435
|
+
attrs: (obj: Record<string, string>) => (el: Element) => Element
|
|
436
|
+
addClass: (...classes: string[]) => (el: Element) => Element
|
|
437
|
+
removeClass: (...classes: string[]) => (el: Element) => Element
|
|
438
|
+
toggleClass: (className: string, force?: boolean) => (el: Element) => Element
|
|
439
|
+
append: (...children: Element[]) => (el: Element) => Element
|
|
440
|
+
pipeEndo: <T>(...fns: Array<(x: T) => T>) => (x: T) => T
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Mounting
|
|
444
|
+
|
|
445
|
+
#### mount
|
|
446
|
+
```ts
|
|
447
|
+
<Props>(component: Component<Props>, props: Props, container: Element): MountedComponent
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Mount component to DOM and manage lifecycle. You probably only need to call this once in your app.
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const state = funState({ count: 0 });
|
|
454
|
+
const mounted = mount(
|
|
455
|
+
Counter,
|
|
456
|
+
{ label: "Clicks", state },
|
|
457
|
+
document.body
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
mounted.unmount(); // Cleanup
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Returns:**
|
|
464
|
+
```typescript
|
|
465
|
+
interface MountedComponent {
|
|
466
|
+
element: Element
|
|
467
|
+
unmount(): void
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Composition
|
|
472
|
+
|
|
473
|
+
Components compose by calling other components and passing focused state via props:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { prop } from "@fun-land/accessor";
|
|
477
|
+
|
|
478
|
+
interface AppState {
|
|
479
|
+
user: UserData;
|
|
480
|
+
settings: Settings;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const App: Component<{ state: FunState<AppState> }> = (signal, props) =>
|
|
484
|
+
h("div", {}, [
|
|
485
|
+
UserProfile(signal, {
|
|
486
|
+
editable: true,
|
|
487
|
+
state: props.state.focus(prop<AppState>()("user")),
|
|
488
|
+
}),
|
|
489
|
+
SettingsPanel(signal, {
|
|
490
|
+
state: props.state.focus(prop<AppState>()("settings")),
|
|
491
|
+
})
|
|
492
|
+
]);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Focused states only trigger updates when their slice changes. Components can also create **local state** using `funState()` instead of receiving it via props - there's no distinction, state is just data.
|
|
496
|
+
|
|
497
|
+
## Examples
|
|
498
|
+
|
|
499
|
+
See working examples:
|
|
500
|
+
- `examples/counter/counter.ts` - Basic reactivity and composition
|
|
501
|
+
- `examples/todo-app/todo-app.ts` - Keyed lists and form binding
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
yarn build:examples
|
|
505
|
+
open examples/counter/index.html
|
|
506
|
+
open examples/todo-app/todo.html
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Status
|
|
510
|
+
|
|
511
|
+
**Experimental** - APIs may change.
|
|
512
|
+
|
|
513
|
+
## License
|
|
514
|
+
|
|
515
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<coverage generated="1768272411491" clover="3.2.0">
|
|
3
|
+
<project timestamp="1768272411491" name="All files">
|
|
4
|
+
<metrics statements="121" coveredstatements="118" conditionals="38" coveredconditionals="34" methods="37" coveredmethods="32" elements="196" coveredelements="184" complexity="0" loc="121" ncloc="121" packages="1" files="3" classes="3"/>
|
|
5
|
+
<file name="dom.ts" path="/Users/jethrolarson/develop/fun-land/packages/fun-web/src/dom.ts">
|
|
6
|
+
<metrics statements="113" coveredstatements="110" conditionals="38" coveredconditionals="34" methods="34" coveredmethods="29"/>
|
|
7
|
+
<line num="4" count="2" type="stmt"/>
|
|
8
|
+
<line num="18" count="2" type="stmt"/>
|
|
9
|
+
<line num="23" count="32" type="stmt"/>
|
|
10
|
+
<line num="26" count="32" type="cond" truecount="2" falsecount="0"/>
|
|
11
|
+
<line num="27" count="13" type="stmt"/>
|
|
12
|
+
<line num="28" count="18" type="cond" truecount="2" falsecount="0"/>
|
|
13
|
+
<line num="30" count="16" type="cond" truecount="4" falsecount="0"/>
|
|
14
|
+
<line num="32" count="3" type="stmt"/>
|
|
15
|
+
<line num="33" count="3" type="stmt"/>
|
|
16
|
+
<line num="34" count="13" type="cond" truecount="4" falsecount="0"/>
|
|
17
|
+
<line num="36" count="2" type="stmt"/>
|
|
18
|
+
<line num="39" count="11" type="stmt"/>
|
|
19
|
+
<line num="45" count="32" type="cond" truecount="2" falsecount="0"/>
|
|
20
|
+
<line num="46" count="17" type="stmt"/>
|
|
21
|
+
<line num="49" count="32" type="stmt"/>
|
|
22
|
+
<line num="55" count="2" type="stmt"/>
|
|
23
|
+
<line num="59" count="29" type="cond" truecount="2" falsecount="0"/>
|
|
24
|
+
<line num="60" count="12" type="stmt"/>
|
|
25
|
+
<line num="61" count="24" type="cond" truecount="2" falsecount="0"/>
|
|
26
|
+
<line num="62" count="22" type="cond" truecount="4" falsecount="0"/>
|
|
27
|
+
<line num="63" count="12" type="stmt"/>
|
|
28
|
+
<line num="65" count="10" type="stmt"/>
|
|
29
|
+
<line num="73" count="2" type="stmt"/>
|
|
30
|
+
<line num="74" count="2" type="stmt"/>
|
|
31
|
+
<line num="75" count="4" type="stmt"/>
|
|
32
|
+
<line num="76" count="4" type="stmt"/>
|
|
33
|
+
<line num="77" count="4" type="stmt"/>
|
|
34
|
+
<line num="83" count="2" type="stmt"/>
|
|
35
|
+
<line num="84" count="2" type="stmt"/>
|
|
36
|
+
<line num="85" count="3" type="stmt"/>
|
|
37
|
+
<line num="86" count="3" type="stmt"/>
|
|
38
|
+
<line num="87" count="3" type="stmt"/>
|
|
39
|
+
<line num="93" count="2" type="stmt"/>
|
|
40
|
+
<line num="94" count="2" type="stmt"/>
|
|
41
|
+
<line num="95" count="2" type="stmt"/>
|
|
42
|
+
<line num="96" count="2" type="stmt"/>
|
|
43
|
+
<line num="97" count="3" type="stmt"/>
|
|
44
|
+
<line num="99" count="2" type="stmt"/>
|
|
45
|
+
<line num="102" count="2" type="stmt"/>
|
|
46
|
+
<line num="109" count="4" type="stmt"/>
|
|
47
|
+
<line num="112" count="4" type="stmt"/>
|
|
48
|
+
<line num="113" count="1" type="stmt"/>
|
|
49
|
+
<line num="115" count="4" type="stmt"/>
|
|
50
|
+
<line num="121" count="2" type="stmt"/>
|
|
51
|
+
<line num="122" count="2" type="stmt"/>
|
|
52
|
+
<line num="123" count="4" type="stmt"/>
|
|
53
|
+
<line num="124" count="4" type="stmt"/>
|
|
54
|
+
<line num="125" count="4" type="stmt"/>
|
|
55
|
+
<line num="131" count="2" type="stmt"/>
|
|
56
|
+
<line num="132" count="2" type="stmt"/>
|
|
57
|
+
<line num="133" count="2" type="stmt"/>
|
|
58
|
+
<line num="134" count="2" type="stmt"/>
|
|
59
|
+
<line num="135" count="2" type="stmt"/>
|
|
60
|
+
<line num="141" count="2" type="stmt"/>
|
|
61
|
+
<line num="142" count="2" type="stmt"/>
|
|
62
|
+
<line num="143" count="6" type="stmt"/>
|
|
63
|
+
<line num="144" count="6" type="stmt"/>
|
|
64
|
+
<line num="145" count="6" type="stmt"/>
|
|
65
|
+
<line num="151" count="2" type="stmt"/>
|
|
66
|
+
<line num="152" count="2" type="stmt"/>
|
|
67
|
+
<line num="153" count="3" type="stmt"/>
|
|
68
|
+
<line num="154" count="4" type="stmt"/>
|
|
69
|
+
<line num="155" count="3" type="stmt"/>
|
|
70
|
+
<line num="162" count="2" type="stmt"/>
|
|
71
|
+
<line num="168" count="3" type="stmt"/>
|
|
72
|
+
<line num="169" count="3" type="stmt"/>
|
|
73
|
+
<line num="175" count="2" type="stmt"/>
|
|
74
|
+
<line num="176" count="2" type="stmt"/>
|
|
75
|
+
<line num="177" count="1" type="stmt"/>
|
|
76
|
+
<line num="178" count="3" type="stmt"/>
|
|
77
|
+
<line num="208" count="2" type="stmt"/>
|
|
78
|
+
<line num="218" count="5" type="stmt"/>
|
|
79
|
+
<line num="220" count="5" type="stmt"/>
|
|
80
|
+
<line num="221" count="5" type="stmt"/>
|
|
81
|
+
<line num="223" count="8" type="stmt"/>
|
|
82
|
+
<line num="225" count="8" type="stmt"/>
|
|
83
|
+
<line num="227" count="5" type="stmt"/>
|
|
84
|
+
<line num="230" count="5" type="stmt"/>
|
|
85
|
+
<line num="231" count="8" type="stmt"/>
|
|
86
|
+
<line num="233" count="8" type="stmt"/>
|
|
87
|
+
<line num="234" count="8" type="stmt"/>
|
|
88
|
+
<line num="235" count="8" type="stmt"/>
|
|
89
|
+
<line num="236" count="17" type="stmt"/>
|
|
90
|
+
<line num="237" count="17" type="cond" truecount="2" falsecount="0"/>
|
|
91
|
+
<line num="238" count="16" type="stmt"/>
|
|
92
|
+
<line num="239" count="16" type="stmt"/>
|
|
93
|
+
<line num="243" count="7" type="stmt"/>
|
|
94
|
+
<line num="244" count="7" type="cond" truecount="2" falsecount="0"/>
|
|
95
|
+
<line num="245" count="1" type="stmt"/>
|
|
96
|
+
<line num="246" count="1" type="stmt"/>
|
|
97
|
+
<line num="247" count="1" type="stmt"/>
|
|
98
|
+
<line num="252" count="7" type="stmt"/>
|
|
99
|
+
<line num="253" count="15" type="cond" truecount="2" falsecount="0"/>
|
|
100
|
+
<line num="254" count="9" type="stmt"/>
|
|
101
|
+
<line num="255" count="21" type="stmt"/>
|
|
102
|
+
<line num="256" count="9" type="stmt"/>
|
|
103
|
+
<line num="259" count="0" type="stmt"/>
|
|
104
|
+
<line num="261" count="9" type="stmt"/>
|
|
105
|
+
<line num="266" count="7" type="stmt"/>
|
|
106
|
+
<line num="267" count="7" type="stmt"/>
|
|
107
|
+
<line num="268" count="15" type="stmt"/>
|
|
108
|
+
<line num="269" count="15" type="stmt"/>
|
|
109
|
+
<line num="270" count="15" type="stmt"/>
|
|
110
|
+
<line num="271" count="15" type="cond" truecount="2" falsecount="0"/>
|
|
111
|
+
<line num="272" count="10" type="cond" truecount="4" falsecount="0"/>
|
|
112
|
+
<line num="278" count="5" type="stmt"/>
|
|
113
|
+
<line num="281" count="5" type="stmt"/>
|
|
114
|
+
<line num="284" count="5" type="stmt"/>
|
|
115
|
+
<line num="286" count="4" type="stmt"/>
|
|
116
|
+
<line num="289" count="2" type="stmt"/>
|
|
117
|
+
<line num="290" count="0" type="cond" truecount="0" falsecount="4"/>
|
|
118
|
+
<line num="292" count="2" type="stmt"/>
|
|
119
|
+
<line num="293" count="0" type="stmt"/>
|
|
120
|
+
</file>
|
|
121
|
+
<file name="mount.ts" path="/Users/jethrolarson/develop/fun-land/packages/fun-web/src/mount.ts">
|
|
122
|
+
<metrics statements="7" coveredstatements="7" conditionals="0" coveredconditionals="0" methods="2" coveredmethods="2"/>
|
|
123
|
+
<line num="23" count="1" type="stmt"/>
|
|
124
|
+
<line num="28" count="9" type="stmt"/>
|
|
125
|
+
<line num="29" count="9" type="stmt"/>
|
|
126
|
+
<line num="30" count="9" type="stmt"/>
|
|
127
|
+
<line num="32" count="9" type="stmt"/>
|
|
128
|
+
<line num="35" count="3" type="stmt"/>
|
|
129
|
+
<line num="36" count="3" type="stmt"/>
|
|
130
|
+
</file>
|
|
131
|
+
<file name="state.ts" path="/Users/jethrolarson/develop/fun-land/packages/fun-web/src/state.ts">
|
|
132
|
+
<metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="1" coveredmethods="1"/>
|
|
133
|
+
<line num="2" count="31" type="stmt"/>
|
|
134
|
+
</file>
|
|
135
|
+
</project>
|
|
136
|
+
</coverage>
|