defuss-morph 0.2.0 → 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 -218
- package/dist/all.js +454 -200
- 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 +344 -121
- package/dist/index.d.ts +308 -26
- package/dist/index.mjs +334 -122
- 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,6 +24,10 @@ 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;
|
|
@@ -43,6 +47,7 @@ type VNodeChildren = VNodeChild[];
|
|
|
43
47
|
* Anything the morph engine accepts as new content.
|
|
44
48
|
*/
|
|
45
49
|
type RenderInput = VNode | object | string | number | boolean | null | undefined | RenderInput[];
|
|
50
|
+
/** The renderer contract returned by `getRenderer()`. */
|
|
46
51
|
interface DomAbstractionImpl {
|
|
47
52
|
hasElNamespace(domElement: Element | Document): boolean;
|
|
48
53
|
hasSvgNamespace(parentElement: Element | Document, type: string): boolean;
|
|
@@ -54,30 +59,14 @@ interface DomAbstractionImpl {
|
|
|
54
59
|
setAttributes(virtualNode: VNode<VNodeAttributes>, domElement: Element): void;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
exit: Record<string, string>;
|
|
62
|
-
exitActive: Record<string, string>;
|
|
63
|
-
}
|
|
64
|
-
type TransitionsEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end";
|
|
65
|
-
interface TransitionConfig {
|
|
66
|
-
type?: TransitionType;
|
|
67
|
-
styles?: TransitionStyles;
|
|
68
|
-
duration?: number;
|
|
69
|
-
easing?: TransitionsEasing | string;
|
|
70
|
-
delay?: number;
|
|
71
|
-
target?: "parent" | "self";
|
|
72
|
-
}
|
|
73
|
-
declare const getTransitionStyles: (type: TransitionType, duration: number, easing?: string) => TransitionStyles;
|
|
74
|
-
declare const applyStyles: (el: HTMLElement, styles: Record<string, string | number>) => void;
|
|
75
|
-
declare const DEFAULT_TRANSITION_CONFIG: TransitionConfig;
|
|
76
|
-
declare const performTransition: (element: HTMLElement, updateCallback: () => Promise<void>, config?: TransitionConfig) => Promise<void>;
|
|
77
|
-
|
|
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
|
+
*/
|
|
78
66
|
declare const queueCallback: <T extends any[]>(cb: (...args: T) => void) => (...args: T) => void;
|
|
79
67
|
|
|
80
68
|
type DelegatedPhase = "bubble" | "capture";
|
|
69
|
+
/** Options of {@link registerDelegatedEvent}. */
|
|
81
70
|
interface DelegatedEventOptions {
|
|
82
71
|
capture?: boolean;
|
|
83
72
|
/** If true, allows multiple handlers per element+type (Dequery mode) */
|
|
@@ -93,8 +82,21 @@ interface ParsedEventProp {
|
|
|
93
82
|
* other non-bubbling event reaches only its target's handlers, natively.
|
|
94
83
|
*/
|
|
95
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
|
+
*/
|
|
96
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
|
+
*/
|
|
97
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
|
+
*/
|
|
98
100
|
declare const removeDelegatedEvent: (target: EventTarget, eventType: string, handler?: EventListener, _options?: DelegatedEventOptions) => void;
|
|
99
101
|
/**
|
|
100
102
|
* Why: listeners a facade attaches natively (defuss-query's on()) live
|
|
@@ -104,10 +106,12 @@ declare const removeDelegatedEvent: (target: EventTarget, eventType: string, han
|
|
|
104
106
|
* Registering the same hook twice is a no-op; returns an unregister function.
|
|
105
107
|
*/
|
|
106
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. */
|
|
107
110
|
declare const clearDelegatedEvents: (target: EventTarget) => void;
|
|
108
111
|
/**
|
|
109
|
-
* Clear delegated events for an element and all its descendants.
|
|
110
|
-
*
|
|
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()).
|
|
111
115
|
*/
|
|
112
116
|
declare const clearDelegatedEventsDeep: (root: HTMLElement) => void;
|
|
113
117
|
/**
|
|
@@ -137,9 +141,27 @@ declare const nsMap: {
|
|
|
137
141
|
xlink: string;
|
|
138
142
|
svg: string;
|
|
139
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
|
+
*/
|
|
140
149
|
declare const observeUnmount: (domNode: Node, onUnmount: () => void) => void;
|
|
141
|
-
/**
|
|
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
|
+
*/
|
|
142
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
|
+
*/
|
|
143
165
|
declare const getRenderer: (document: Document) => DomAbstractionImpl;
|
|
144
166
|
|
|
145
167
|
/**
|
|
@@ -209,13 +231,26 @@ declare const COMMENT_TYPE = "#comment";
|
|
|
209
231
|
type CommentVNode = VNode & {
|
|
210
232
|
type: typeof COMMENT_TYPE;
|
|
211
233
|
};
|
|
234
|
+
/** True for a comment VNode (`{ type: "#comment", value }`). */
|
|
212
235
|
declare const isCommentVNode: (value: unknown) => value is CommentVNode;
|
|
236
|
+
/** Parses `input` into a detached document with the given parser. */
|
|
213
237
|
declare function parseDOM(input: string, type: DOMParserSupportedType, Parser: typeof DOMParser): Document;
|
|
238
|
+
/** True when `input` parses as an SVG document (root element `<svg>`). */
|
|
214
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>`. */
|
|
215
241
|
declare function isHTML(input: string, Parser: typeof DOMParser): boolean;
|
|
242
|
+
/** True when `input` contains a tag and parses as HTML or SVG. */
|
|
216
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. */
|
|
217
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`. */
|
|
218
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;
|
|
219
254
|
/**
|
|
220
255
|
* Converts a DOM node to a VNode structure for use with updateDomWithVdom.
|
|
221
256
|
* This allows us to leverage the sophisticated partial update system even for Node inputs.
|
|
@@ -227,6 +262,43 @@ declare function domNodeToVNode(node: Node): VNode<VNodeAttributes> | string;
|
|
|
227
262
|
*/
|
|
228
263
|
declare function htmlStringToVNodes(html: string, Parser: typeof DOMParser): Array<VNode<VNodeAttributes> | string>;
|
|
229
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}. */
|
|
230
302
|
interface MorphOptions {
|
|
231
303
|
/**
|
|
232
304
|
* Optional transition to apply around the morph.
|
|
@@ -255,7 +327,217 @@ interface MorphOptions {
|
|
|
255
327
|
*/
|
|
256
328
|
diff?: boolean;
|
|
257
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
|
+
*/
|
|
258
376
|
declare const morph: (el: Element, newContent: RenderInput, options?: MorphOptions) => void | Promise<void>;
|
|
259
377
|
|
|
260
|
-
|
|
261
|
-
|
|
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 };
|