@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 ADDED
@@ -0,0 +1,12 @@
1
+ # @fun-land/fun-web
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8f57a13: Add derive and mapRead to fun-state, add bindClass and hx to fun-web
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [8f57a13]
12
+ - @fun-land/fun-state@9.2.0
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
- h,
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
- const button = h("button", {}, `Count: ${state.get()}`);
48
-
49
- // bindProperty subscribes to the state and updates named property
50
- enhance(button, bindProperty("textContent", state, signal));
51
-
52
- // Event handlers never go stale (component doesn't re-run)
53
- enhance(button, on("click", () => state.mod((n) => n + 1), signal));
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 = h("div");
73
- const button = h("button", {}, "Increment");
68
+ const display = hx("div", {
69
+ signal,
70
+ bind: { textContent: props.count },
71
+ });
74
72
 
75
- // This subscription handles updates, not re-rendering
76
- enhance(display, bindProperty("textContent", props.count, signal));
77
- enhance(button, on("click", () => props.count.mod(n => n + 1), signal));
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 = h("div");
121
- const button = h("button", {}, "+");
120
+ const display = hx("div", {
121
+ signal,
122
+ bind: { textContent: props.count },
123
+ });
122
124
 
123
- // No useCallback needed - this closure never goes stale
124
- enhance(button, on("click", () => props.count.mod(n => n + 1), signal));
125
- enhance(display, bindProperty("textContent", props.count, signal));
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
- **Use `bindProperty` for simple one-way bindings:**
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
- enhance(nameEl, bindProperty("textContent", nameState, signal));
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
- **Use `on` for events:**
227
+ **Two-way binding with `hx`:**
200
228
 
201
229
  ```typescript
202
- const button = h("button", {}, "Click me");
203
- enhance(button, on("click", (e: MouseEvent & { currentTarget: HTMLButtonElement }) => {
204
- console.log(e.currentTarget.textContent);
205
- }, signal));
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
- **Chain for two-way bindings:**
238
+ **Event handlers with `hx`:**
209
239
 
210
240
  ```typescript
211
- const input = enhance(
212
- h("input", { type: "text" }),
213
- bindProperty("value", state.prop("name"), signal),
214
- on("input", (e) => state.prop("name").set(e.currentTarget.value), signal)
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 that one state**
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
- **Prefer helpers over manual subscriptions:**
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
- enhance(element, bindProperty("textContent", state.prop("count"), signal));
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 (when bindProperty works)
276
- state.prop("count").watch(signal, (count) => {
277
- element.textContent = String(count);
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
- **Use `on()` for type safety:**
343
+ **Don't bind events in `h()` props:**
282
344
 
283
345
  ```typescript
284
- // ✅ Good - types inferred
285
- enhance(button, on("click", (e) => {
286
- e.currentTarget.disabled = true; // TypeScript knows it's HTMLButtonElement
287
- }, signal));
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 binding events in props
295
- h('button', {onclick: (e) => {
296
- // type info lost!
297
- (e.currentTarget as HTMLButtonElement).disabled = true;
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
- Declaratively create an HTML Element with properties and children.
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
- - Don't event bind with properties, use `on()`
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 and reconcile keyed lists efficiently. Each row gets its own AbortSignal for cleanup and a focused state. Returns `Enhancer`.
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: Acc<Todo>().prop("id"),
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: FunState<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 = h("button", { textContent: "Toggle Details" });
439
- enhance(toggleBtn, on("click", () => {
440
- showDetailsState.mod(show => !show);
441
- }, signal));
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
- #### $ and $$ - DOM Query Utilities
499
-
500
- Convenient shortcuts for `querySelector` and `querySelectorAll` with better TypeScript support.
601
+ #### querySelectorAll
501
602
 
502
- **`$<T extends Element>(selector: string): T | undefined`**
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 button = $<HTMLButtonElement>("#submit-btn");
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
- **`$$<T extends Element>(selector: string): T[]`**
609
+ #### enhance
516
610
 
517
- Query multiple elements. Returns a real Array (not NodeList) for better ergonomics.
611
+ Apply multiple enhancers to an element. Useful with `h()` and the enhancer utilities below.
518
612
 
519
613
  ```typescript
520
- const items = $$<HTMLDivElement>(".item");
521
- items.forEach(item => item.classList.add("active"));
522
-
523
- // Array methods work directly
524
- const texts = $$(".label").map(el => el.textContent);
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";
@@ -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: FunState<E[K]>, signal: AbortSignal) => (el: E) => E;
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: FunState<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
- export declare const $: <T extends Element>(selector: string) => T | undefined;
124
- export declare const $$: <T extends Element>(selector: string) => T[];
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 {};
@@ -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
- export const $ = (selector) => { var _a; return (_a = document.querySelector(selector)) !== null && _a !== void 0 ? _a : undefined; };
289
- export const $$ = (selector) => Array.from(document.querySelectorAll(selector));
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