defuss-morph 0.1.1 → 0.3.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 +34 -0
- package/README.md +281 -212
- package/dist/all.js +639 -429
- package/dist/all.js.map +15 -11
- package/dist/all.min.js +1 -1
- package/dist/all.min.js.map +1 -1
- package/dist/index.cjs +520 -347
- package/dist/index.d.ts +338 -28
- package/dist/index.mjs +507 -348
- package/dist/stats.json +9 -9
- package/package.json +46 -13
- package/skills/defuss-morph/SKILL.md +101 -0
- package/skills/defuss-morph/references/api.md +86 -0
- package/skills/defuss-morph/references/integrations.md +116 -0
- package/skills/defuss-morph/references/patterns.md +190 -0
- package/skills/defuss-morph/references/pitfalls.md +29 -0
package/dist/index.d.ts
CHANGED
|
@@ -24,10 +24,19 @@ interface VNodeAttributes {
|
|
|
24
24
|
[attributeName: string]: any;
|
|
25
25
|
}
|
|
26
26
|
type VNodeType = string | Function | any;
|
|
27
|
+
/**
|
|
28
|
+
* A virtual node as JSX factories produce it: `type` is a tag name (or `"#comment"`), `attributes`
|
|
29
|
+
* the props, `children` nested VNodes, text, numbers or values that render nothing.
|
|
30
|
+
*/
|
|
27
31
|
interface VNode<A = VNodeAttributes> {
|
|
28
32
|
type?: VNodeType;
|
|
29
33
|
attributes?: A;
|
|
30
34
|
children?: VNodeChildren;
|
|
35
|
+
/**
|
|
36
|
+
* Comment data of a `{ type: "#comment" }` VNode (see `COMMENT_TYPE`).
|
|
37
|
+
* VERIFIED: optional, so defuss core's VNode (no `value`) stays assignable.
|
|
38
|
+
*/
|
|
39
|
+
value?: string;
|
|
31
40
|
sourceInfo?: unknown;
|
|
32
41
|
/** Original props passed to a function component (set by jsx runtime for SSG hydration). */
|
|
33
42
|
componentProps?: Record<string, any>;
|
|
@@ -38,6 +47,7 @@ type VNodeChildren = VNodeChild[];
|
|
|
38
47
|
* Anything the morph engine accepts as new content.
|
|
39
48
|
*/
|
|
40
49
|
type RenderInput = VNode | object | string | number | boolean | null | undefined | RenderInput[];
|
|
50
|
+
/** The renderer contract returned by `getRenderer()`. */
|
|
41
51
|
interface DomAbstractionImpl {
|
|
42
52
|
hasElNamespace(domElement: Element | Document): boolean;
|
|
43
53
|
hasSvgNamespace(parentElement: Element | Document, type: string): boolean;
|
|
@@ -49,30 +59,14 @@ interface DomAbstractionImpl {
|
|
|
49
59
|
setAttributes(virtualNode: VNode<VNodeAttributes>, domElement: Element): void;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
exit: Record<string, string>;
|
|
57
|
-
exitActive: Record<string, string>;
|
|
58
|
-
}
|
|
59
|
-
type TransitionsEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end";
|
|
60
|
-
interface TransitionConfig {
|
|
61
|
-
type?: TransitionType;
|
|
62
|
-
styles?: TransitionStyles;
|
|
63
|
-
duration?: number;
|
|
64
|
-
easing?: TransitionsEasing | string;
|
|
65
|
-
delay?: number;
|
|
66
|
-
target?: "parent" | "self";
|
|
67
|
-
}
|
|
68
|
-
declare const getTransitionStyles: (type: TransitionType, duration: number, easing?: string) => TransitionStyles;
|
|
69
|
-
declare const applyStyles: (el: HTMLElement, styles: Record<string, string | number>) => void;
|
|
70
|
-
declare const DEFAULT_TRANSITION_CONFIG: TransitionConfig;
|
|
71
|
-
declare const performTransition: (element: HTMLElement, updateCallback: () => Promise<void>, config?: TransitionConfig) => Promise<void>;
|
|
72
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Wraps `cb` so each call runs it in a microtask, with the same arguments.
|
|
64
|
+
* VERIFIED: renderer.test.ts asserts onMount has not run before that microtask.
|
|
65
|
+
*/
|
|
73
66
|
declare const queueCallback: <T extends any[]>(cb: (...args: T) => void) => (...args: T) => void;
|
|
74
67
|
|
|
75
68
|
type DelegatedPhase = "bubble" | "capture";
|
|
69
|
+
/** Options of {@link registerDelegatedEvent}. */
|
|
76
70
|
interface DelegatedEventOptions {
|
|
77
71
|
capture?: boolean;
|
|
78
72
|
/** If true, allows multiple handlers per element+type (Dequery mode) */
|
|
@@ -82,15 +76,42 @@ interface ParsedEventProp {
|
|
|
82
76
|
eventType: string;
|
|
83
77
|
capture: boolean;
|
|
84
78
|
}
|
|
85
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Events whose handlers are forced into the capture phase, so an ancestor's
|
|
81
|
+
* handler also sees descendants' events (container onFocus/onBlur). Every
|
|
82
|
+
* other non-bubbling event reaches only its target's handlers, natively.
|
|
83
|
+
*/
|
|
86
84
|
declare const CAPTURE_ONLY_EVENTS: Set<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Parses a JSX event prop name: `onClick` gives `{ eventType: "click", capture: false }`,
|
|
87
|
+
* `onClickCapture` the capture phase. Case-insensitive; `null` for anything not starting with `on`.
|
|
88
|
+
*/
|
|
87
89
|
declare const parseEventPropName: (propName: string) => ParsedEventProp | null;
|
|
90
|
+
/**
|
|
91
|
+
* Attaches `handler` to `element` the way JSX props do: one slot per event type and phase, so
|
|
92
|
+
* registering again replaces the handler without adding a listener. With `{ multi: true }`
|
|
93
|
+
* every handler becomes its own native listener (dequery-style `on()`). `focus`, `blur`,
|
|
94
|
+
* `scroll`, `mouseenter` and `mouseleave` always use the capture phase.
|
|
95
|
+
*/
|
|
88
96
|
declare const registerDelegatedEvent: (element: HTMLElement, eventType: string, handler: EventListener, options?: DelegatedEventOptions) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Removes `handler` (from both phases), or every handler of `eventType` when no handler is given.
|
|
99
|
+
*/
|
|
89
100
|
declare const removeDelegatedEvent: (target: EventTarget, eventType: string, handler?: EventListener, _options?: DelegatedEventOptions) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Why: listeners a facade attaches natively (defuss-query's on()) live
|
|
103
|
+
* outside this registry, yet must go whenever morph clears an element
|
|
104
|
+
* (removal, replacement, clearDelegatedEventsDeep). A hook keeps morph
|
|
105
|
+
* ignorant of who owns them, instead of morph also tracking native listeners.
|
|
106
|
+
* Registering the same hook twice is a no-op; returns an unregister function.
|
|
107
|
+
*/
|
|
108
|
+
declare const onClearDelegatedEvents: (hook: (target: EventTarget) => void) => (() => void);
|
|
109
|
+
/** Removes every registered handler of one element and runs the {@link onClearDelegatedEvents} hooks for it. */
|
|
90
110
|
declare const clearDelegatedEvents: (target: EventTarget) => void;
|
|
91
111
|
/**
|
|
92
|
-
* Clear delegated events for an element and all its descendants.
|
|
93
|
-
*
|
|
112
|
+
* Clear delegated events for an element and all its descendants. Morph runs
|
|
113
|
+
* it on every element it removes or replaces; facades call it before removing
|
|
114
|
+
* nodes themselves (defuss-query does in its internal remove()).
|
|
94
115
|
*/
|
|
95
116
|
declare const clearDelegatedEventsDeep: (root: HTMLElement) => void;
|
|
96
117
|
/**
|
|
@@ -120,9 +141,27 @@ declare const nsMap: {
|
|
|
120
141
|
xlink: string;
|
|
121
142
|
svg: string;
|
|
122
143
|
};
|
|
144
|
+
/**
|
|
145
|
+
* Calls `onUnmount` once `domNode` leaves the document. A move (removal and re-insertion in the
|
|
146
|
+
* same task) is not an unmount: the observer follows the node to its new parent. Throws when the
|
|
147
|
+
* node has no parent; a no-op where `MutationObserver` does not exist (SSR).
|
|
148
|
+
*/
|
|
123
149
|
declare const observeUnmount: (domNode: Node, onUnmount: () => void) => void;
|
|
124
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Runs the pending `onMount` hook and arms the `onUnmount` observer of an
|
|
152
|
+
* element that was just inserted or patched in place.
|
|
153
|
+
*
|
|
154
|
+
* Why one observer that reads `$onUnmount` when it fires: morph calls this on
|
|
155
|
+
* every in-place patch, and each re-render sets a fresh `onUnmount` closure.
|
|
156
|
+
* Arming one observer per call made a single removal run every handler ever
|
|
157
|
+
* registered (VERIFIED: 1 render + 3 morphs fired onUnmount 10 times).
|
|
158
|
+
*/
|
|
125
159
|
declare const handleLifecycleEventsForOnMount: (newEl: HTMLElement) => void;
|
|
160
|
+
/**
|
|
161
|
+
* Returns the VNode-to-DOM renderer for `document`: element and SVG creation, text and comment
|
|
162
|
+
* nodes, attributes (class arrays, style objects, controlled form props, namespaced SVG
|
|
163
|
+
* attributes), refs, JSX event props and the `onMount`/`onUnmount` hooks.
|
|
164
|
+
*/
|
|
126
165
|
declare const getRenderer: (document: Document) => DomAbstractionImpl;
|
|
127
166
|
|
|
128
167
|
/**
|
|
@@ -130,7 +169,7 @@ declare const getRenderer: (document: Document) => DomAbstractionImpl;
|
|
|
130
169
|
* 1. Checks for reference equality.
|
|
131
170
|
* 2. Compares node types.
|
|
132
171
|
* 3. For Element nodes, compares tag names and attributes.
|
|
133
|
-
* 4. For Text nodes, compares
|
|
172
|
+
* 4. For Text and Comment nodes, compares their data.
|
|
134
173
|
*/
|
|
135
174
|
declare const areDomNodesEqual: (oldNode: Node, newNode: Node) => boolean;
|
|
136
175
|
/********************************************************
|
|
@@ -182,12 +221,36 @@ declare function replaceDomWithVdom(parentElement: Element, newVDOM: RenderInput
|
|
|
182
221
|
* (same rationale as uncontrolled form-state preservation).
|
|
183
222
|
*/
|
|
184
223
|
declare const FROM_DOM_MARKER: unique symbol;
|
|
224
|
+
/**
|
|
225
|
+
* VNode type of an HTML comment: `{ type: "#comment", value }`.
|
|
226
|
+
* Why "#comment": it is the DOM's own nodeName for comments and can never
|
|
227
|
+
* collide with a tag name ("#" is invalid there). The data lives in `value`,
|
|
228
|
+
* not `children`, so generic tree walkers never mistake it for content.
|
|
229
|
+
*/
|
|
230
|
+
declare const COMMENT_TYPE = "#comment";
|
|
231
|
+
type CommentVNode = VNode & {
|
|
232
|
+
type: typeof COMMENT_TYPE;
|
|
233
|
+
};
|
|
234
|
+
/** True for a comment VNode (`{ type: "#comment", value }`). */
|
|
235
|
+
declare const isCommentVNode: (value: unknown) => value is CommentVNode;
|
|
236
|
+
/** Parses `input` into a detached document with the given parser. */
|
|
185
237
|
declare function parseDOM(input: string, type: DOMParserSupportedType, Parser: typeof DOMParser): Document;
|
|
238
|
+
/** True when `input` parses as an SVG document (root element `<svg>`). */
|
|
186
239
|
declare function isSVG(input: string, Parser: typeof DOMParser): boolean;
|
|
240
|
+
/** True when `input` parses into at least one element besides the implied `<head>` and `<body>`. */
|
|
187
241
|
declare function isHTML(input: string, Parser: typeof DOMParser): boolean;
|
|
242
|
+
/** True when `input` contains a tag and parses as HTML or SVG. */
|
|
188
243
|
declare const isMarkup: (input: string, Parser: typeof DOMParser) => boolean;
|
|
244
|
+
/** Parses `markup` (HTML or SVG) and returns its top-level nodes, detached from any live document. */
|
|
189
245
|
declare function renderMarkup(markup: string, Parser: typeof DOMParser, doc?: Document): ChildNode[];
|
|
246
|
+
/** The `DOMParser` MIME type for `input`: `image/svg+xml` for SVG documents, else `text/html`. */
|
|
190
247
|
declare function getMimeType(input: string, Parser: typeof DOMParser): DOMParserSupportedType;
|
|
248
|
+
/**
|
|
249
|
+
* The content fragment of a `<template>` element, where the parser puts its markup; `undefined` for other elements.
|
|
250
|
+
* morph reads, creates and patches a template's children there, so markup such as defuss-i18n's translation
|
|
251
|
+
* templates survives a morph.
|
|
252
|
+
*/
|
|
253
|
+
declare const templateContent: (element: Element) => DocumentFragment | undefined;
|
|
191
254
|
/**
|
|
192
255
|
* Converts a DOM node to a VNode structure for use with updateDomWithVdom.
|
|
193
256
|
* This allows us to leverage the sophisticated partial update system even for Node inputs.
|
|
@@ -199,6 +262,43 @@ declare function domNodeToVNode(node: Node): VNode<VNodeAttributes> | string;
|
|
|
199
262
|
*/
|
|
200
263
|
declare function htmlStringToVNodes(html: string, Parser: typeof DOMParser): Array<VNode<VNodeAttributes> | string>;
|
|
201
264
|
|
|
265
|
+
type TransitionType = "fade" | "slide-left" | "slide-right" | "shake" | "none";
|
|
266
|
+
interface TransitionStyles {
|
|
267
|
+
enter: Record<string, string>;
|
|
268
|
+
enterActive: Record<string, string>;
|
|
269
|
+
exit: Record<string, string>;
|
|
270
|
+
exitActive: Record<string, string>;
|
|
271
|
+
}
|
|
272
|
+
type TransitionsEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end";
|
|
273
|
+
/** The `transition` option of `morph()`; every field is optional (see {@link DEFAULT_TRANSITION_CONFIG}). */
|
|
274
|
+
interface TransitionConfig {
|
|
275
|
+
/** Built-in animation; `"none"` keeps `morph()` synchronous. Default `"fade"`. */
|
|
276
|
+
type?: TransitionType;
|
|
277
|
+
/** Own style maps for the four phases (not used by `"fade"`). */
|
|
278
|
+
styles?: TransitionStyles;
|
|
279
|
+
/** Milliseconds per phase. Default 300. */
|
|
280
|
+
duration?: number;
|
|
281
|
+
/** CSS timing function. Default `"ease-in-out"`. */
|
|
282
|
+
easing?: TransitionsEasing | string;
|
|
283
|
+
/** Milliseconds to wait before starting. Default 0. */
|
|
284
|
+
delay?: number;
|
|
285
|
+
/** Animate the morphed element's parent (default) or the element itself. */
|
|
286
|
+
target?: "parent" | "self";
|
|
287
|
+
}
|
|
288
|
+
/** The four phase style maps of a built-in transition type (empty maps for an unknown type). */
|
|
289
|
+
declare const getTransitionStyles: (type: TransitionType, duration: number, easing?: string) => TransitionStyles;
|
|
290
|
+
/** Sets each CSS property of `styles` on `el` (`style.setProperty`). */
|
|
291
|
+
declare const applyStyles: (el: HTMLElement, styles: Record<string, string | number>) => void;
|
|
292
|
+
/** Defaults merged under every transition config. */
|
|
293
|
+
declare const DEFAULT_TRANSITION_CONFIG: TransitionConfig;
|
|
294
|
+
/**
|
|
295
|
+
* Runs `updateCallback` inside a transition on `element`. `fade` cross-fades a snapshot of the old
|
|
296
|
+
* content over the new one; the other types animate out, run the update, then animate in. Styles
|
|
297
|
+
* the transition set are restored afterwards, also when the update throws (the error propagates).
|
|
298
|
+
*/
|
|
299
|
+
declare const performTransition: (element: HTMLElement, updateCallback: () => Promise<void>, config?: TransitionConfig) => Promise<void>;
|
|
300
|
+
|
|
301
|
+
/** Options of {@link morph}. */
|
|
202
302
|
interface MorphOptions {
|
|
203
303
|
/**
|
|
204
304
|
* Optional transition to apply around the morph.
|
|
@@ -227,7 +327,217 @@ interface MorphOptions {
|
|
|
227
327
|
*/
|
|
228
328
|
diff?: boolean;
|
|
229
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Morphs the children of `el` to match the given HTML string or JSX/VNode input.
|
|
332
|
+
*
|
|
333
|
+
* A string is parsed into a DOM tree via the native `DOMParser`
|
|
334
|
+
* of the element's own document (isomorphic: browser, happy-dom, ...);
|
|
335
|
+
* any other `RenderInput` (JSX, VNodes, arrays, text) is used directly.
|
|
336
|
+
* Both paths then morph into the existing DOM using the defuss morph algorithm:
|
|
337
|
+
*
|
|
338
|
+
* - key/id-aware node matching (`key` and `id` attributes)
|
|
339
|
+
* - move-not-replace semantics (preserves node identity, focus, selection
|
|
340
|
+
* and event listeners)
|
|
341
|
+
* - in-place attribute patching for same-tag elements
|
|
342
|
+
* - uncontrolled form-state preservation (live input values survive
|
|
343
|
+
* unless the new HTML explicitly controls them)
|
|
344
|
+
* - shadow DOM / custom element (light DOM) aware
|
|
345
|
+
* - re-entrant morphs on the same or an ancestor element are queued
|
|
346
|
+
* latest-wins and replayed after the active morph finishes
|
|
347
|
+
* - overlapping transitions are latest-wins too: a morph issued while a
|
|
348
|
+
* transition is in flight replaces the content that transition will apply
|
|
349
|
+
*
|
|
350
|
+
* Plain text input morphs as a text node.
|
|
351
|
+
*
|
|
352
|
+
* Unlike an HTML string, a VNode/JSX input can express explicit state
|
|
353
|
+
* (`checked: false`, `value: ""`) and carry event handlers.
|
|
354
|
+
*
|
|
355
|
+
* @param el the element whose children (or shadow root) are morphed
|
|
356
|
+
* @param newContent HTML string, JSX/VNode(s) or text describing the next children
|
|
357
|
+
* @param options `transition` animates the swap; `diff` applies a partial change-set
|
|
358
|
+
* @returns `undefined` without a transition (the morph is synchronous); with
|
|
359
|
+
* one, a Promise that resolves once the transition finished
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* import { morph } from "defuss-morph";
|
|
364
|
+
*
|
|
365
|
+
* morph(document.getElementById("app")!, "<ul><li key='a'>Hello</li></ul>");
|
|
366
|
+
*
|
|
367
|
+
* // JSX / VNodes work too (same API, same algorithm):
|
|
368
|
+
* morph(document.getElementById("app")!, <p>Hi</p>);
|
|
369
|
+
*
|
|
370
|
+
* // with a transition (async):
|
|
371
|
+
* await morph(document.getElementById("app")!, "<p>Hi</p>", {
|
|
372
|
+
* transition: { type: "fade", duration: 200 },
|
|
373
|
+
* });
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
230
376
|
declare const morph: (el: Element, newContent: RenderInput, options?: MorphOptions) => void | Promise<void>;
|
|
231
377
|
|
|
232
|
-
|
|
233
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Why: morph() takes VNodes, and JSX is the readable way to write them, but a JSX compiler needs a runtime to call.
|
|
380
|
+
* Without one here, using `<Reactive>` or a ref meant installing the whole defuss framework for its `jsx()`. This
|
|
381
|
+
* is the smallest runtime that fits morph: it returns plain `{ type, attributes, children }` objects, calls function
|
|
382
|
+
* components at once (there is no component tree to keep, the next render is a new call) and has no state.
|
|
383
|
+
*
|
|
384
|
+
* - automatic runtime: `jsxImportSource: "defuss-morph"` (TypeScript, Bun, esbuild, Vite) imports `jsx`, `jsxs` and
|
|
385
|
+
* `Fragment` from `defuss-morph/jsx-runtime` (`jsxDEV` from `defuss-morph/jsx-dev-runtime`);
|
|
386
|
+
* - classic runtime and pages without a build step: `h(type, props, ...children)`.
|
|
387
|
+
*
|
|
388
|
+
* VERIFIED: TypeScript, Bun, esbuild and Vite take `jsxImportSource` from tsconfig.json (probed 2026-11-01; Bun
|
|
389
|
+
* imports `jsx-dev-runtime` by default, so both subpaths exist); scripts/e2e/consumer.tsx type-checks JSX against
|
|
390
|
+
* the packed declarations in every `make e2e`.
|
|
391
|
+
*/
|
|
392
|
+
/** A function component: called with its props (including `children`, `ref` and `key`), returns what to render. */
|
|
393
|
+
type Component<P = any> = (props: P) => RenderInput;
|
|
394
|
+
type Props = {
|
|
395
|
+
[name: string]: unknown;
|
|
396
|
+
children?: unknown;
|
|
397
|
+
key?: DefussKey;
|
|
398
|
+
};
|
|
399
|
+
/** `<>…</>`: its children, rendered in place of the fragment. */
|
|
400
|
+
declare const Fragment: (props: {
|
|
401
|
+
children?: unknown;
|
|
402
|
+
}) => RenderInput;
|
|
403
|
+
/**
|
|
404
|
+
* The automatic-runtime factory: `jsx(type, props, key)`. A tag name gives a VNode; a function component is called
|
|
405
|
+
* with `props` (children included). A `key` given to a component lands on the element it returns, unless that
|
|
406
|
+
* element has one, so `items.map((item) => <Row key={item.id} item={item} />)` is keyed for morph.
|
|
407
|
+
*/
|
|
408
|
+
declare function jsx(type: string | Component, props?: Props | null, key?: DefussKey): RenderInput;
|
|
409
|
+
/** Same as {@link jsx}; compilers call it for elements with several static children. */
|
|
410
|
+
declare const jsxs: typeof jsx;
|
|
411
|
+
/** Same as {@link jsx}; compilers call it in development mode. The extra source arguments are ignored. */
|
|
412
|
+
declare const jsxDEV: (type: string | Component, props?: Props | null, key?: DefussKey) => RenderInput;
|
|
413
|
+
/**
|
|
414
|
+
* The classic factory, for `jsxFactory: "h"` and for pages without a build step:
|
|
415
|
+
* `h("button", { onClick: save }, "Save")` is `<button onClick={save}>Save</button>`.
|
|
416
|
+
*/
|
|
417
|
+
declare function h(type: string | Component, props?: Props | null, ...children: unknown[]): RenderInput;
|
|
418
|
+
/**
|
|
419
|
+
* JSX types for `jsxImportSource: "defuss-morph"`. Loose on purpose: any tag, any attribute. A JSX expression is a
|
|
420
|
+
* `RenderInput` (a fragment is an array), and a component may return anything morph renders.
|
|
421
|
+
*/
|
|
422
|
+
declare namespace JSX {
|
|
423
|
+
type Element = RenderInput;
|
|
424
|
+
type ElementType = string | Component;
|
|
425
|
+
interface ElementChildrenAttribute {
|
|
426
|
+
children: {};
|
|
427
|
+
}
|
|
428
|
+
interface IntrinsicAttributes {
|
|
429
|
+
key?: DefussKey;
|
|
430
|
+
}
|
|
431
|
+
interface IntrinsicElements {
|
|
432
|
+
[tagName: string]: {
|
|
433
|
+
[attribute: string]: any;
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Why a plain object and not defuss's store-backed Ref: morph only ever assigns `current`, and the forwardRef
|
|
440
|
+
* pattern needs one free slot for a component's API, so `{ current, state }` is all a ref has to be; defuss's richer
|
|
441
|
+
* Ref stays assignable to it (RefLike). VERIFIED: src/reactive.test.ts keeps `current` on the same node across
|
|
442
|
+
* morphs and drives a component through `ref.state`.
|
|
443
|
+
*
|
|
444
|
+
* A reference to a rendered element. morph sets `current` when it creates or patches the element that carries
|
|
445
|
+
* `ref={...}`; a morph that keeps the element keeps `current` pointing at the same node. `state` is free for the
|
|
446
|
+
* component that owns the element: the forwardRef pattern puts the component's API there, for its parent to call.
|
|
447
|
+
*/
|
|
448
|
+
interface Ref<T extends Element = HTMLElement, S = unknown> extends RefLike {
|
|
449
|
+
current: T | null;
|
|
450
|
+
state?: S;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Creates an empty {@link Ref}. `current` stays `null` until morph renders the element that carries it, so read it
|
|
454
|
+
* in an event handler, in `onMount` or after the morph call, not in the component body.
|
|
455
|
+
*/
|
|
456
|
+
declare const createRef: <T extends Element = HTMLElement, S = unknown>(state?: S) => Ref<T, S>;
|
|
457
|
+
/** True for objects with a `current` property, the shape morph assigns refs to. */
|
|
458
|
+
declare const isRef: (value: unknown) => value is RefLike;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Why: re-rendering a region whenever a store changes is the loop most pages need, and morph makes the naive
|
|
462
|
+
* version correct: render the whole region again and morph it in, and every node that stays keeps its focus, typed
|
|
463
|
+
* input and listeners. Three entry points share one core, as in defuss: `reactive()` for a known element or ref,
|
|
464
|
+
* `<Reactive />` for a JSX tree, and defuss-query's `df$(selector).reactive()` for markup already in the page.
|
|
465
|
+
*/
|
|
466
|
+
/**
|
|
467
|
+
* What `reactive()` subscribes to: a defuss-store store (`createStore()`, `computed()`), or anything whose
|
|
468
|
+
* `subscribe(listener)` returns an unsubscribe function or an object with `unsubscribe()`.
|
|
469
|
+
*/
|
|
470
|
+
interface Subscribable {
|
|
471
|
+
subscribe(listener: (...args: any[]) => unknown): unknown;
|
|
472
|
+
}
|
|
473
|
+
/** Configuration of {@link reactive} and {@link Reactive}. */
|
|
474
|
+
interface ReactiveConfig {
|
|
475
|
+
/** One store, or several: a change in any of them re-renders. */
|
|
476
|
+
store: Subscribable | readonly Subscribable[];
|
|
477
|
+
/** Returns the region's next content: JSX/VNodes or an HTML string. Keep it pure; it may run more often than the stores change. */
|
|
478
|
+
render: () => RenderInput;
|
|
479
|
+
/** Runs once, when the region is disposed (the returned function, `signal` or the `<Reactive>` element's unmount). */
|
|
480
|
+
cleanup?: () => void;
|
|
481
|
+
/** Disposes the region when aborted: the way to end a `df$(...).reactive()` binding, which returns the selection. */
|
|
482
|
+
signal?: AbortSignal;
|
|
483
|
+
}
|
|
484
|
+
/** Where {@link reactive} renders: an element, a ref to one, or a list of elements (a defuss-query selection, a NodeList). */
|
|
485
|
+
type ReactiveTarget = Element | RefLike | ArrayLike<Element>;
|
|
486
|
+
/**
|
|
487
|
+
* Renders `config.render()` into `target` now, then again whenever a store changes, by morphing it in: nodes that
|
|
488
|
+
* stay keep their identity, focus, typed input and listeners. The first render runs before the subscriptions start.
|
|
489
|
+
* Returns the dispose function: it unsubscribes from every store and runs `config.cleanup` once.
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```ts
|
|
493
|
+
* import { reactive } from "defuss-morph";
|
|
494
|
+
* import { createStore } from "defuss-store";
|
|
495
|
+
*
|
|
496
|
+
* const cart = createStore({ count: 0 });
|
|
497
|
+
* const dispose = reactive({ store: cart, render: () => `<p>${cart.value.count} items</p>` }, cartEl);
|
|
498
|
+
* cart.set("count", 2); // <p>2 items</p>, the same <p> as before
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
declare function reactive(config: ReactiveConfig, target: ReactiveTarget): () => void;
|
|
502
|
+
/** Props of {@link Reactive}: the {@link ReactiveConfig}, the wrapper's tag and any attribute for the wrapper. */
|
|
503
|
+
interface ReactiveProps extends Omit<ReactiveConfig, "signal"> {
|
|
504
|
+
/** The wrapper element's tag. Default: `"div"`. */
|
|
505
|
+
tag?: string;
|
|
506
|
+
/** Set to the wrapper element. */
|
|
507
|
+
ref?: RefLike;
|
|
508
|
+
[attribute: string]: unknown;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* A JSX component that re-renders its content whenever a store changes. It renders a wrapper element (`tag`,
|
|
512
|
+
* default `div`) whose children are `render()`; other props go to the wrapper. The subscription starts when the
|
|
513
|
+
* wrapper is mounted and ends when it leaves the document, which also runs `cleanup`. A parent re-render passes the
|
|
514
|
+
* latest `render` and `store`, and the children it produces are morphed in like any other render.
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```tsx
|
|
518
|
+
* import { Reactive } from "defuss-morph";
|
|
519
|
+
* import { createStore } from "defuss-store";
|
|
520
|
+
*
|
|
521
|
+
* const counter = createStore({ count: 0 });
|
|
522
|
+
*
|
|
523
|
+
* <Reactive tag="section" store={counter} render={() => (
|
|
524
|
+
* <button onClick={() => counter.set("count", counter.value.count + 1)}>{counter.value.count}</button>
|
|
525
|
+
* )} />
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
declare function Reactive(props: ReactiveProps): VNode;
|
|
529
|
+
/** The `.reactive()` method {@link installReactive} adds to every defuss-query selection. */
|
|
530
|
+
interface ReactiveQueryMethod {
|
|
531
|
+
/** Calls {@link reactive} with this selection as the target and returns the selection; end it with `config.signal`. */
|
|
532
|
+
reactive(config: ReactiveConfig): this;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Adds `.reactive(config)` to defuss-query's selections (`df$.fn`, the shared prototype), so markup already in the
|
|
536
|
+
* page becomes reactive by selector: `df$("#cart").reactive({ store, render })`. Idempotent; returns `df$`. The CDN
|
|
537
|
+
* bundle calls it itself when it loads after defuss-query; with defuss-morph loaded first, call
|
|
538
|
+
* `df$.installReactive(df$)` once defuss-query has loaded.
|
|
539
|
+
*/
|
|
540
|
+
declare const installReactive: <Q>(df$: Q) => Q;
|
|
541
|
+
|
|
542
|
+
export { CAPTURE_ONLY_EVENTS, CLASS_ATTRIBUTE_NAME, COMMENT_TYPE, DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE, DEFAULT_TRANSITION_CONFIG, FROM_DOM_MARKER, Fragment, JSX, REF_ATTRIBUTE_NAME, Reactive, XLINK_ATTRIBUTE_NAME, XMLNS_ATTRIBUTE_NAME, applyStyles, areDomNodesEqual, clearDelegatedEvents, clearDelegatedEventsDeep, createRef, domNodeToVNode, getMimeType, getRegisteredEventKeys, getRegisteredEventTypes, getRenderer, getTransitionStyles, h, handleLifecycleEventsForOnMount, htmlStringToVNodes, installReactive, isCommentVNode, isHTML, isMarkup, isRef, isSVG, jsx, jsxDEV, jsxs, morph, nsMap, observeUnmount, onClearDelegatedEvents, parseDOM, parseEventPropName, performTransition, queueCallback, reactive, registerDelegatedEvent, removeDelegatedEvent, removeDelegatedEventByKey, renderMarkup, replaceDomWithVdom, resolveGlobals, templateContent, updateDomWithVdom };
|
|
543
|
+
export type { CommentVNode, Component, DefussKey, DelegatedEventOptions, DelegatedPhase, DomAbstractionImpl, Globals, MorphMode, MorphOptions, MountHandler, ParsedEventProp, ReactiveConfig, ReactiveProps, ReactiveQueryMethod, ReactiveTarget, Ref, RefLike, RenderInput, Subscribable, TransitionConfig, TransitionStyles, TransitionType, TransitionsEasing, UnmountHandler, VNode, VNodeAttributes, VNodeChild, VNodeChildren, VNodeType, ValidChild };
|