@fun-land/fun-web 0.5.0 → 1.0.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/CHANGELOG.md +12 -0
- package/README.md +188 -94
- package/dist/esm/src/dom.d.ts +47 -5
- package/dist/esm/src/dom.js +62 -3
- package/dist/esm/src/dom.js.map +1 -1
- package/dist/esm/src/index.d.ts +1 -3
- package/dist/esm/src/index.js +1 -2
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/tsconfig.publish.tsbuildinfo +1 -1
- package/dist/src/dom.d.ts +47 -5
- package/dist/src/dom.js +66 -6
- package/dist/src/dom.js.map +1 -1
- package/dist/src/index.d.ts +1 -3
- package/dist/src/index.js +16 -19
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/examples/counter/counter.ts +2 -7
- package/examples/todo-app/DraggableTodoList.ts +1 -1
- package/examples/todo-app/Todo.ts +28 -30
- package/examples/todo-app/TodoApp.ts +10 -20
- package/package.json +12 -13
- package/src/dom.test.ts +157 -354
- package/src/dom.ts +136 -6
- package/src/index.ts +1 -20
- package/src/mount.test.ts +2 -2
- package/src/state.test.ts +0 -251
- package/src/state.ts +0 -2
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -31,28 +31,24 @@ pnpm add @fun-land/fun-web @fun-land/accessor
|
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
33
|
import {
|
|
34
|
-
|
|
34
|
+
hx,
|
|
35
35
|
funState,
|
|
36
36
|
mount,
|
|
37
|
-
bindProperty,
|
|
38
|
-
on,
|
|
39
|
-
enhance,
|
|
40
37
|
type Component,
|
|
41
38
|
type FunState,
|
|
39
|
+
type FunRead,
|
|
42
40
|
} from "@fun-land/fun-web";
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
const Counter: Component<{state: FunState<number>}> = (signal, {state}) => {
|
|
46
44
|
// Component runs once - no re-rendering on state changes
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return button;
|
|
45
|
+
return hx("button", {
|
|
46
|
+
signal,
|
|
47
|
+
// bind syncs the button text with state
|
|
48
|
+
bind: { textContent: state },
|
|
49
|
+
// Event handlers never go stale (component doesn't re-run)
|
|
50
|
+
on: { click: () => state.mod((n) => n + 1) },
|
|
51
|
+
});
|
|
56
52
|
};
|
|
57
53
|
|
|
58
54
|
// Create reactive state and mount
|
|
@@ -69,12 +65,16 @@ const mounted = mount(Counter, { state: funState(0) }, document.body);
|
|
|
69
65
|
const Counter: Component<{ count: FunState<number> }> = (signal, props) => {
|
|
70
66
|
console.log("Component runs once");
|
|
71
67
|
|
|
72
|
-
const display =
|
|
73
|
-
|
|
68
|
+
const display = hx("div", {
|
|
69
|
+
signal,
|
|
70
|
+
bind: { textContent: props.count },
|
|
71
|
+
});
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const button = hx("button", {
|
|
74
|
+
signal,
|
|
75
|
+
props: { textContent: "Increment" },
|
|
76
|
+
on: { click: () => props.count.mod(n => n + 1) },
|
|
77
|
+
});
|
|
78
78
|
|
|
79
79
|
return h("div", {}, [display, button]);
|
|
80
80
|
// Component function exits, but subscriptions keep working
|
|
@@ -117,12 +117,17 @@ function ReactCounter() {
|
|
|
117
117
|
const FunWebCounter: Component<{ count: FunState<number> }> = (signal, props) => {
|
|
118
118
|
console.log("Mounting!"); // Logs once, never again
|
|
119
119
|
|
|
120
|
-
const display =
|
|
121
|
-
|
|
120
|
+
const display = hx("div", {
|
|
121
|
+
signal,
|
|
122
|
+
bind: { textContent: props.count },
|
|
123
|
+
});
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const button = hx("button", {
|
|
126
|
+
signal,
|
|
127
|
+
props: { textContent: "+" },
|
|
128
|
+
// No useCallback needed - this closure never goes stale
|
|
129
|
+
on: { click: () => props.count.mod(n => n + 1) },
|
|
130
|
+
});
|
|
126
131
|
|
|
127
132
|
return h("div", {}, [display, button]);
|
|
128
133
|
};
|
|
@@ -149,6 +154,27 @@ userState.prop("name").watch(signal, (name) => {
|
|
|
149
154
|
});
|
|
150
155
|
```
|
|
151
156
|
|
|
157
|
+
**FunRead** - Read-only reactive values returned by `mapRead` and `derive`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { mapRead, derive } from "@fun-land/fun-state";
|
|
161
|
+
|
|
162
|
+
// Transform single value
|
|
163
|
+
const count = funState(5);
|
|
164
|
+
const doubled = mapRead(count, n => n * 2);
|
|
165
|
+
|
|
166
|
+
// Combine multiple values
|
|
167
|
+
const firstName = funState("Alice");
|
|
168
|
+
const lastName = funState("Smith");
|
|
169
|
+
const fullName = derive(firstName, lastName, (f, l) => `${f} ${l}`);
|
|
170
|
+
|
|
171
|
+
// Use with bindings (accepts FunRead)
|
|
172
|
+
const display = hx("div", {
|
|
173
|
+
signal,
|
|
174
|
+
bind: { textContent: fullName },
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
152
178
|
### Component Signature
|
|
153
179
|
|
|
154
180
|
Components are functions that receive an AbortSignal and props:
|
|
@@ -186,33 +212,42 @@ const Dashboard: Component<{
|
|
|
186
212
|
|
|
187
213
|
### Reactivity Patterns
|
|
188
214
|
|
|
189
|
-
**
|
|
215
|
+
**One-way binding with `hx`:**
|
|
190
216
|
|
|
191
217
|
```typescript
|
|
192
|
-
const nameEl = h("div");
|
|
193
218
|
const nameState: FunState<string> = state.prop("user").prop("name");
|
|
194
219
|
|
|
195
|
-
|
|
220
|
+
const nameEl = hx("div", {
|
|
221
|
+
signal,
|
|
222
|
+
bind: { textContent: nameState },
|
|
223
|
+
});
|
|
196
224
|
// nameEl.textContent stays in sync with nameState
|
|
197
225
|
```
|
|
198
226
|
|
|
199
|
-
**
|
|
227
|
+
**Two-way binding with `hx`:**
|
|
200
228
|
|
|
201
229
|
```typescript
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
},
|
|
230
|
+
const input = hx("input", {
|
|
231
|
+
signal,
|
|
232
|
+
props: { type: "text" },
|
|
233
|
+
bind: { value: state.prop("name") },
|
|
234
|
+
on: { input: (e) => state.prop("name").set(e.currentTarget.value) },
|
|
235
|
+
});
|
|
206
236
|
```
|
|
207
237
|
|
|
208
|
-
**
|
|
238
|
+
**Event handlers with `hx`:**
|
|
209
239
|
|
|
210
240
|
```typescript
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
on
|
|
215
|
-
)
|
|
241
|
+
const button = hx("button", {
|
|
242
|
+
signal,
|
|
243
|
+
props: { textContent: "Click me" },
|
|
244
|
+
on: {
|
|
245
|
+
click: (e) => {
|
|
246
|
+
console.log(e.currentTarget.textContent);
|
|
247
|
+
e.currentTarget.disabled = true; // type-safe!
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
});
|
|
216
251
|
```
|
|
217
252
|
|
|
218
253
|
**Use `.watch()` for complex logic:**
|
|
@@ -225,6 +260,17 @@ state.watch(signal, (s) => {
|
|
|
225
260
|
});
|
|
226
261
|
```
|
|
227
262
|
|
|
263
|
+
**Using enhancers (alternative to `hx`):**
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// If you prefer functional composition
|
|
267
|
+
const input = enhance(
|
|
268
|
+
h("input", { type: "text" }),
|
|
269
|
+
bindProperty("value", state.prop("name"), signal),
|
|
270
|
+
on("input", (e) => state.prop("name").set(e.currentTarget.value), signal)
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
228
274
|
### Cleanup with AbortSignal
|
|
229
275
|
|
|
230
276
|
All subscriptions and event listeners require an AbortSignal. When the signal aborts, everything cleans up automatically:
|
|
@@ -253,7 +299,7 @@ For the most part you won't have to worry about the abort signal if you use the
|
|
|
253
299
|
**Don't have one big app state**
|
|
254
300
|
This isn't redux. There's little reason to create giant state objects. Create state near the leaves and hoist it when you need to. That said
|
|
255
301
|
|
|
256
|
-
**You can have more
|
|
302
|
+
**You can have more than one state**
|
|
257
303
|
It's just a value. If having one state manage multiple properties works then great but if you want to have several that's fine too.
|
|
258
304
|
|
|
259
305
|
**Deep chains of .prop() are a smell**
|
|
@@ -266,37 +312,49 @@ const bazFromState = Acc<MyState>().prop('foo').prop('bar').prop('baz')
|
|
|
266
312
|
const baz = state.focus(bazFromState).get()
|
|
267
313
|
```
|
|
268
314
|
|
|
269
|
-
**
|
|
315
|
+
**Using .get() in component scope is a smell**
|
|
316
|
+
State is for when you want components to respond to state changes as such you should be using it with bindProperty or state.watch or inside event handlers. There are many cases for .get() but if it's the first thing you reach for you're probably gonna be confused that the UI isn't updating.
|
|
317
|
+
|
|
318
|
+
**Prefer `hx` for reactive elements:**
|
|
270
319
|
|
|
271
320
|
```typescript
|
|
272
|
-
// ✅ Good
|
|
273
|
-
|
|
321
|
+
// ✅ Good - declarative and type-safe
|
|
322
|
+
const input = hx("input", {
|
|
323
|
+
signal,
|
|
324
|
+
props: { type: "text" },
|
|
325
|
+
bind: { value: state.prop("name") },
|
|
326
|
+
on: { input: (e) => state.prop("name").set(e.currentTarget.value) },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ✅ Also good - functional composition with enhancers
|
|
330
|
+
const input = enhance(
|
|
331
|
+
h("input", { type: "text" }),
|
|
332
|
+
bindProperty("value", state.prop("name"), signal),
|
|
333
|
+
on("input", (e) => state.prop("name").set(e.currentTarget.value), signal)
|
|
334
|
+
);
|
|
274
335
|
|
|
275
|
-
// ❌ Avoid
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
// ❌ Avoid manual subscriptions when `bind` works
|
|
337
|
+
const input = h("input", { type: "text" });
|
|
338
|
+
state.prop("name").watch(signal, (name) => {
|
|
339
|
+
input.value = name; // Just use bind!
|
|
278
340
|
});
|
|
279
341
|
```
|
|
280
342
|
|
|
281
|
-
**
|
|
343
|
+
**Don't bind events in `h()` props:**
|
|
282
344
|
|
|
283
345
|
```typescript
|
|
284
|
-
// ✅ Good -
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
// ❌ Avoid - loses type information
|
|
290
|
-
button.addEventListener("click", (e) => {
|
|
291
|
-
(e.currentTarget as HTMLButtonElement).disabled = true;
|
|
292
|
-
}); // ❌ Forgot {signal} !
|
|
346
|
+
// ✅ Good - uses hx with type-safe events
|
|
347
|
+
const button = hx("button", {
|
|
348
|
+
signal,
|
|
349
|
+
on: { click: (e) => e.currentTarget.disabled = true },
|
|
350
|
+
});
|
|
293
351
|
|
|
294
|
-
// ❌ Avoid
|
|
295
|
-
h(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// ❌ event handler not cleaned up!
|
|
299
|
-
}
|
|
352
|
+
// ❌ Avoid - h() event props aren't cleaned up and lose types
|
|
353
|
+
const button = h("button", {
|
|
354
|
+
onclick: (e) => {
|
|
355
|
+
(e.currentTarget as HTMLButtonElement).disabled = true; // needs cast
|
|
356
|
+
}, // ❌ event handler not cleaned up!
|
|
357
|
+
});
|
|
300
358
|
```
|
|
301
359
|
|
|
302
360
|
**Manual subscriptions for complex updates:**
|
|
@@ -319,9 +377,30 @@ state.watch(signal, (s) => {
|
|
|
319
377
|
|
|
320
378
|
### DOM Utilities
|
|
321
379
|
|
|
380
|
+
#### `hx` (recommended)
|
|
381
|
+
|
|
382
|
+
Create elements with structured props, attrs, event handlers, and reactive bindings. More complex but provides better type safety and ergonomics than `h` + enhancers.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const input = hx("input", {
|
|
386
|
+
signal,
|
|
387
|
+
props: { type: "text", placeholder: "Enter name" },
|
|
388
|
+
attrs: { "data-test": "name-input" },
|
|
389
|
+
bind: { value: nameState },
|
|
390
|
+
on: { input: (e) => nameState.set(e.currentTarget.value) },
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Parameters:**
|
|
395
|
+
- `props` - Element properties (typed per element, writable properties only)
|
|
396
|
+
- `attrs` - HTML attributes (data-*, aria-*, etc.)
|
|
397
|
+
- `on` - Event handlers (type-safe, with inferred `currentTarget`)
|
|
398
|
+
- `bind` - Reactive bindings (properties sync with FunState)
|
|
399
|
+
- `signal` - **Required** AbortSignal for cleanup
|
|
400
|
+
|
|
322
401
|
#### `h`
|
|
323
402
|
|
|
324
|
-
|
|
403
|
+
Create HTML elements declaratively. Consider using `hx` if you want to add event handlers or respond to state changes.
|
|
325
404
|
|
|
326
405
|
```typescript
|
|
327
406
|
const div = h("div", { id: "app" }, [
|
|
@@ -332,12 +411,12 @@ const div = h("div", { id: "app" }, [
|
|
|
332
411
|
|
|
333
412
|
**Attribute conventions:**
|
|
334
413
|
- Dashed properties (`data-*`, `aria-*`) → `setAttribute()`
|
|
335
|
-
-
|
|
414
|
+
- Properties starting with `on` → event listeners (⚠️ not cleaned up, use `hx` or `on()` enhancer)
|
|
336
415
|
- Everything else → property assignment
|
|
337
416
|
|
|
338
417
|
#### bindProperty
|
|
339
418
|
|
|
340
|
-
Bind element property to state. Returns `Enhancer`.
|
|
419
|
+
Bind element property to state. Accepts `FunRead` (including `FunState`, `map`, `derive` results). Returns `Enhancer`. Consider using `hx` with `bind` for better ergonomics.
|
|
341
420
|
|
|
342
421
|
```typescript
|
|
343
422
|
const input = enhance(
|
|
@@ -345,11 +424,24 @@ const input = enhance(
|
|
|
345
424
|
bindProperty("value", state.prop("name"), signal)
|
|
346
425
|
);
|
|
347
426
|
// input.value syncs with state.name
|
|
427
|
+
|
|
428
|
+
// Works with derived values
|
|
429
|
+
const formatted = mapRead(state.prop("price"), p => `$${p.toFixed(2)}`);
|
|
430
|
+
const display = enhance(
|
|
431
|
+
h("div"),
|
|
432
|
+
bindProperty("textContent", formatted, signal)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Equivalent with hx:
|
|
436
|
+
const input = hx("input", {
|
|
437
|
+
signal,
|
|
438
|
+
bind: { value: state.prop("name") },
|
|
439
|
+
});
|
|
348
440
|
```
|
|
349
441
|
|
|
350
442
|
#### on
|
|
351
443
|
|
|
352
|
-
Add type-safe event listener. Returns `Enhancer`.
|
|
444
|
+
Add type-safe event listener. Returns `Enhancer`. Consider using `hx` with `on` for better ergonomics.
|
|
353
445
|
|
|
354
446
|
```typescript
|
|
355
447
|
const button = enhance(
|
|
@@ -358,11 +450,21 @@ const button = enhance(
|
|
|
358
450
|
e.currentTarget.disabled = true;
|
|
359
451
|
}, signal)
|
|
360
452
|
);
|
|
453
|
+
|
|
454
|
+
// Equivalent with hx:
|
|
455
|
+
const button = hx("button", {
|
|
456
|
+
signal,
|
|
457
|
+
on: {
|
|
458
|
+
click: (e) => {
|
|
459
|
+
e.currentTarget.disabled = true;
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
});
|
|
361
463
|
```
|
|
362
464
|
|
|
363
465
|
#### bindListChildren
|
|
364
466
|
|
|
365
|
-
Render
|
|
467
|
+
Render a list of items from a state array. Each row gets its own AbortSignal for cleanup and a focused state. Returns `Enhancer`.
|
|
366
468
|
|
|
367
469
|
```typescript
|
|
368
470
|
import { Acc } from "@fun-land/accessor";
|
|
@@ -382,7 +484,7 @@ const list = enhance(
|
|
|
382
484
|
bindListChildren({
|
|
383
485
|
signal,
|
|
384
486
|
state: todos,
|
|
385
|
-
key:
|
|
487
|
+
key: prop<Todo>()("id"), // key is an accessor that targets the unique string value in your array items
|
|
386
488
|
row: ({ signal, state, remove }) => {
|
|
387
489
|
const li = h("li");
|
|
388
490
|
|
|
@@ -403,7 +505,7 @@ const list = enhance(
|
|
|
403
505
|
#### renderWhen
|
|
404
506
|
```ts
|
|
405
507
|
function renderWhen<State, Props>(options: {
|
|
406
|
-
state:
|
|
508
|
+
state: FunRead<State>;
|
|
407
509
|
predicate?: (value: State) => boolean;
|
|
408
510
|
component: Component<Props>;
|
|
409
511
|
props: Props;
|
|
@@ -411,7 +513,7 @@ function renderWhen<State, Props>(options: {
|
|
|
411
513
|
}): Element
|
|
412
514
|
```
|
|
413
515
|
|
|
414
|
-
Conditionally render a component based on state and an optional predicate. Returns a container element that mounts/unmounts the component as the condition changes.
|
|
516
|
+
Conditionally render a component based on state and an optional predicate. Accepts `FunRead` (including `FunState`, `map`, `derive` results). Returns a container element that mounts/unmounts the component as the condition changes.
|
|
415
517
|
|
|
416
518
|
**Key features:**
|
|
417
519
|
- Component is mounted when condition is `true`, unmounted when `false`
|
|
@@ -435,10 +537,11 @@ const App: Component = (signal) => {
|
|
|
435
537
|
const showDetailsState = funState(false);
|
|
436
538
|
const userState = funState({ email: "alice@example.com", joinDate: "2024" });
|
|
437
539
|
|
|
438
|
-
const toggleBtn =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
540
|
+
const toggleBtn = hx("button", {
|
|
541
|
+
signal,
|
|
542
|
+
props: { textContent: "Toggle Details" },
|
|
543
|
+
on: { click: () => showDetailsState.mod(show => !show) },
|
|
544
|
+
});
|
|
442
545
|
|
|
443
546
|
// Details component mounts/unmounts based on showDetailsState
|
|
444
547
|
const detailsEl = renderWhen({
|
|
@@ -495,38 +598,30 @@ showState.watch(signal, (show) => {
|
|
|
495
598
|
});
|
|
496
599
|
```
|
|
497
600
|
|
|
498
|
-
####
|
|
499
|
-
|
|
500
|
-
Convenient shortcuts for `querySelector` and `querySelectorAll` with better TypeScript support.
|
|
601
|
+
#### querySelectorAll
|
|
501
602
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Query a single element. Returns `undefined` instead of `null` if not found.
|
|
603
|
+
Query multiple elements. Returns a real Array (not NodeList) for better ergonomics.
|
|
505
604
|
|
|
506
605
|
```typescript
|
|
507
|
-
const
|
|
508
|
-
if (button) {
|
|
509
|
-
button.disabled = true;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const input = $<HTMLInputElement>(".name-input");
|
|
606
|
+
const items = querySelectorAll<HTMLDivElement>(".item").map(addClass("active"));
|
|
513
607
|
```
|
|
514
608
|
|
|
515
|
-
|
|
609
|
+
#### enhance
|
|
516
610
|
|
|
517
|
-
|
|
611
|
+
Apply multiple enhancers to an element. Useful with `h()` and the enhancer utilities below.
|
|
518
612
|
|
|
519
613
|
```typescript
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
614
|
+
const input = enhance(
|
|
615
|
+
h("input", { type: "text" }),
|
|
616
|
+
addClass("form-control"),
|
|
617
|
+
bindProperty("value", nameState, signal),
|
|
618
|
+
on("input", (e) => nameState.set(e.currentTarget.value), signal)
|
|
619
|
+
);
|
|
525
620
|
```
|
|
526
621
|
|
|
527
622
|
#### Other utilities
|
|
528
623
|
|
|
529
|
-
All return the element for chaining:
|
|
624
|
+
All return the element for chaining (used with `enhance`):
|
|
530
625
|
|
|
531
626
|
```typescript
|
|
532
627
|
text: (content: string | number) => (el: Element) => Element
|
|
@@ -536,8 +631,6 @@ addClass: (...classes: string[]) => (el: Element) => Element
|
|
|
536
631
|
removeClass: (...classes: string[]) => (el: Element) => Element
|
|
537
632
|
toggleClass: (className: string, force?: boolean) => (el: Element) => Element
|
|
538
633
|
append: (...children: Element[]) => (el: Element) => Element
|
|
539
|
-
// pipe a value through a series of endomorphisms
|
|
540
|
-
pipeAll: <T>(x: T, ...fns: Array<(x: T) => T>) => T
|
|
541
634
|
```
|
|
542
635
|
|
|
543
636
|
### Mounting
|
|
@@ -573,6 +666,7 @@ interface MountedComponent {
|
|
|
573
666
|
Components compose by calling other components and passing focused state via props:
|
|
574
667
|
|
|
575
668
|
```typescript
|
|
669
|
+
import { h } from "@fun-land/fun-web";
|
|
576
670
|
import { prop } from "@fun-land/accessor";
|
|
577
671
|
import { UserProfile, UserData } from "./UserProfile";
|
|
578
672
|
import { SettingsPanel, Settings } from "./Settings";
|
package/dist/esm/src/dom.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** DOM utilities for functional element creation and manipulation */
|
|
2
|
-
import { type FunState } from "@fun-land/fun-state";
|
|
2
|
+
import { type FunState, type FunRead } from "@fun-land/fun-state";
|
|
3
3
|
import type { Component, ElementChild } from "./types";
|
|
4
4
|
import { Accessor } from "@fun-land/accessor";
|
|
5
5
|
export type Enhancer<El extends Element> = (element: El) => El;
|
|
@@ -16,6 +16,46 @@ export type Enhancer<El extends Element> = (element: El) => El;
|
|
|
16
16
|
* h('div', {id: 'app', 'data-test': 'foo'}, [child1, child2])
|
|
17
17
|
*/
|
|
18
18
|
export declare const h: <Tag extends keyof HTMLElementTagNameMap>(tag: Tag, attrs?: Record<string, any> | null, children?: ElementChild | ElementChild[]) => HTMLElementTagNameMap[Tag];
|
|
19
|
+
type WritableKeys<T> = {
|
|
20
|
+
[K in keyof T]-?: (<F>() => F extends {
|
|
21
|
+
[Q in K]: T[K];
|
|
22
|
+
} ? 1 : 2) extends <F>() => F extends {
|
|
23
|
+
-readonly [Q in K]: T[K];
|
|
24
|
+
} ? 1 : 2 ? K : never;
|
|
25
|
+
}[keyof T];
|
|
26
|
+
type HxProps<El extends Element> = Partial<{
|
|
27
|
+
[K in WritableKeys<El> & string]: El[K] | null | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
type HxHandlers<El extends Element> = Partial<{
|
|
30
|
+
[K in keyof GlobalEventHandlersEventMap]: (ev: GlobalEventHandlersEventMap[K] & {
|
|
31
|
+
currentTarget: El;
|
|
32
|
+
}) => void;
|
|
33
|
+
}>;
|
|
34
|
+
type HxBindings<El extends Element> = Partial<{
|
|
35
|
+
[K in WritableKeys<El> & string]: FunRead<El[K]>;
|
|
36
|
+
}>;
|
|
37
|
+
type HxOptionsBase<El extends Element> = {
|
|
38
|
+
props?: HxProps<El>;
|
|
39
|
+
attrs?: Record<string, string | number | boolean | null | undefined>;
|
|
40
|
+
};
|
|
41
|
+
type HxOptions<El extends Element> = HxOptionsBase<El> & {
|
|
42
|
+
signal: AbortSignal;
|
|
43
|
+
on?: HxHandlers<El>;
|
|
44
|
+
bind?: HxBindings<El>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Create an element with structured props, attrs, event handlers, and bindings.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* hx("input", {
|
|
51
|
+
* signal,
|
|
52
|
+
* props: { type: "text" },
|
|
53
|
+
* attrs: { "data-test": "name" },
|
|
54
|
+
* bind: { value: nameState },
|
|
55
|
+
* on: { input: (e) => nameState.set(e.currentTarget.value) },
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
export declare function hx<Tag extends keyof HTMLElementTagNameMap>(tag: Tag, options: HxOptions<HTMLElementTagNameMap[Tag]>, children?: ElementChild | ElementChild[]): HTMLElementTagNameMap[Tag];
|
|
19
59
|
/**
|
|
20
60
|
* Set text content of an element (returns element for chaining)
|
|
21
61
|
* @returns {Enhancer}
|
|
@@ -34,7 +74,7 @@ export declare const attrs: (obj: Record<string, string>) => <El extends Element
|
|
|
34
74
|
* update property `key` of element when state updates
|
|
35
75
|
* @returns {Enhancer}
|
|
36
76
|
*/
|
|
37
|
-
export declare const bindProperty: <E extends Element, K extends keyof E & string>(key: K, state:
|
|
77
|
+
export declare const bindProperty: <E extends Element, K extends keyof E & string>(key: K, state: FunRead<E[K]>, signal: AbortSignal) => (el: E) => E;
|
|
38
78
|
/**
|
|
39
79
|
* Add CSS classes to an element (returns element for chaining)
|
|
40
80
|
* @returns {Enhancer}
|
|
@@ -114,11 +154,13 @@ export declare const bindListChildren: <T>(options: {
|
|
|
114
154
|
* });
|
|
115
155
|
*/
|
|
116
156
|
export declare function renderWhen<State, Props>(options: {
|
|
117
|
-
state:
|
|
157
|
+
state: FunRead<State>;
|
|
118
158
|
predicate?: (value: State) => boolean;
|
|
119
159
|
component: Component<Props>;
|
|
120
160
|
props: Props;
|
|
121
161
|
signal: AbortSignal;
|
|
122
162
|
}): Element;
|
|
123
|
-
|
|
124
|
-
export declare const
|
|
163
|
+
/** add passed class (idempotent) to element when state returns true */
|
|
164
|
+
export declare const bindClass: (className: string, state: FunRead<boolean>, signal: AbortSignal) => <E extends Element>(el: E) => E;
|
|
165
|
+
export declare const querySelectorAll: <T extends Element>(selector: string) => T[];
|
|
166
|
+
export {};
|
package/dist/esm/src/dom.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-preserving Object.entries helper for objects with known keys
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
const entries = (obj) => Object.entries(obj);
|
|
1
6
|
/**
|
|
2
7
|
* Create an HTML element with attributes and children
|
|
3
8
|
*
|
|
@@ -12,7 +17,9 @@
|
|
|
12
17
|
*/
|
|
13
18
|
export const h = (tag,
|
|
14
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
-
attrs, children
|
|
20
|
+
attrs, children
|
|
21
|
+
// eslint-disable-next-line complexity
|
|
22
|
+
) => {
|
|
16
23
|
const element = document.createElement(tag);
|
|
17
24
|
// Apply attributes/properties/events
|
|
18
25
|
if (attrs) {
|
|
@@ -42,6 +49,52 @@ attrs, children) => {
|
|
|
42
49
|
}
|
|
43
50
|
return element;
|
|
44
51
|
};
|
|
52
|
+
// eslint-disable-next-line complexity
|
|
53
|
+
export function hx(tag, options, children) {
|
|
54
|
+
if (!(options === null || options === void 0 ? void 0 : options.signal)) {
|
|
55
|
+
throw new Error("hx: signal is required");
|
|
56
|
+
}
|
|
57
|
+
const { signal, props, attrs: attrMap, on: onMap, bind } = options;
|
|
58
|
+
const element = document.createElement(tag);
|
|
59
|
+
if (props) {
|
|
60
|
+
for (const [key, value] of entries(props)) {
|
|
61
|
+
if (value == null)
|
|
62
|
+
continue;
|
|
63
|
+
element[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (attrMap) {
|
|
67
|
+
for (const [key, value] of Object.entries(attrMap)) {
|
|
68
|
+
if (value == null)
|
|
69
|
+
continue;
|
|
70
|
+
element.setAttribute(key, String(value));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (children != null) {
|
|
74
|
+
appendChildren(children)(element);
|
|
75
|
+
}
|
|
76
|
+
if (bind) {
|
|
77
|
+
const bindElementProperty = (key, state) => {
|
|
78
|
+
bindProperty(key, state, signal)(element);
|
|
79
|
+
};
|
|
80
|
+
for (const key of Object.keys(bind)) {
|
|
81
|
+
const state = bind[key];
|
|
82
|
+
if (!state)
|
|
83
|
+
continue;
|
|
84
|
+
bindElementProperty(key, state);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (onMap) {
|
|
88
|
+
for (const [event, handler] of Object.entries(onMap)) {
|
|
89
|
+
if (!handler)
|
|
90
|
+
continue;
|
|
91
|
+
element.addEventListener(event, handler, {
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return element;
|
|
97
|
+
}
|
|
45
98
|
/**
|
|
46
99
|
* Append children to an element, flattening arrays and converting primitives to text nodes
|
|
47
100
|
* @returns {Enhancer}
|
|
@@ -180,6 +233,7 @@ export const bindListChildren = (options) => (parent) => {
|
|
|
180
233
|
}
|
|
181
234
|
rows.clear();
|
|
182
235
|
};
|
|
236
|
+
// eslint-disable-next-line complexity
|
|
183
237
|
const reconcile = () => {
|
|
184
238
|
const items = list.get();
|
|
185
239
|
const nextKeys = [];
|
|
@@ -259,6 +313,7 @@ export function renderWhen(options) {
|
|
|
259
313
|
container.style.display = "contents";
|
|
260
314
|
let childCtrl = null;
|
|
261
315
|
let childEl = null;
|
|
316
|
+
// eslint-disable-next-line complexity
|
|
262
317
|
const reconcile = () => {
|
|
263
318
|
const shouldRender = predicate(state.get());
|
|
264
319
|
if (shouldRender && !childEl) {
|
|
@@ -285,6 +340,10 @@ export function renderWhen(options) {
|
|
|
285
340
|
reconcile();
|
|
286
341
|
return container;
|
|
287
342
|
}
|
|
288
|
-
|
|
289
|
-
export const
|
|
343
|
+
/** add passed class (idempotent) to element when state returns true */
|
|
344
|
+
export const bindClass = (className, state, signal) => (el) => {
|
|
345
|
+
state.watch(signal, (active) => el.classList.toggle(className, active));
|
|
346
|
+
return el;
|
|
347
|
+
};
|
|
348
|
+
export const querySelectorAll = (selector) => Array.from(document.querySelectorAll(selector));
|
|
290
349
|
//# sourceMappingURL=dom.js.map
|