@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/list.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRoot,
|
|
3
|
+
effect,
|
|
4
|
+
signal,
|
|
5
|
+
type Dispose,
|
|
6
|
+
type Signal,
|
|
7
|
+
} from "@lab206/core";
|
|
8
|
+
import { remove } from "./insert.js";
|
|
9
|
+
|
|
10
|
+
export interface ListOptions<T> {
|
|
11
|
+
/** Stable identity for reuse. Defaults to index (fine for append-only demos). */
|
|
12
|
+
key?: (item: T, index: number) => string | number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Entry<T> = {
|
|
16
|
+
key: string | number;
|
|
17
|
+
node: Node;
|
|
18
|
+
dispose: Dispose;
|
|
19
|
+
index: Signal<number>;
|
|
20
|
+
item: Signal<T>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reactive list rendering with keyed reconciliation.
|
|
25
|
+
*
|
|
26
|
+
* **Note:** `parent` should be a dedicated container (e.g. a `ul` or `div`)
|
|
27
|
+
* whose children are owned by this list.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* list(
|
|
32
|
+
* ul,
|
|
33
|
+
* () => todos(),
|
|
34
|
+
* (item, index) => h("li", { text: () => `${index()}: ${item().title}` }),
|
|
35
|
+
* { key: (t) => t.id },
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function list<T>(
|
|
40
|
+
parent: ParentNode,
|
|
41
|
+
getItems: () => readonly T[],
|
|
42
|
+
render: (item: () => T, index: () => number) => Node,
|
|
43
|
+
options?: ListOptions<T>,
|
|
44
|
+
): Dispose {
|
|
45
|
+
const keyOf = options?.key;
|
|
46
|
+
const entries = new Map<string | number, Entry<T>>();
|
|
47
|
+
let order: Array<string | number> = [];
|
|
48
|
+
|
|
49
|
+
const stop = effect(() => {
|
|
50
|
+
const items = getItems();
|
|
51
|
+
const nextKeys: Array<string | number> = [];
|
|
52
|
+
const seen = new Set<string | number>();
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < items.length; i++) {
|
|
55
|
+
const value = items[i] as T;
|
|
56
|
+
const key = keyOf ? keyOf(value, i) : i;
|
|
57
|
+
nextKeys.push(key);
|
|
58
|
+
seen.add(key);
|
|
59
|
+
|
|
60
|
+
const existing = entries.get(key);
|
|
61
|
+
if (!existing) {
|
|
62
|
+
const index = signal(i);
|
|
63
|
+
const item = signal(value);
|
|
64
|
+
let node!: Node;
|
|
65
|
+
let dispose!: Dispose;
|
|
66
|
+
|
|
67
|
+
createRoot((d) => {
|
|
68
|
+
dispose = d;
|
|
69
|
+
node = render(
|
|
70
|
+
() => item(),
|
|
71
|
+
() => index(),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
entries.set(key, { key, node, dispose, index, item });
|
|
76
|
+
} else {
|
|
77
|
+
existing.index.set(i);
|
|
78
|
+
existing.item.set(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const key of order) {
|
|
83
|
+
if (!seen.has(key)) {
|
|
84
|
+
const entry = entries.get(key);
|
|
85
|
+
if (entry) {
|
|
86
|
+
entry.dispose();
|
|
87
|
+
remove(entry.node);
|
|
88
|
+
entries.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Move nodes into correct order (appendChild relocates existing nodes).
|
|
94
|
+
for (const key of nextKeys) {
|
|
95
|
+
const entry = entries.get(key);
|
|
96
|
+
if (entry) parent.appendChild(entry.node);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
order = nextKeys;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
stop();
|
|
104
|
+
for (const entry of entries.values()) {
|
|
105
|
+
entry.dispose();
|
|
106
|
+
remove(entry.node);
|
|
107
|
+
}
|
|
108
|
+
entries.clear();
|
|
109
|
+
order = [];
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRoot, type Dispose } from "@lab206/core";
|
|
2
|
+
import { remove } from "./insert.js";
|
|
3
|
+
|
|
4
|
+
export type MountResult = Node | Node[] | null | undefined | void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mount a reactive app into a parent node.
|
|
8
|
+
* Returns a dispose function that tears down effects and removes mounted nodes.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const stop = mount(document.getElementById("app")!, () => {
|
|
13
|
+
* const count = signal(0);
|
|
14
|
+
* return h("button", {
|
|
15
|
+
* onClick: () => count.update(n => n + 1),
|
|
16
|
+
* text: () => `Count: ${count()}`,
|
|
17
|
+
* });
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function mount(
|
|
22
|
+
parent: ParentNode,
|
|
23
|
+
app: () => MountResult,
|
|
24
|
+
): Dispose {
|
|
25
|
+
const nodes: Node[] = [];
|
|
26
|
+
let disposeRoot!: Dispose;
|
|
27
|
+
|
|
28
|
+
createRoot((dispose) => {
|
|
29
|
+
disposeRoot = dispose;
|
|
30
|
+
const result = app();
|
|
31
|
+
if (result == null) return;
|
|
32
|
+
const list = Array.isArray(result) ? result : [result];
|
|
33
|
+
for (const node of list) {
|
|
34
|
+
parent.appendChild(node);
|
|
35
|
+
nodes.push(node);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
disposeRoot();
|
|
41
|
+
for (const node of nodes) {
|
|
42
|
+
remove(node);
|
|
43
|
+
}
|
|
44
|
+
nodes.length = 0;
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/on.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Dispose } from "@lab206/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attach a DOM event listener. Returns a dispose that removes it.
|
|
5
|
+
* Does not track signals inside the handler (handlers are events, not renders).
|
|
6
|
+
*/
|
|
7
|
+
export function on<K extends keyof HTMLElementEventMap>(
|
|
8
|
+
el: EventTarget,
|
|
9
|
+
type: K,
|
|
10
|
+
handler: (event: HTMLElementEventMap[K]) => void,
|
|
11
|
+
options?: boolean | AddEventListenerOptions,
|
|
12
|
+
): Dispose;
|
|
13
|
+
|
|
14
|
+
export function on(
|
|
15
|
+
el: EventTarget,
|
|
16
|
+
type: string,
|
|
17
|
+
handler: EventListenerOrEventListenerObject,
|
|
18
|
+
options?: boolean | AddEventListenerOptions,
|
|
19
|
+
): Dispose;
|
|
20
|
+
|
|
21
|
+
export function on(
|
|
22
|
+
el: EventTarget,
|
|
23
|
+
type: string,
|
|
24
|
+
handler: EventListenerOrEventListenerObject,
|
|
25
|
+
options?: boolean | AddEventListenerOptions,
|
|
26
|
+
): Dispose {
|
|
27
|
+
el.addEventListener(type, handler, options);
|
|
28
|
+
return () => {
|
|
29
|
+
el.removeEventListener(type, handler, options);
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { Signal } from "@lab206/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reactive component props.
|
|
5
|
+
*
|
|
6
|
+
* Access like normal fields (`props.name`). If the parent passed a signal or
|
|
7
|
+
* a zero-arg accessor `() => …`, the value is unwrapped **on read** and tracked.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Parent — pass a signal or accessor for live updates:
|
|
12
|
+
* <Hello name={name} />
|
|
13
|
+
* <Hello name={() => user().name} />
|
|
14
|
+
*
|
|
15
|
+
* // Child — read as a plain field inside reactive scopes:
|
|
16
|
+
* <p>{() => props.name}</p>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type ReactiveProps<P extends object> = {
|
|
20
|
+
readonly [K in keyof P]: P[K];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const reactivePropsSet = new WeakSet<object>();
|
|
24
|
+
|
|
25
|
+
/** True if value looks like a Powers signal. */
|
|
26
|
+
export function isSignal(value: unknown): value is Signal<unknown> {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === "function" &&
|
|
29
|
+
typeof (value as Signal<unknown>).peek === "function" &&
|
|
30
|
+
typeof (value as Signal<unknown>).set === "function"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True if this object was created by `createProps` / `mergeProps` / `splitProps`. */
|
|
35
|
+
export function isReactiveProps(value: unknown): value is object {
|
|
36
|
+
return typeof value === "object" && value !== null && reactivePropsSet.has(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isEventKey(key: string): boolean {
|
|
40
|
+
return (
|
|
41
|
+
key.startsWith("on") && key.length > 2 && key[2] === key[2]!.toUpperCase()
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Unwrap a prop value for reactive read.
|
|
47
|
+
* - Signals → call (tracked)
|
|
48
|
+
* - Zero-arg functions (accessors) → call (tracked)
|
|
49
|
+
* - Event handlers / multi-arg fns → return as-is
|
|
50
|
+
* - Everything else → as-is
|
|
51
|
+
*/
|
|
52
|
+
export function unwrapProp(key: string, value: unknown): unknown {
|
|
53
|
+
// children + bind must stay as passed (bind is a signal ref for two-way forms)
|
|
54
|
+
if (key === "children" || key === "bind") return value;
|
|
55
|
+
if (typeof value !== "function") return value;
|
|
56
|
+
if (isEventKey(key)) return value;
|
|
57
|
+
if (isSignal(value)) return value();
|
|
58
|
+
if (value.length === 0) return (value as () => unknown)();
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawSourceMap = new WeakMap<object, object>();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Peek at the raw prop bag value without unwrapping signals/accessors.
|
|
66
|
+
* Useful for dev warnings (e.g. detect snapshot `value={sig()}` vs `value={sig}`).
|
|
67
|
+
*/
|
|
68
|
+
export function getRawProp(props: object, key: string): unknown {
|
|
69
|
+
const src = rawSourceMap.get(props) ?? props;
|
|
70
|
+
return Reflect.get(src, key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wrap a raw props object so property access is reactive.
|
|
75
|
+
* Idempotent — wrapping an existing reactive props object returns it as-is.
|
|
76
|
+
*/
|
|
77
|
+
export function createProps<P extends object>(raw: P): ReactiveProps<P> {
|
|
78
|
+
if (isReactiveProps(raw)) {
|
|
79
|
+
return raw as ReactiveProps<P>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const source = (raw ?? {}) as object;
|
|
83
|
+
|
|
84
|
+
const proxy = new Proxy(source, {
|
|
85
|
+
get(target, key, receiver) {
|
|
86
|
+
if (typeof key === "symbol") {
|
|
87
|
+
return Reflect.get(target, key, receiver);
|
|
88
|
+
}
|
|
89
|
+
const value = Reflect.get(target, key, receiver);
|
|
90
|
+
return unwrapProp(key, value);
|
|
91
|
+
},
|
|
92
|
+
set() {
|
|
93
|
+
return false;
|
|
94
|
+
},
|
|
95
|
+
has(target, key) {
|
|
96
|
+
return Reflect.has(target, key);
|
|
97
|
+
},
|
|
98
|
+
ownKeys(target) {
|
|
99
|
+
return Reflect.ownKeys(target);
|
|
100
|
+
},
|
|
101
|
+
getOwnPropertyDescriptor(target, key) {
|
|
102
|
+
if (!Reflect.has(target, key)) return undefined;
|
|
103
|
+
return {
|
|
104
|
+
enumerable: true,
|
|
105
|
+
configurable: true,
|
|
106
|
+
get: () =>
|
|
107
|
+
unwrapProp(String(key), Reflect.get(target, key)),
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
reactivePropsSet.add(proxy);
|
|
113
|
+
rawSourceMap.set(proxy, source);
|
|
114
|
+
return proxy as ReactiveProps<P>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type AnyProps = Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge default props with overrides. Later sources win.
|
|
121
|
+
* Accessors stay lazy — resolved only when the child reads a key.
|
|
122
|
+
*/
|
|
123
|
+
export function mergeProps<T extends object, U extends object>(
|
|
124
|
+
defaults: T,
|
|
125
|
+
props: U,
|
|
126
|
+
): ReactiveProps<T & U>;
|
|
127
|
+
export function mergeProps<T extends object, U extends object, V extends object>(
|
|
128
|
+
a: T,
|
|
129
|
+
b: U,
|
|
130
|
+
c: V,
|
|
131
|
+
): ReactiveProps<T & U & V>;
|
|
132
|
+
export function mergeProps(...sources: object[]): ReactiveProps<object> {
|
|
133
|
+
const list = sources.filter(Boolean) as AnyProps[];
|
|
134
|
+
|
|
135
|
+
const proxy = new Proxy(
|
|
136
|
+
{},
|
|
137
|
+
{
|
|
138
|
+
get(_target, key) {
|
|
139
|
+
if (typeof key === "symbol") return undefined;
|
|
140
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
141
|
+
const src = list[i]!;
|
|
142
|
+
if (key in src) {
|
|
143
|
+
// If source is already reactive props, read through it
|
|
144
|
+
// (already unwrapped). Prefer raw: if reactive, reading is fine.
|
|
145
|
+
const value = src[key];
|
|
146
|
+
// When reading from reactive source, value is already unwrapped.
|
|
147
|
+
// When reading from plain source, unwrap.
|
|
148
|
+
if (isReactiveProps(src)) return value;
|
|
149
|
+
return unwrapProp(key, value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
},
|
|
154
|
+
has(_target, key) {
|
|
155
|
+
if (typeof key === "symbol") return false;
|
|
156
|
+
return list.some((src) => key in src);
|
|
157
|
+
},
|
|
158
|
+
ownKeys() {
|
|
159
|
+
const keys = new Set<string | symbol>();
|
|
160
|
+
for (const src of list) {
|
|
161
|
+
for (const k of Reflect.ownKeys(src)) keys.add(k);
|
|
162
|
+
}
|
|
163
|
+
return [...keys];
|
|
164
|
+
},
|
|
165
|
+
getOwnPropertyDescriptor(_target, key) {
|
|
166
|
+
if (typeof key === "symbol") return undefined;
|
|
167
|
+
if (!list.some((src) => key in src)) return undefined;
|
|
168
|
+
return {
|
|
169
|
+
enumerable: true,
|
|
170
|
+
configurable: true,
|
|
171
|
+
get: () => {
|
|
172
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
173
|
+
const src = list[i]!;
|
|
174
|
+
if (key in src) {
|
|
175
|
+
if (isReactiveProps(src)) return src[key];
|
|
176
|
+
return unwrapProp(String(key), src[key]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
reactivePropsSet.add(proxy);
|
|
187
|
+
return proxy as ReactiveProps<object>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Split props into a picked reactive bag + the rest.
|
|
192
|
+
* Both bags read through the same underlying props (no snapshot).
|
|
193
|
+
*/
|
|
194
|
+
export function splitProps<P extends object, K extends keyof P>(
|
|
195
|
+
props: P,
|
|
196
|
+
keys: readonly K[],
|
|
197
|
+
): [ReactiveProps<Pick<P, K>>, ReactiveProps<Omit<P, K>>] {
|
|
198
|
+
const keySet = new Set(keys as readonly string[]);
|
|
199
|
+
|
|
200
|
+
const picked = new Proxy({} as object, {
|
|
201
|
+
get(_t, key) {
|
|
202
|
+
if (typeof key === "symbol") return undefined;
|
|
203
|
+
if (!keySet.has(key)) return undefined;
|
|
204
|
+
return (props as AnyProps)[key];
|
|
205
|
+
},
|
|
206
|
+
has(_t, key) {
|
|
207
|
+
return typeof key === "string" && keySet.has(key) && key in (props as object);
|
|
208
|
+
},
|
|
209
|
+
ownKeys() {
|
|
210
|
+
return [...keySet].filter((k) => k in (props as object));
|
|
211
|
+
},
|
|
212
|
+
getOwnPropertyDescriptor(_t, key) {
|
|
213
|
+
if (typeof key !== "string" || !keySet.has(key)) return undefined;
|
|
214
|
+
if (!(key in (props as object))) return undefined;
|
|
215
|
+
return {
|
|
216
|
+
enumerable: true,
|
|
217
|
+
configurable: true,
|
|
218
|
+
get: () => (props as AnyProps)[key],
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const rest = new Proxy({} as object, {
|
|
224
|
+
get(_t, key) {
|
|
225
|
+
if (typeof key === "symbol") return undefined;
|
|
226
|
+
if (keySet.has(key)) return undefined;
|
|
227
|
+
return (props as AnyProps)[key];
|
|
228
|
+
},
|
|
229
|
+
has(_t, key) {
|
|
230
|
+
return (
|
|
231
|
+
typeof key === "string" &&
|
|
232
|
+
!keySet.has(key) &&
|
|
233
|
+
key in (props as object)
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
ownKeys() {
|
|
237
|
+
return Reflect.ownKeys(props as object).filter(
|
|
238
|
+
(k) => typeof k !== "string" || !keySet.has(k),
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
getOwnPropertyDescriptor(_t, key) {
|
|
242
|
+
if (typeof key !== "string" || keySet.has(key)) return undefined;
|
|
243
|
+
if (!(key in (props as object))) return undefined;
|
|
244
|
+
return {
|
|
245
|
+
enumerable: true,
|
|
246
|
+
configurable: true,
|
|
247
|
+
get: () => (props as AnyProps)[key],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
reactivePropsSet.add(picked);
|
|
253
|
+
reactivePropsSet.add(rest);
|
|
254
|
+
|
|
255
|
+
return [
|
|
256
|
+
picked as ReactiveProps<Pick<P, K>>,
|
|
257
|
+
rest as ReactiveProps<Omit<P, K>>,
|
|
258
|
+
];
|
|
259
|
+
}
|
package/src/show.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRoot,
|
|
3
|
+
effect,
|
|
4
|
+
type Dispose,
|
|
5
|
+
} from "@lab206/core";
|
|
6
|
+
import { remove } from "./insert.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Conditionally mount a DOM subtree.
|
|
10
|
+
* When `when()` is false, the subtree is disposed and removed.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* show(parent, () => open(), () => h("div", null, "Hello"));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function show(
|
|
18
|
+
parent: ParentNode,
|
|
19
|
+
when: () => boolean,
|
|
20
|
+
factory: () => Node,
|
|
21
|
+
anchor: Node | null = null,
|
|
22
|
+
): Dispose {
|
|
23
|
+
let childDispose: Dispose | undefined;
|
|
24
|
+
let node: Node | undefined;
|
|
25
|
+
|
|
26
|
+
const stop = effect(() => {
|
|
27
|
+
if (when()) {
|
|
28
|
+
if (!node) {
|
|
29
|
+
createRoot((dispose) => {
|
|
30
|
+
childDispose = dispose;
|
|
31
|
+
node = factory();
|
|
32
|
+
parent.insertBefore(node, anchor);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} else if (node) {
|
|
36
|
+
childDispose?.();
|
|
37
|
+
childDispose = undefined;
|
|
38
|
+
remove(node);
|
|
39
|
+
node = undefined;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
stop();
|
|
45
|
+
childDispose?.();
|
|
46
|
+
childDispose = undefined;
|
|
47
|
+
if (node) {
|
|
48
|
+
remove(node);
|
|
49
|
+
node = undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Window } from "happy-dom";
|
|
2
|
+
|
|
3
|
+
/** Install a happy-dom window as the global document/window for tests. */
|
|
4
|
+
export function installDom(): { window: Window; document: Document } {
|
|
5
|
+
const window = new Window({ url: "https://localhost/" });
|
|
6
|
+
const document = window.document as unknown as Document;
|
|
7
|
+
|
|
8
|
+
const g = globalThis as unknown as Record<string, unknown>;
|
|
9
|
+
g.window = window;
|
|
10
|
+
g.document = document;
|
|
11
|
+
g.Node = window.Node;
|
|
12
|
+
g.HTMLElement = window.HTMLElement;
|
|
13
|
+
g.Text = window.Text;
|
|
14
|
+
g.Element = window.Element;
|
|
15
|
+
|
|
16
|
+
return { window, document };
|
|
17
|
+
}
|