@lab206/dom 0.1.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 +114 -0
- package/README.md +6 -0
- package/dist/bind.d.ts +23 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +170 -0
- package/dist/bind.js.map +1 -0
- package/dist/component.d.ts +47 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +95 -0
- package/dist/component.js.map +1 -0
- package/dist/dynamic.d.ts +17 -0
- package/dist/dynamic.d.ts.map +1 -0
- package/dist/dynamic.js +68 -0
- package/dist/dynamic.js.map +1 -0
- package/dist/fragment.d.ts +13 -0
- package/dist/fragment.d.ts.map +1 -0
- package/dist/fragment.js +41 -0
- package/dist/fragment.js.map +1 -0
- package/dist/h.d.ts +26 -0
- package/dist/h.d.ts.map +1 -0
- package/dist/h.js +164 -0
- package/dist/h.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/insert.d.ts +5 -0
- package/dist/insert.d.ts.map +1 -0
- package/dist/insert.js +10 -0
- package/dist/insert.js.map +1 -0
- package/dist/jsx-runtime.d.ts +52 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +43 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/list.d.ts +23 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +77 -0
- package/dist/list.js.map +1 -0
- package/dist/mount.d.ts +19 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +40 -0
- package/dist/mount.js.map +1 -0
- package/dist/on.d.ts +8 -0
- package/dist/on.d.ts.map +1 -0
- package/dist/on.js +7 -0
- package/dist/on.js.map +1 -0
- package/dist/props.d.ts +54 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +211 -0
- package/dist/props.js.map +1 -0
- package/dist/show.d.ts +12 -0
- package/dist/show.d.ts.map +1 -0
- package/dist/show.js +42 -0
- package/dist/show.js.map +1 -0
- package/package.json +71 -0
- package/src/bind.ts +195 -0
- package/src/component.ts +127 -0
- package/src/dynamic.ts +93 -0
- package/src/fragment.ts +43 -0
- package/src/h.ts +236 -0
- package/src/index.ts +54 -0
- package/src/insert.ts +14 -0
- package/src/jsx-runtime.ts +100 -0
- package/src/list.ts +111 -0
- package/src/mount.ts +46 -0
- package/src/on.ts +31 -0
- package/src/props.ts +259 -0
- package/src/show.ts +52 -0
- package/src/test-setup.ts +17 -0
package/src/component.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Child } from "./h.js";
|
|
2
|
+
import { h } from "./h.js";
|
|
3
|
+
import { show } from "./show.js";
|
|
4
|
+
import { list, type ListOptions } from "./list.js";
|
|
5
|
+
import { createProps, type ReactiveProps } from "./props.js";
|
|
6
|
+
|
|
7
|
+
export type ComponentProps<P extends object> = ReactiveProps<
|
|
8
|
+
P & { children?: unknown }
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export type ComponentSetup<P extends object> = (
|
|
12
|
+
props: ComponentProps<P>,
|
|
13
|
+
) => Node | DocumentFragment | null | undefined;
|
|
14
|
+
|
|
15
|
+
export type Component<P extends object = Record<string, never>> = (
|
|
16
|
+
props?: P & { children?: unknown },
|
|
17
|
+
) => Node | DocumentFragment | null | undefined;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Declare a function component with **reactive props**.
|
|
21
|
+
*
|
|
22
|
+
* Setup runs **once** per instance. Reading `props.x` inside effects /
|
|
23
|
+
* JSX reactive scopes tracks the parent's signal or accessor.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const Hello = component((props: { name: string }) => (
|
|
28
|
+
* <p>{() => `Hello, ${props.name}`}</p>
|
|
29
|
+
* ));
|
|
30
|
+
*
|
|
31
|
+
* // Live updates — pass a signal or accessor:
|
|
32
|
+
* <Hello name={name} />
|
|
33
|
+
* <Hello name={() => user().name} />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function component<P extends object>(
|
|
37
|
+
setup: ComponentSetup<P>,
|
|
38
|
+
): Component<P> {
|
|
39
|
+
const Comp: Component<P> = (raw) => {
|
|
40
|
+
// If JSX already applied createProps, `raw` is a Proxy — still safe
|
|
41
|
+
// to wrap again only when we have a plain object. Detect proxy via
|
|
42
|
+
// missing own keys reliability: always normalize through createProps
|
|
43
|
+
// on a shallow bag of current keys so double-proxy still reads source.
|
|
44
|
+
const props = createProps(
|
|
45
|
+
(raw ?? {}) as P & { children?: unknown },
|
|
46
|
+
) as ComponentProps<P>;
|
|
47
|
+
return setup(props);
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(Comp, "name", {
|
|
50
|
+
value: setup.name || "Component",
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
return Comp;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Conditional render for JSX.
|
|
58
|
+
* `when` may be a boolean, signal, or zero-arg accessor — unwrapped on read
|
|
59
|
+
* when used with reactive props / JSX.
|
|
60
|
+
*/
|
|
61
|
+
export function Show(props: {
|
|
62
|
+
when: boolean | (() => boolean);
|
|
63
|
+
children: (() => Node) | Node | Child;
|
|
64
|
+
fallback?: (() => Node) | Node | Child;
|
|
65
|
+
}): HTMLElement {
|
|
66
|
+
// Normalize so accessors stay lazy for the show() effect.
|
|
67
|
+
const p = props;
|
|
68
|
+
const whenFn = (): boolean => {
|
|
69
|
+
const w = p.when;
|
|
70
|
+
if (typeof w === "function") return !!(w as () => boolean)();
|
|
71
|
+
return !!w;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const host = document.createElement("div");
|
|
75
|
+
host.style.display = "contents";
|
|
76
|
+
|
|
77
|
+
const resolve = (
|
|
78
|
+
value: (() => Node) | Node | Child | undefined,
|
|
79
|
+
): Node => {
|
|
80
|
+
if (value == null || value === false || value === true) {
|
|
81
|
+
return document.createComment("show");
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "function") {
|
|
84
|
+
const result = (value as () => unknown)();
|
|
85
|
+
if (result instanceof Node) return result;
|
|
86
|
+
return h("span", { text: value as () => string | number });
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
89
|
+
return document.createTextNode(String(value));
|
|
90
|
+
}
|
|
91
|
+
return value as Node;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
show(host, whenFn, () => resolve(p.children as (() => Node) | Node));
|
|
95
|
+
|
|
96
|
+
if (p.fallback !== undefined) {
|
|
97
|
+
const fallbackHost = document.createElement("div");
|
|
98
|
+
fallbackHost.style.display = "contents";
|
|
99
|
+
show(fallbackHost, () => !whenFn(), () => resolve(p.fallback));
|
|
100
|
+
const outer = document.createElement("div");
|
|
101
|
+
outer.style.display = "contents";
|
|
102
|
+
outer.append(host, fallbackHost);
|
|
103
|
+
return outer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return host;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keyed list for JSX.
|
|
111
|
+
*/
|
|
112
|
+
export function For<T>(props: {
|
|
113
|
+
each: readonly T[] | (() => readonly T[]);
|
|
114
|
+
children: (item: () => T, index: () => number) => Node;
|
|
115
|
+
key?: ListOptions<T>["key"];
|
|
116
|
+
}): HTMLElement {
|
|
117
|
+
const host = document.createElement("div");
|
|
118
|
+
host.style.display = "contents";
|
|
119
|
+
const getItems = (): readonly T[] => {
|
|
120
|
+
const e = props.each;
|
|
121
|
+
return typeof e === "function" ? (e as () => readonly T[])() : e;
|
|
122
|
+
};
|
|
123
|
+
list(host, getItems, props.children, {
|
|
124
|
+
...(props.key ? { key: props.key } : {}),
|
|
125
|
+
});
|
|
126
|
+
return host;
|
|
127
|
+
}
|
package/src/dynamic.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { effect, type Dispose } from "@lab206/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Values a reactive JSX child function may return.
|
|
5
|
+
* Nodes mount as DOM; strings/numbers become text; nullish clears the slot.
|
|
6
|
+
*/
|
|
7
|
+
export type DynamicChild =
|
|
8
|
+
| Node
|
|
9
|
+
| DocumentFragment
|
|
10
|
+
| string
|
|
11
|
+
| number
|
|
12
|
+
| boolean
|
|
13
|
+
| null
|
|
14
|
+
| undefined
|
|
15
|
+
| DynamicChild[];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mount a reactive child that may return **text or DOM nodes**.
|
|
19
|
+
*
|
|
20
|
+
* Previously function children were always treated as text via `String(value)`,
|
|
21
|
+
* which produced `[object HTMLParagraphElement]` when a node was returned
|
|
22
|
+
* (common for Tabs / Accordion / List content).
|
|
23
|
+
*
|
|
24
|
+
* Uses a `display: contents` host so layout is unaffected.
|
|
25
|
+
*/
|
|
26
|
+
export function bindDynamic(
|
|
27
|
+
parent: ParentNode,
|
|
28
|
+
get: () => DynamicChild,
|
|
29
|
+
): Dispose {
|
|
30
|
+
const host = document.createElement("span");
|
|
31
|
+
host.setAttribute("data-pu-dyn", "");
|
|
32
|
+
// contents: children participate in parent layout as if unwrapped
|
|
33
|
+
(host.style as CSSStyleDeclaration).display = "contents";
|
|
34
|
+
parent.appendChild(host);
|
|
35
|
+
|
|
36
|
+
let textNode: Text | null = null;
|
|
37
|
+
let mode: "empty" | "text" | "nodes" = "empty";
|
|
38
|
+
|
|
39
|
+
return effect(() => {
|
|
40
|
+
const value = get();
|
|
41
|
+
|
|
42
|
+
if (value == null || value === false || value === true) {
|
|
43
|
+
host.replaceChildren();
|
|
44
|
+
textNode = null;
|
|
45
|
+
mode = "empty";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
50
|
+
const s = String(value);
|
|
51
|
+
if (mode === "text" && textNode) {
|
|
52
|
+
textNode.data = s;
|
|
53
|
+
} else {
|
|
54
|
+
host.replaceChildren();
|
|
55
|
+
textNode = document.createTextNode(s);
|
|
56
|
+
host.appendChild(textNode);
|
|
57
|
+
mode = "text";
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Nodes / arrays — never String(object)
|
|
63
|
+
textNode = null;
|
|
64
|
+
mode = "nodes";
|
|
65
|
+
host.replaceChildren();
|
|
66
|
+
appendValue(host, value);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appendValue(parent: ParentNode, value: DynamicChild): void {
|
|
71
|
+
if (value == null || value === false || value === true) return;
|
|
72
|
+
|
|
73
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
74
|
+
parent.appendChild(document.createTextNode(String(value)));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prefer nodeType over instanceof — DocumentFragment may be missing in some test envs
|
|
79
|
+
if (
|
|
80
|
+
typeof value === "object" &&
|
|
81
|
+
value !== null &&
|
|
82
|
+
"nodeType" in (value as object) &&
|
|
83
|
+
typeof (value as Node).nodeType === "number"
|
|
84
|
+
) {
|
|
85
|
+
parent.appendChild(value as Node);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
for (const item of value) appendValue(parent, item);
|
|
91
|
+
}
|
|
92
|
+
// Non-node objects are ignored (avoids "[object HTML…]")
|
|
93
|
+
}
|
package/src/fragment.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Child } from "./h.js";
|
|
2
|
+
import { bindDynamic, type DynamicChild } from "./dynamic.js";
|
|
3
|
+
|
|
4
|
+
/** Normalize JSX children into a flat list. */
|
|
5
|
+
export function normalizeChildren(children: unknown): Child[] {
|
|
6
|
+
if (children == null || children === false || children === true) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(children)) {
|
|
10
|
+
const out: Child[] = [];
|
|
11
|
+
for (const child of children) {
|
|
12
|
+
out.push(...normalizeChildren(child));
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
return [children as Child];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Append a Child to a parent (static node, reactive text, or reactive nodes). */
|
|
20
|
+
export function appendChild(parent: ParentNode, child: Child): void {
|
|
21
|
+
if (child == null || child === false || child === true) return;
|
|
22
|
+
if (typeof child === "function") {
|
|
23
|
+
bindDynamic(parent, child as () => DynamicChild);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
27
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
parent.appendChild(child);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* JSX Fragment — groups children without a wrapper element.
|
|
35
|
+
* Returns a DocumentFragment (children move into the parent on insert).
|
|
36
|
+
*/
|
|
37
|
+
export function Fragment(props: { children?: unknown }): DocumentFragment {
|
|
38
|
+
const frag = document.createDocumentFragment();
|
|
39
|
+
for (const child of normalizeChildren(props.children)) {
|
|
40
|
+
appendChild(frag, child);
|
|
41
|
+
}
|
|
42
|
+
return frag;
|
|
43
|
+
}
|
package/src/h.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { bindAttr, bindClass, bindProp, bindStyle, bindText } from "./bind.js";
|
|
2
|
+
import { bindDynamic, type DynamicChild } from "./dynamic.js";
|
|
3
|
+
import { on } from "./on.js";
|
|
4
|
+
import { createProps } from "./props.js";
|
|
5
|
+
|
|
6
|
+
export type Child =
|
|
7
|
+
| Node
|
|
8
|
+
| DocumentFragment
|
|
9
|
+
| string
|
|
10
|
+
| number
|
|
11
|
+
| boolean
|
|
12
|
+
| null
|
|
13
|
+
| undefined
|
|
14
|
+
| (() => DynamicChild);
|
|
15
|
+
|
|
16
|
+
export type FunctionComponent<P = Record<string, unknown>> = (
|
|
17
|
+
props: P,
|
|
18
|
+
) => Node | DocumentFragment | null | undefined;
|
|
19
|
+
|
|
20
|
+
export type Props = {
|
|
21
|
+
/** Static or reactive text content (sets textContent). */
|
|
22
|
+
text?: string | number | (() => string | number | boolean | null | undefined);
|
|
23
|
+
class?: string | (() => string | Record<string, boolean | undefined | null> | null | undefined);
|
|
24
|
+
className?: string | (() => string | Record<string, boolean | undefined | null> | null | undefined);
|
|
25
|
+
style?:
|
|
26
|
+
| Record<string, string | number | null | undefined>
|
|
27
|
+
| (() => Record<string, string | number | null | undefined> | null | undefined);
|
|
28
|
+
/** ref callback with the created element */
|
|
29
|
+
ref?: (el: HTMLElement) => void;
|
|
30
|
+
children?: unknown;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RESERVED = new Set([
|
|
35
|
+
"text",
|
|
36
|
+
"class",
|
|
37
|
+
"className",
|
|
38
|
+
"style",
|
|
39
|
+
"ref",
|
|
40
|
+
"children",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create an element — or invoke a function component.
|
|
45
|
+
*
|
|
46
|
+
* Classic JSX (`jsxFactory: "h"`) compiles `<App />` to `h(App, props)`.
|
|
47
|
+
* Automatic runtime uses `jsx()` which also supports function types.
|
|
48
|
+
*/
|
|
49
|
+
export function h<K extends keyof HTMLElementTagNameMap>(
|
|
50
|
+
tag: K,
|
|
51
|
+
props?: Props | null,
|
|
52
|
+
...children: Child[]
|
|
53
|
+
): HTMLElementTagNameMap[K];
|
|
54
|
+
|
|
55
|
+
export function h(
|
|
56
|
+
tag: string,
|
|
57
|
+
props?: Props | null,
|
|
58
|
+
...children: Child[]
|
|
59
|
+
): HTMLElement;
|
|
60
|
+
|
|
61
|
+
export function h<P extends Record<string, unknown>>(
|
|
62
|
+
tag: FunctionComponent<P>,
|
|
63
|
+
props?: (P & Props) | null,
|
|
64
|
+
...children: Child[]
|
|
65
|
+
): Node | DocumentFragment;
|
|
66
|
+
|
|
67
|
+
export function h(
|
|
68
|
+
tag: string | FunctionComponent,
|
|
69
|
+
props?: Props | null,
|
|
70
|
+
...children: Child[]
|
|
71
|
+
): Node | DocumentFragment {
|
|
72
|
+
// Function component (classic JSX: h(App, props, ...children))
|
|
73
|
+
if (typeof tag === "function") {
|
|
74
|
+
const raw: Record<string, unknown> = { ...(props ?? {}) };
|
|
75
|
+
if (children.length === 1) {
|
|
76
|
+
raw.children = children[0];
|
|
77
|
+
} else if (children.length > 1) {
|
|
78
|
+
raw.children = children;
|
|
79
|
+
}
|
|
80
|
+
const result = tag(createProps(raw) as never);
|
|
81
|
+
if (result == null) {
|
|
82
|
+
return document.createComment("powers");
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof tag !== "string" || tag === "") {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[powers/dom] h() expected a tag name string or component function, got: ${typeof tag}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const el = document.createElement(tag);
|
|
94
|
+
|
|
95
|
+
if (props) {
|
|
96
|
+
applyProps(el, props);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const child of children) {
|
|
100
|
+
appendChild(el, child);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Also support props.children (automatic runtime path sometimes)
|
|
104
|
+
if (props?.children != null && children.length === 0) {
|
|
105
|
+
const c = props.children;
|
|
106
|
+
if (Array.isArray(c)) {
|
|
107
|
+
for (const item of c) appendChild(el, item as Child);
|
|
108
|
+
} else {
|
|
109
|
+
appendChild(el, c as Child);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return el;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Create a reactive or static text node. */
|
|
117
|
+
export function text(
|
|
118
|
+
value: string | number | (() => string | number | boolean | null | undefined),
|
|
119
|
+
): Text {
|
|
120
|
+
const node = document.createTextNode("");
|
|
121
|
+
if (typeof value === "function") {
|
|
122
|
+
bindText(node, value);
|
|
123
|
+
} else {
|
|
124
|
+
node.data = String(value);
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function applyProps(el: HTMLElement, props: Props): void {
|
|
130
|
+
for (const key of Object.keys(props)) {
|
|
131
|
+
if (RESERVED.has(key)) continue;
|
|
132
|
+
const value = props[key];
|
|
133
|
+
|
|
134
|
+
if (key.startsWith("on") && key.length > 2 && typeof value === "function") {
|
|
135
|
+
// onClick → click, onMouseDown → mousedown
|
|
136
|
+
const event = key.slice(2).toLowerCase();
|
|
137
|
+
on(el, event, value as EventListener);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof value === "function") {
|
|
142
|
+
if (isDomProp(key)) {
|
|
143
|
+
bindProp(el, key, value as () => unknown);
|
|
144
|
+
} else {
|
|
145
|
+
bindAttr(
|
|
146
|
+
el,
|
|
147
|
+
toAttrName(key),
|
|
148
|
+
value as () => string | number | boolean | null | undefined,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isDomProp(key)) {
|
|
155
|
+
(el as unknown as Record<string, unknown>)[key] = value;
|
|
156
|
+
} else if (value === true) {
|
|
157
|
+
el.setAttribute(toAttrName(key), "");
|
|
158
|
+
} else if (value !== false && value != null) {
|
|
159
|
+
el.setAttribute(toAttrName(key), String(value));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const classValue = props.class ?? props.className;
|
|
164
|
+
if (classValue !== undefined) {
|
|
165
|
+
if (typeof classValue === "function") {
|
|
166
|
+
bindClass(el, classValue);
|
|
167
|
+
} else if (classValue != null) {
|
|
168
|
+
el.className = classValue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (props.style !== undefined) {
|
|
173
|
+
if (typeof props.style === "function") {
|
|
174
|
+
bindStyle(el, props.style);
|
|
175
|
+
} else if (props.style) {
|
|
176
|
+
bindStyle(el, () => props.style as Record<string, string | number>);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (props.text !== undefined) {
|
|
181
|
+
if (typeof props.text === "function") {
|
|
182
|
+
bindText(el, props.text);
|
|
183
|
+
} else {
|
|
184
|
+
el.textContent = String(props.text);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof props.ref === "function") {
|
|
189
|
+
props.ref(el);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function appendChild(parent: ParentNode, child: Child): void {
|
|
194
|
+
if (child == null || child === false || child === true) return;
|
|
195
|
+
if (typeof child === "function") {
|
|
196
|
+
// May return text *or* DOM nodes (Tabs/Accordion content, etc.)
|
|
197
|
+
bindDynamic(parent, child as () => DynamicChild);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
201
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
parent.appendChild(child);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const DOM_PROPS = new Set([
|
|
208
|
+
"value",
|
|
209
|
+
"checked",
|
|
210
|
+
"selected",
|
|
211
|
+
"disabled",
|
|
212
|
+
"hidden",
|
|
213
|
+
"readOnly",
|
|
214
|
+
"multiple",
|
|
215
|
+
"muted",
|
|
216
|
+
"controls",
|
|
217
|
+
"defaultValue",
|
|
218
|
+
"defaultChecked",
|
|
219
|
+
"innerHTML",
|
|
220
|
+
"tabIndex",
|
|
221
|
+
"spellcheck",
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
function isDomProp(name: string): boolean {
|
|
225
|
+
return DOM_PROPS.has(name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toAttrName(key: string): string {
|
|
229
|
+
if (key.startsWith("aria")) {
|
|
230
|
+
return key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
231
|
+
}
|
|
232
|
+
if (key.startsWith("data") && key.length > 4 && key[4] !== "-") {
|
|
233
|
+
return key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
return key;
|
|
236
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lab206/dom
|
|
3
|
+
*
|
|
4
|
+
* Thin, explicit DOM bindings over @lab206/core.
|
|
5
|
+
*
|
|
6
|
+
* Learn order:
|
|
7
|
+
* mount → h / JSX → component → reactive props → Show / For
|
|
8
|
+
*
|
|
9
|
+
* Form controls in @lab206/ui use `bind={signal}` for two-way state —
|
|
10
|
+
* `bind` is intentionally not unwrapped by createProps (see props.ts).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { mount } from "./mount.js";
|
|
14
|
+
export type { MountResult } from "./mount.js";
|
|
15
|
+
|
|
16
|
+
export { h, text } from "./h.js";
|
|
17
|
+
export type { Child, Props, FunctionComponent } from "./h.js";
|
|
18
|
+
|
|
19
|
+
export { Fragment, normalizeChildren, appendChild } from "./fragment.js";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
bindText,
|
|
23
|
+
bindAttr,
|
|
24
|
+
bindProp,
|
|
25
|
+
bindClass,
|
|
26
|
+
bindStyle,
|
|
27
|
+
} from "./bind.js";
|
|
28
|
+
|
|
29
|
+
export { bindDynamic } from "./dynamic.js";
|
|
30
|
+
export type { DynamicChild } from "./dynamic.js";
|
|
31
|
+
|
|
32
|
+
export { on } from "./on.js";
|
|
33
|
+
export { show } from "./show.js";
|
|
34
|
+
export { list } from "./list.js";
|
|
35
|
+
export type { ListOptions } from "./list.js";
|
|
36
|
+
export { insert, remove } from "./insert.js";
|
|
37
|
+
|
|
38
|
+
export { component, Show, For } from "./component.js";
|
|
39
|
+
export type {
|
|
40
|
+
Component,
|
|
41
|
+
ComponentProps,
|
|
42
|
+
ComponentSetup,
|
|
43
|
+
} from "./component.js";
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
createProps,
|
|
47
|
+
mergeProps,
|
|
48
|
+
splitProps,
|
|
49
|
+
unwrapProp,
|
|
50
|
+
getRawProp,
|
|
51
|
+
isSignal,
|
|
52
|
+
isReactiveProps,
|
|
53
|
+
} from "./props.js";
|
|
54
|
+
export type { ReactiveProps } from "./props.js";
|
package/src/insert.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Append or insert a node. Returns the node for chaining. */
|
|
2
|
+
export function insert<T extends Node>(
|
|
3
|
+
parent: ParentNode,
|
|
4
|
+
child: T,
|
|
5
|
+
anchor: Node | null = null,
|
|
6
|
+
): T {
|
|
7
|
+
parent.insertBefore(child, anchor);
|
|
8
|
+
return child;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Remove a node from its parent if attached. */
|
|
12
|
+
export function remove(node: Node): void {
|
|
13
|
+
node.parentNode?.removeChild(node);
|
|
14
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic JSX runtime for Powers.
|
|
3
|
+
*
|
|
4
|
+
* Vite / TypeScript:
|
|
5
|
+
* jsx: "react-jsx"
|
|
6
|
+
* jsxImportSource: "@lab206/dom"
|
|
7
|
+
*
|
|
8
|
+
* Function components receive **reactive props** via `createProps`
|
|
9
|
+
* (unless they already wrapped themselves with `component()`).
|
|
10
|
+
*/
|
|
11
|
+
import { h, type Props } from "./h.js";
|
|
12
|
+
import { Fragment, normalizeChildren } from "./fragment.js";
|
|
13
|
+
import { createProps } from "./props.js";
|
|
14
|
+
|
|
15
|
+
export { Fragment };
|
|
16
|
+
|
|
17
|
+
export type FunctionComponent<P = Record<string, unknown>> = (
|
|
18
|
+
props: P,
|
|
19
|
+
) => Node | DocumentFragment | null | undefined;
|
|
20
|
+
|
|
21
|
+
/** Marker set on components that already apply createProps. */
|
|
22
|
+
export const REACTIVE_PROPS = Symbol.for("powers.reactiveProps");
|
|
23
|
+
|
|
24
|
+
export function jsx(
|
|
25
|
+
type: string | FunctionComponent<Record<string, unknown>>,
|
|
26
|
+
props: (Props & { children?: unknown }) | null,
|
|
27
|
+
_key?: string | number,
|
|
28
|
+
): Node | DocumentFragment {
|
|
29
|
+
return create(type, props);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function jsxs(
|
|
33
|
+
type: string | FunctionComponent<Record<string, unknown>>,
|
|
34
|
+
props: (Props & { children?: unknown }) | null,
|
|
35
|
+
_key?: string | number,
|
|
36
|
+
): Node | DocumentFragment {
|
|
37
|
+
return create(type, props);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function jsxDEV(
|
|
41
|
+
type: string | FunctionComponent<Record<string, unknown>>,
|
|
42
|
+
props: (Props & { children?: unknown }) | null,
|
|
43
|
+
_key?: string | number,
|
|
44
|
+
): Node | DocumentFragment {
|
|
45
|
+
return create(type, props);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function create(
|
|
49
|
+
type: string | FunctionComponent<Record<string, unknown>>,
|
|
50
|
+
props: (Props & { children?: unknown }) | null,
|
|
51
|
+
): Node | DocumentFragment {
|
|
52
|
+
const p = props ?? {};
|
|
53
|
+
const { children, ...rest } = p;
|
|
54
|
+
|
|
55
|
+
if (typeof type === "function") {
|
|
56
|
+
// Always pass through createProps so bare function components get
|
|
57
|
+
// reactive field access. component() also wraps — double-wrap is OK
|
|
58
|
+
// (outer raw object, inner Proxy of raw; component's createProps runs first).
|
|
59
|
+
const reactive = createProps(p as Record<string, unknown>);
|
|
60
|
+
const result = type(reactive as Record<string, unknown>);
|
|
61
|
+
if (result == null) {
|
|
62
|
+
return document.createComment("powers");
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const childList = normalizeChildren(children);
|
|
68
|
+
return h(type, rest as Props, ...childList);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export namespace JSX {
|
|
72
|
+
export type Element = Node | DocumentFragment;
|
|
73
|
+
export type ElementType = string | FunctionComponent<any>;
|
|
74
|
+
|
|
75
|
+
export interface ElementAttributesProperty {
|
|
76
|
+
props: unknown;
|
|
77
|
+
}
|
|
78
|
+
export interface ElementChildrenAttribute {
|
|
79
|
+
children: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type LibraryManagedAttributes<C, P> = P;
|
|
83
|
+
|
|
84
|
+
export interface IntrinsicAttributes {
|
|
85
|
+
key?: string | number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type IntrinsicElements = {
|
|
89
|
+
[K in keyof HTMLElementTagNameMap]: Props & {
|
|
90
|
+
children?: unknown;
|
|
91
|
+
} & {
|
|
92
|
+
[attr: string]: unknown;
|
|
93
|
+
};
|
|
94
|
+
} & {
|
|
95
|
+
[elemName: string]: Props & {
|
|
96
|
+
children?: unknown;
|
|
97
|
+
[attr: string]: unknown;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|