@vielzeug/ore 26.11.7 → 26.11.9
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/dist/base-element.cjs +1 -1
- package/dist/base-element.cjs.map +1 -1
- package/dist/base-element.d.ts +17 -4
- package/dist/base-element.d.ts.map +1 -1
- package/dist/base-element.js +1 -1
- package/dist/base-element.js.map +1 -1
- package/dist/directives/each.cjs +1 -1
- package/dist/directives/each.cjs.map +1 -1
- package/dist/directives/each.d.ts +5 -0
- package/dist/directives/each.d.ts.map +1 -1
- package/dist/directives/each.js +1 -1
- package/dist/directives/each.js.map +1 -1
- package/dist/directives/unsafe-html.cjs +1 -1
- package/dist/directives/unsafe-html.cjs.map +1 -1
- package/dist/directives/unsafe-html.d.ts.map +1 -1
- package/dist/directives/unsafe-html.js +1 -1
- package/dist/directives/unsafe-html.js.map +1 -1
- package/dist/directives/when.cjs +1 -1
- package/dist/directives/when.cjs.map +1 -1
- package/dist/directives/when.d.ts.map +1 -1
- package/dist/directives/when.js +1 -1
- package/dist/directives/when.js.map +1 -1
- package/dist/ore.cjs +1 -1
- package/dist/ore.cjs.map +1 -1
- package/dist/ore.iife.js +1 -1
- package/dist/ore.iife.js.map +1 -1
- package/dist/ore.js +208 -145
- package/dist/ore.js.map +1 -1
- package/dist/slots.cjs +1 -1
- package/dist/slots.cjs.map +1 -1
- package/dist/slots.d.ts.map +1 -1
- package/dist/slots.js +1 -1
- package/dist/slots.js.map +1 -1
- package/dist/utils/dom.cjs +1 -1
- package/dist/utils/dom.cjs.map +1 -1
- package/dist/utils/dom.d.ts.map +1 -1
- package/dist/utils/dom.js +1 -1
- package/dist/utils/dom.js.map +1 -1
- package/package.json +5 -5
package/dist/base-element.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs"),t=require("./props.cjs"),n=require("./runtime.cjs"),r=require("./utils/css.cjs");let i=require("@vielzeug/ripple");var a={SETUP_DONE:`setup_done`,SETUP_RUNNING:`setup_running`,UNINITIALIZED:`uninitialized`,UNMOUNTED:`unmounted`},o=()=>({formResetCallbacks:[],generation:0,mountCallbacks:[],phase:a.UNINITIALIZED,scope:(0,i.createScope)(),templateResult:null}),s=e=>(typeof e==`object`||typeof e==`function`)&&e!==null&&`then`in e&&typeof e.then==`function`,c=class extends HTMLElement{static _definition;static _normalizedPropDefs;static formAssociated=!1;static observedAttributes=[];_component;constructor(){super();let e=this.constructor._definition;e?.shadow!==!1&&this.attachShadow({mode:`open`,...e?.shadow}),this._component=o()}connectedCallback(){(0,i.untrack)(()=>{this._component.phase===a.UNINITIALIZED&&this._runSetup(),this._init()})}attributeChangedCallback(e,n,r){if(n===r)return;let a=t.getPropMeta(this,e);if(!a)return;let o=a.parse(r);Object.is((0,i.untrack)(()=>a.signal.value),o)||(a.signal.value=o)}disconnectedCallback(){this._component.
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./props.cjs"),n=require("./runtime.cjs"),r=require("./utils/css.cjs");let i=require("@vielzeug/ripple");var a={SETUP_DONE:`setup_done`,SETUP_RUNNING:`setup_running`,UNINITIALIZED:`uninitialized`,UNMOUNTED:`unmounted`},o=()=>({formResetCallbacks:[],generation:0,mountCallbacks:[],phase:a.UNINITIALIZED,scope:(0,i.createScope)(),templateResult:null}),s=e=>(typeof e==`object`||typeof e==`function`)&&e!==null&&`then`in e&&typeof e.then==`function`,c=class extends HTMLElement{static _definition;static _normalizedPropDefs;static formAssociated=!1;static observedAttributes=[];_component;_pendingTeardown=null;_stylesApplied=!1;constructor(){super();let e=this.constructor._definition;e?.shadow!==!1&&this.attachShadow({mode:`open`,...e?.shadow}),this._component=o()}connectedCallback(){(0,i.untrack)(()=>{this._pendingTeardown===null?(this._component.phase===a.UNINITIALIZED&&this._runSetup(),this._init()):(this._pendingTeardown=null,this._component.phase=a.SETUP_DONE,this._scheduleMountCallbacks())})}attributeChangedCallback(e,n,r){if(n===r)return;let a=t.getPropMeta(this,e);if(!a)return;let o=a.parse(r);Object.is((0,i.untrack)(()=>a.signal.value),o)||(a.signal.value=o)}disconnectedCallback(){this._component.phase=a.UNMOUNTED;let e={};this._pendingTeardown=e,queueMicrotask(()=>{this._pendingTeardown===e&&(this._pendingTeardown=null,this._component.generation++,this._resetSetupState())})}_resetSetupState(){this._component.scope.dispose(),this._component.formResetCallbacks=[],this._component.mountCallbacks=[],this._component.phase=a.UNINITIALIZED,this._component.scope=(0,i.createScope)(),this._component.templateResult=null}formResetCallback(){for(let e of this._component.formResetCallbacks)try{e()}catch(e){this._reportLifecycleError(e,`form-reset`)}}_reportLifecycleError(t,n){let r=t instanceof Error?t:Error(String(t)),i=new e.OreLifecycleError(`<${this.localName}> failed during ${this._component.phase} (${n})`,{cause:r,component:this.localName,phase:n});e.reportRuntimeError(i,this)}_runSetup(){this._component.phase=a.SETUP_RUNNING;let r=this.constructor._definition,i=this.constructor._normalizedPropDefs,o=n.createRuntimeContext(this);try{let c;if(this._component.scope.run(()=>{c=n.runWithContext(o,()=>{let e=i?t.createProps(this,i):{};return r.setup(e)})}),this._component.mountCallbacks.push(...o.mountCallbacks),this._component.formResetCallbacks.push(...o.formResetCallbacks),s(c))throw new e.OreApiError(e.ORE_ERRORS.asyncSetupUnsupported);this._component.templateResult=c??null,this._component.phase=a.SETUP_DONE}catch(e){throw this._reportLifecycleError(e,`setup`),this._resetSetupState(),e}}_isStale(e){return this._component.generation!==e||!this.isConnected}_applyResult(e){if(!e)return;let t=this.shadowRoot??this,r=n.createRuntimeContext(this);t.replaceChildren(),this._component.scope.run(()=>{n.runWithContext(r,()=>{e.mount(t,null,n.onCleanup)})})}_init(){this._applyStyles(),this._mountTemplate(),this._component.phase===a.SETUP_DONE&&this._scheduleMountCallbacks()}_applyStyles(){if(this._stylesApplied)return;let e=this.constructor._definition;this.shadowRoot&&e?.styles?.length&&(this.shadowRoot.adoptedStyleSheets=e.styles.map(r.loadStylesheet),this._stylesApplied=!0)}_mountTemplate(){let e=this._component.templateResult;e&&this._applyResult(e)}_scheduleMountCallbacks(){if(this._component.mountCallbacks.length===0)return;let e=this._component.generation,t=n.beginPendingWork();queueMicrotask(()=>{try{if(this._isStale(e))return;let t=this._component.mountCallbacks.splice(0);for(let e=0;e<t.length;e++){let r=t[e];try{let e=n.createRuntimeContext(this);this._component.scope.run(()=>{n.runWithContext(e,()=>{let e=r();typeof e==`function`&&n.onCleanup(e)})}),e.mountCallbacks.length>0&&t.push(...e.mountCallbacks),e.formResetCallbacks.length>0&&this._component.formResetCallbacks.push(...e.formResetCallbacks)}catch(e){this._reportLifecycleError(e,`mounted`)}}}finally{t()}})}};exports.BaseElement=c;
|
|
2
2
|
//# sourceMappingURL=base-element.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-element.cjs","names":["createScope","getPropMeta","untrack","OreLifecycleError","createRuntimeContext","runWithContext","createProps","OreApiError","ORE_ERRORS","onCleanup","loadStylesheet","beginPendingWork","require_errors","require_props","require_runtime","require_css","_vielzeug_ripple","ComponentPhase","createComponentState","isPromiseLike","value","BaseElement","def","oldValue","newValue","propMeta","name","parsed","callback","error","err","oreError","phase","normalizedPropDefs","ctx","setupResult","setupProps","capturedGeneration","result","host","context","endWork","batch","i","nestedCtx","cleanup"],"sources":["../src/base-element.ts"],"sourcesContent":["import { createScope, type Scope, untrack } from '@vielzeug/ripple';\n\nimport type { ComponentDefinition } from './component-types';\n\nimport { ORE_ERRORS, OreApiError, type OreErrorPhase, OreLifecycleError, reportRuntimeError } from './errors';\nimport { createProps, getPropMeta, type InferProps, type PropInputDefs, type PropsDef } from './props';\nimport {\n beginPendingWork,\n createRuntimeContext,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n runWithContext,\n} from './runtime';\nimport type { HTMLResult } from './template/result';\nimport { loadStylesheet } from './utils/css';\n\n// ─── Component phases ─────────────────────────────────────────────────────────\n// Internal to BaseElement: the only state machine in the package.\n\nconst ComponentPhase = {\n SETUP_DONE: 'setup_done',\n SETUP_RUNNING: 'setup_running',\n UNINITIALIZED: 'uninitialized',\n UNMOUNTED: 'unmounted',\n} as const;\n\ntype ComponentPhase = (typeof ComponentPhase)[keyof typeof ComponentPhase];\n\n// ─── Internal component state ─────────────────────────────────────────────────\n\ntype ComponentState = {\n /** Registered via `onFormReset()`: persists across mount callbacks, unlike `mountCallbacks`. */\n formResetCallbacks: OnFormResetCallback[];\n /** Incremented on every disconnect: invalidates queued mount callbacks. */\n generation: number;\n mountCallbacks: OnMountedCallback[];\n phase: ComponentPhase;\n scope: Scope;\n templateResult: HTMLResult | null;\n};\n\nconst createComponentState = (): ComponentState => ({\n formResetCallbacks: [],\n generation: 0,\n mountCallbacks: [],\n phase: ComponentPhase.UNINITIALIZED,\n scope: createScope(),\n templateResult: null,\n});\n\nconst isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function';\n\n// ─── BaseElement ──────────────────────────────────────────────────────────────\n\n/**\n * Phase transitions:\n *\n * ```\n * UNINITIALIZED ──_runSetup()──► SETUP_DONE\n * SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(reset)──► UNINITIALIZED\n * ```\n *\n * `generation` increments on every disconnect. Scheduled mount callbacks capture\n * it so callbacks belonging to a disconnected instance cannot run after a\n * reconnect (see `_isStale`).\n *\n * Why this lives on the class instead of a standalone pure reducer: every\n * transition here is triggered by running actual user code (`def.setup()`,\n * `onMounted` callbacks) inside a reactive `scope.run()` + `runWithContext()`\n *: there is no meaningful \"decide the next phase\" step that can be separated\n * from \"run the side-effecting thing that produces the phase change\" without\n * introducing a data-only effect-description layer that this package has no\n * other use for. That's why the methods below stay as direct, readable\n * procedural steps instead of a reducer + effect interpreter.\n */\nexport class BaseElement extends HTMLElement {\n static _definition: ComponentDefinition;\n static _normalizedPropDefs: PropsDef<Record<never, never>> | undefined;\n static formAssociated = false;\n static observedAttributes: string[] = [];\n\n private _component: ComponentState;\n\n constructor() {\n super();\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (def?.shadow !== false) {\n this.attachShadow({ mode: 'open', ...(def?.shadow as Partial<ShadowRootInit> | undefined) });\n }\n\n this._component = createComponentState();\n }\n\n connectedCallback(): void {\n untrack(() => {\n if (this._component.phase === ComponentPhase.UNINITIALIZED) this._runSetup();\n\n this._init();\n });\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n\n const propMeta = getPropMeta(this, name);\n\n if (!propMeta) return;\n\n const parsed = propMeta.parse(newValue);\n\n if (\n !Object.is(\n untrack(() => propMeta.signal.value),\n parsed,\n )\n )\n propMeta.signal.value = parsed as never;\n }\n\n disconnectedCallback(): void {\n this._component.generation++;\n this._component.phase = ComponentPhase.UNMOUNTED;\n this._resetSetupState();\n }\n\n /** Dispose one connection's resources before its state can be rebuilt. */\n private _resetSetupState(): void {\n this._component.scope.dispose();\n // Reset mutable fields for next connection, keeping the same object for stable references.\n this._component.formResetCallbacks = [];\n this._component.mountCallbacks = [];\n this._component.phase = ComponentPhase.UNINITIALIZED;\n this._component.scope = createScope();\n this._component.templateResult = null;\n }\n\n /**\n * Native form-association lifecycle callback: the browser calls this on every\n * `formAssociated: true` element inside a `<form>` when that form is reset.\n * Runs every `onFormReset()` callback registered during `setup()`.\n */\n formResetCallback(): void {\n for (const callback of this._component.formResetCallbacks) {\n try {\n callback();\n } catch (error) {\n this._reportLifecycleError(error, 'form-reset');\n }\n }\n }\n\n private _reportLifecycleError(error: unknown, phase: OreErrorPhase): void {\n const err = error instanceof Error ? error : new Error(String(error));\n const oreError = new OreLifecycleError(`<${this.localName}> failed during ${this._component.phase} (${phase})`, {\n cause: err,\n component: this.localName,\n phase,\n });\n\n reportRuntimeError(oreError, this);\n }\n\n private _runSetup(): void {\n this._component.phase = ComponentPhase.SETUP_RUNNING;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n const normalizedPropDefs = (this.constructor as typeof BaseElement)._normalizedPropDefs;\n const ctx = createRuntimeContext(this);\n\n try {\n let setupResult: HTMLResult | null | undefined;\n\n this._component.scope.run(() => {\n setupResult = runWithContext(ctx, () => {\n const setupProps = normalizedPropDefs\n ? createProps(this, normalizedPropDefs)\n : ({} as InferProps<PropInputDefs>);\n\n return def.setup(setupProps as InferProps<PropInputDefs>);\n });\n });\n this._component.mountCallbacks.push(...ctx.mountCallbacks);\n this._component.formResetCallbacks.push(...ctx.formResetCallbacks);\n\n if (isPromiseLike(setupResult)) throw new OreApiError(ORE_ERRORS.asyncSetupUnsupported);\n\n this._component.templateResult = setupResult ?? null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n } catch (error) {\n this._reportLifecycleError(error, 'setup');\n // Setup is atomic: a failed run must not leave partial effects or cleanups\n // live until a later disconnect.\n this._resetSetupState();\n throw error;\n }\n }\n\n private _isStale(capturedGeneration: number): boolean {\n return this._component.generation !== capturedGeneration || !this.isConnected;\n }\n\n private _applyResult(result: HTMLResult | null): void {\n if (!result) return;\n\n const host: Element | ShadowRoot = this.shadowRoot ?? this;\n\n // Mounting can register component cleanup, so preserve a runtime context here too.\n const context = createRuntimeContext(this);\n\n host.replaceChildren();\n this._component.scope.run(() => {\n runWithContext(context, () => {\n result.mount(host, null, onCleanup);\n });\n });\n }\n\n private _init(): void {\n this._applyStyles();\n this._mountTemplate();\n\n // Setup completes before the template mounts, so callbacks always observe live DOM.\n if (this._component.phase === ComponentPhase.SETUP_DONE) this._scheduleMountCallbacks();\n }\n\n private _applyStyles(): void {\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (this.shadowRoot && def?.styles?.length) {\n this.shadowRoot.adoptedStyleSheets = def.styles.map(loadStylesheet);\n }\n }\n\n private _mountTemplate(): void {\n const result = this._component.templateResult;\n\n if (!result) return;\n\n this._applyResult(result);\n }\n\n private _scheduleMountCallbacks(): void {\n if (this._component.mountCallbacks.length === 0) return;\n\n const capturedGeneration = this._component.generation;\n // Tracked as pending work for the duration of this microtask: ended in a `finally`\n // so a thrown callback (already caught per-callback below, but defensive here too)\n // never leaves the counter stuck above zero. See runtime.ts's beginPendingWork().\n const endWork = beginPendingWork();\n\n queueMicrotask(() => {\n try {\n if (this._isStale(capturedGeneration)) return;\n\n // Snapshot callbacks so in-loop registrations don't extend this iteration.\n // Nested onMounted registrations are appended to `batch` and run in the same\n // microtask: no recursive scheduling. Index-based loop because the array\n // grows as nested callbacks are discovered.\n const batch = this._component.mountCallbacks.splice(0);\n\n for (let i = 0; i < batch.length; i++) {\n const callback = batch[i];\n\n try {\n const nestedCtx = createRuntimeContext(this);\n\n this._component.scope.run(() => {\n runWithContext(nestedCtx, () => {\n const cleanup = callback();\n\n if (typeof cleanup === 'function') onCleanup(cleanup);\n });\n });\n\n if (nestedCtx.mountCallbacks.length > 0) {\n batch.push(...nestedCtx.mountCallbacks);\n }\n\n if (nestedCtx.formResetCallbacks.length > 0) {\n this._component.formResetCallbacks.push(...nestedCtx.formResetCallbacks);\n }\n } catch (error) {\n this._reportLifecycleError(error, 'mounted');\n }\n }\n } finally {\n endWork();\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"base-element.cjs","names":["createScope","getPropMeta","untrack","OreLifecycleError","createRuntimeContext","runWithContext","createProps","OreApiError","ORE_ERRORS","onCleanup","loadStylesheet","beginPendingWork","require_errors","require_props","require_runtime","require_css","_vielzeug_ripple","ComponentPhase","createComponentState","isPromiseLike","value","BaseElement","def","oldValue","newValue","propMeta","name","parsed","token","callback","error","err","oreError","phase","normalizedPropDefs","ctx","setupResult","setupProps","capturedGeneration","result","host","context","endWork","batch","i","nestedCtx","cleanup"],"sources":["../src/base-element.ts"],"sourcesContent":["import { createScope, type Scope, untrack } from '@vielzeug/ripple';\n\nimport type { ComponentDefinition } from './component-types';\n\nimport { ORE_ERRORS, OreApiError, type OreErrorPhase, OreLifecycleError, reportRuntimeError } from './errors';\nimport { createProps, getPropMeta, type InferProps, type PropInputDefs, type PropsDef } from './props';\nimport {\n beginPendingWork,\n createRuntimeContext,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n runWithContext,\n} from './runtime';\nimport type { HTMLResult } from './template/result';\nimport { loadStylesheet } from './utils/css';\n\n// ─── Component phases ─────────────────────────────────────────────────────────\n// Internal to BaseElement: the only state machine in the package.\n\nconst ComponentPhase = {\n SETUP_DONE: 'setup_done',\n SETUP_RUNNING: 'setup_running',\n UNINITIALIZED: 'uninitialized',\n UNMOUNTED: 'unmounted',\n} as const;\n\ntype ComponentPhase = (typeof ComponentPhase)[keyof typeof ComponentPhase];\n\n// ─── Internal component state ─────────────────────────────────────────────────\n\ntype ComponentState = {\n /** Registered via `onFormReset()`: persists across mount callbacks, unlike `mountCallbacks`. */\n formResetCallbacks: OnFormResetCallback[];\n /** Incremented on every disconnect: invalidates queued mount callbacks. */\n generation: number;\n mountCallbacks: OnMountedCallback[];\n phase: ComponentPhase;\n scope: Scope;\n templateResult: HTMLResult | null;\n};\n\nconst createComponentState = (): ComponentState => ({\n formResetCallbacks: [],\n generation: 0,\n mountCallbacks: [],\n phase: ComponentPhase.UNINITIALIZED,\n scope: createScope(),\n templateResult: null,\n});\n\nconst isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function';\n\n// ─── BaseElement ──────────────────────────────────────────────────────────────\n\n/**\n * Phase transitions:\n *\n * ```\n * UNINITIALIZED ──_runSetup()──► SETUP_DONE\n * SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(teardown microtask)──► UNINITIALIZED\n * UNMOUNTED ──connectedCallback() before the teardown runs──► SETUP_DONE (same-turn move)\n * ```\n *\n * `generation` increments on every completed teardown. Scheduled mount callbacks\n * capture it so callbacks belonging to a torn-down connection cannot run after a\n * later connection (see `_isStale`; its `isConnected` check covers the window\n * while a teardown is still pending).\n *\n * Why this lives on the class instead of a standalone pure reducer: every\n * transition here is triggered by running actual user code (`def.setup()`,\n * `onMounted` callbacks) inside a reactive `scope.run()` + `runWithContext()`\n *: there is no meaningful \"decide the next phase\" step that can be separated\n * from \"run the side-effecting thing that produces the phase change\" without\n * introducing a data-only effect-description layer that this package has no\n * other use for. That's why the methods below stay as direct, readable\n * procedural steps instead of a reducer + effect interpreter.\n */\nexport class BaseElement extends HTMLElement {\n static _definition: ComponentDefinition;\n static _normalizedPropDefs: PropsDef<Record<never, never>> | undefined;\n static formAssociated = false;\n static observedAttributes: string[] = [];\n\n private _component: ComponentState;\n /** Cancellation token for the teardown microtask; `null` when none is pending. */\n private _pendingTeardown: object | null = null;\n private _stylesApplied = false;\n\n constructor() {\n super();\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (def?.shadow !== false) {\n this.attachShadow({ mode: 'open', ...(def?.shadow as Partial<ShadowRootInit> | undefined) });\n }\n\n this._component = createComponentState();\n }\n\n connectedCallback(): void {\n untrack(() => {\n // Move detection: `disconnectedCallback()` deferred its teardown to the\n // next microtask and nothing ran it yet, so this connect is the second\n // half of a same-turn node move (reparent, `insertBefore` reordering).\n // The scope, mounted template, and every piece of component state are\n // still intact, and the shadow root kept its style sheets and DOM while\n // detached: re-running setup or re-mounting the template would destroy\n // and rebuild all of it. Skip straight back to the mounted phase.\n if (this._pendingTeardown !== null) {\n this._pendingTeardown = null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n // Mount callbacks scheduled before the move were skipped as stale while\n // detached (the microtask checks `isConnected`); run them now.\n this._scheduleMountCallbacks();\n\n return;\n }\n\n if (this._component.phase === ComponentPhase.UNINITIALIZED) this._runSetup();\n\n this._init();\n });\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n\n const propMeta = getPropMeta(this, name);\n\n if (!propMeta) return;\n\n const parsed = propMeta.parse(newValue);\n\n if (\n !Object.is(\n untrack(() => propMeta.signal.value),\n parsed,\n )\n )\n propMeta.signal.value = parsed as never;\n }\n\n /**\n * Teardown is deferred one microtask so a same-turn node *move* is not a\n * lifecycle boundary: `connectedCallback()` cancels the pending teardown and\n * the component keeps its scope, state, and mounted template (see\n * `connectedCallback`). A real disconnect disposes everything one microtask\n * later; a reconnection after that point is a full remount, exactly as a\n * late re-add always has been.\n */\n disconnectedCallback(): void {\n this._component.phase = ComponentPhase.UNMOUNTED;\n\n const token = {};\n\n this._pendingTeardown = token;\n\n queueMicrotask(() => {\n if (this._pendingTeardown !== token) return;\n\n this._pendingTeardown = null;\n this._component.generation++;\n this._resetSetupState();\n });\n }\n\n /** Dispose one connection's resources before its state can be rebuilt. */\n private _resetSetupState(): void {\n this._component.scope.dispose();\n // Reset mutable fields for next connection, keeping the same object for stable references.\n this._component.formResetCallbacks = [];\n this._component.mountCallbacks = [];\n this._component.phase = ComponentPhase.UNINITIALIZED;\n this._component.scope = createScope();\n this._component.templateResult = null;\n }\n\n /**\n * Native form-association lifecycle callback: the browser calls this on every\n * `formAssociated: true` element inside a `<form>` when that form is reset.\n * Runs every `onFormReset()` callback registered during `setup()`.\n */\n formResetCallback(): void {\n for (const callback of this._component.formResetCallbacks) {\n try {\n callback();\n } catch (error) {\n this._reportLifecycleError(error, 'form-reset');\n }\n }\n }\n\n private _reportLifecycleError(error: unknown, phase: OreErrorPhase): void {\n const err = error instanceof Error ? error : new Error(String(error));\n const oreError = new OreLifecycleError(`<${this.localName}> failed during ${this._component.phase} (${phase})`, {\n cause: err,\n component: this.localName,\n phase,\n });\n\n reportRuntimeError(oreError, this);\n }\n\n private _runSetup(): void {\n this._component.phase = ComponentPhase.SETUP_RUNNING;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n const normalizedPropDefs = (this.constructor as typeof BaseElement)._normalizedPropDefs;\n const ctx = createRuntimeContext(this);\n\n try {\n let setupResult: HTMLResult | null | undefined;\n\n this._component.scope.run(() => {\n setupResult = runWithContext(ctx, () => {\n const setupProps = normalizedPropDefs\n ? createProps(this, normalizedPropDefs)\n : ({} as InferProps<PropInputDefs>);\n\n return def.setup(setupProps as InferProps<PropInputDefs>);\n });\n });\n this._component.mountCallbacks.push(...ctx.mountCallbacks);\n this._component.formResetCallbacks.push(...ctx.formResetCallbacks);\n\n if (isPromiseLike(setupResult)) throw new OreApiError(ORE_ERRORS.asyncSetupUnsupported);\n\n this._component.templateResult = setupResult ?? null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n } catch (error) {\n this._reportLifecycleError(error, 'setup');\n // Setup is atomic: a failed run must not leave partial effects or cleanups\n // live until a later disconnect.\n this._resetSetupState();\n throw error;\n }\n }\n\n private _isStale(capturedGeneration: number): boolean {\n return this._component.generation !== capturedGeneration || !this.isConnected;\n }\n\n private _applyResult(result: HTMLResult | null): void {\n if (!result) return;\n\n const host: Element | ShadowRoot = this.shadowRoot ?? this;\n\n // Mounting can register component cleanup, so preserve a runtime context here too.\n const context = createRuntimeContext(this);\n\n host.replaceChildren();\n this._component.scope.run(() => {\n runWithContext(context, () => {\n result.mount(host, null, onCleanup);\n });\n });\n }\n\n private _init(): void {\n this._applyStyles();\n this._mountTemplate();\n\n // Setup completes before the template mounts, so callbacks always observe live DOM.\n if (this._component.phase === ComponentPhase.SETUP_DONE) this._scheduleMountCallbacks();\n }\n\n private _applyStyles(): void {\n // `adoptedStyleSheets` lives on the shadow root, which outlives every\n // disconnect/reconnect: assign once per instance, never re-assign churn.\n if (this._stylesApplied) return;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (this.shadowRoot && def?.styles?.length) {\n this.shadowRoot.adoptedStyleSheets = def.styles.map(loadStylesheet);\n this._stylesApplied = true;\n }\n }\n\n private _mountTemplate(): void {\n const result = this._component.templateResult;\n\n if (!result) return;\n\n this._applyResult(result);\n }\n\n private _scheduleMountCallbacks(): void {\n if (this._component.mountCallbacks.length === 0) return;\n\n const capturedGeneration = this._component.generation;\n // Tracked as pending work for the duration of this microtask: ended in a `finally`\n // so a thrown callback (already caught per-callback below, but defensive here too)\n // never leaves the counter stuck above zero. See runtime.ts's beginPendingWork().\n const endWork = beginPendingWork();\n\n queueMicrotask(() => {\n try {\n if (this._isStale(capturedGeneration)) return;\n\n // Snapshot callbacks so in-loop registrations don't extend this iteration.\n // Nested onMounted registrations are appended to `batch` and run in the same\n // microtask: no recursive scheduling. Index-based loop because the array\n // grows as nested callbacks are discovered.\n const batch = this._component.mountCallbacks.splice(0);\n\n for (let i = 0; i < batch.length; i++) {\n const callback = batch[i];\n\n try {\n const nestedCtx = createRuntimeContext(this);\n\n this._component.scope.run(() => {\n runWithContext(nestedCtx, () => {\n const cleanup = callback();\n\n if (typeof cleanup === 'function') onCleanup(cleanup);\n });\n });\n\n if (nestedCtx.mountCallbacks.length > 0) {\n batch.push(...nestedCtx.mountCallbacks);\n }\n\n if (nestedCtx.formResetCallbacks.length > 0) {\n this._component.formResetCallbacks.push(...nestedCtx.formResetCallbacks);\n }\n } catch (error) {\n this._reportLifecycleError(error, 'mounted');\n }\n }\n } finally {\n endWork();\n }\n });\n }\n}\n"],"mappings":"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"}
|
package/dist/base-element.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { type PropsDef } from './props';
|
|
|
5
5
|
*
|
|
6
6
|
* ```
|
|
7
7
|
* UNINITIALIZED ──_runSetup()──► SETUP_DONE
|
|
8
|
-
* SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(
|
|
8
|
+
* SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(teardown microtask)──► UNINITIALIZED
|
|
9
|
+
* UNMOUNTED ──connectedCallback() before the teardown runs──► SETUP_DONE (same-turn move)
|
|
9
10
|
* ```
|
|
10
11
|
*
|
|
11
|
-
* `generation` increments on every
|
|
12
|
-
* it so callbacks belonging to a
|
|
13
|
-
*
|
|
12
|
+
* `generation` increments on every completed teardown. Scheduled mount callbacks
|
|
13
|
+
* capture it so callbacks belonging to a torn-down connection cannot run after a
|
|
14
|
+
* later connection (see `_isStale`; its `isConnected` check covers the window
|
|
15
|
+
* while a teardown is still pending).
|
|
14
16
|
*
|
|
15
17
|
* Why this lives on the class instead of a standalone pure reducer: every
|
|
16
18
|
* transition here is triggered by running actual user code (`def.setup()`,
|
|
@@ -27,9 +29,20 @@ export declare class BaseElement extends HTMLElement {
|
|
|
27
29
|
static formAssociated: boolean;
|
|
28
30
|
static observedAttributes: string[];
|
|
29
31
|
private _component;
|
|
32
|
+
/** Cancellation token for the teardown microtask; `null` when none is pending. */
|
|
33
|
+
private _pendingTeardown;
|
|
34
|
+
private _stylesApplied;
|
|
30
35
|
constructor();
|
|
31
36
|
connectedCallback(): void;
|
|
32
37
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
38
|
+
/**
|
|
39
|
+
* Teardown is deferred one microtask so a same-turn node *move* is not a
|
|
40
|
+
* lifecycle boundary: `connectedCallback()` cancels the pending teardown and
|
|
41
|
+
* the component keeps its scope, state, and mounted template (see
|
|
42
|
+
* `connectedCallback`). A real disconnect disposes everything one microtask
|
|
43
|
+
* later; a reconnection after that point is a full remount, exactly as a
|
|
44
|
+
* late re-add always has been.
|
|
45
|
+
*/
|
|
33
46
|
disconnectedCallback(): void;
|
|
34
47
|
/** Dispose one connection's resources before its state can be rebuilt. */
|
|
35
48
|
private _resetSetupState;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-element.d.ts","sourceRoot":"","sources":["../src/base-element.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAiE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAsDvG
|
|
1
|
+
{"version":3,"file":"base-element.d.ts","sourceRoot":"","sources":["../src/base-element.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAiE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAsDvG;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,WAAY,SAAQ,WAAW;IAC1C,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC;IACxC,MAAM,CAAC,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC;IACvE,MAAM,CAAC,cAAc,UAAS;IAC9B,MAAM,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAM;IAEzC,OAAO,CAAC,UAAU,CAAiB;IACnC,kFAAkF;IAClF,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,cAAc,CAAS;;IAc/B,iBAAiB,IAAI,IAAI;IAyBzB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAkB9F;;;;;;;OAOG;IACH,oBAAoB,IAAI,IAAI;IAgB5B,0EAA0E;IAC1E,OAAO,CAAC,gBAAgB;IAUxB;;;;OAIG;IACH,iBAAiB,IAAI,IAAI;IAUzB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,SAAS;IAmCjB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,uBAAuB;CAiDhC"}
|
package/dist/base-element.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ORE_ERRORS as e,OreApiError as t,OreLifecycleError as n,reportRuntimeError as r}from"./errors.js";import{createProps as i,getPropMeta as a}from"./props.js";import{beginPendingWork as o,createRuntimeContext as s,onCleanup as c,runWithContext as l}from"./runtime.js";import{loadStylesheet as u}from"./utils/css.js";import{createScope as d,untrack as f}from"@vielzeug/ripple";var p={SETUP_DONE:`setup_done`,SETUP_RUNNING:`setup_running`,UNINITIALIZED:`uninitialized`,UNMOUNTED:`unmounted`},m=()=>({formResetCallbacks:[],generation:0,mountCallbacks:[],phase:p.UNINITIALIZED,scope:d(),templateResult:null}),h=e=>(typeof e==`object`||typeof e==`function`)&&e!==null&&`then`in e&&typeof e.then==`function`,g=class extends HTMLElement{static _definition;static _normalizedPropDefs;static formAssociated=!1;static observedAttributes=[];_component;constructor(){super();let e=this.constructor._definition;e?.shadow!==!1&&this.attachShadow({mode:`open`,...e?.shadow}),this._component=m()}connectedCallback(){f(()=>{this._component.phase===p.UNINITIALIZED&&this._runSetup(),this._init()})}attributeChangedCallback(e,t,n){if(t===n)return;let r=a(this,e);if(!r)return;let i=r.parse(n);Object.is(f(()=>r.signal.value),i)||(r.signal.value=i)}disconnectedCallback(){this._component.
|
|
1
|
+
import{ORE_ERRORS as e,OreApiError as t,OreLifecycleError as n,reportRuntimeError as r}from"./errors.js";import{createProps as i,getPropMeta as a}from"./props.js";import{beginPendingWork as o,createRuntimeContext as s,onCleanup as c,runWithContext as l}from"./runtime.js";import{loadStylesheet as u}from"./utils/css.js";import{createScope as d,untrack as f}from"@vielzeug/ripple";var p={SETUP_DONE:`setup_done`,SETUP_RUNNING:`setup_running`,UNINITIALIZED:`uninitialized`,UNMOUNTED:`unmounted`},m=()=>({formResetCallbacks:[],generation:0,mountCallbacks:[],phase:p.UNINITIALIZED,scope:d(),templateResult:null}),h=e=>(typeof e==`object`||typeof e==`function`)&&e!==null&&`then`in e&&typeof e.then==`function`,g=class extends HTMLElement{static _definition;static _normalizedPropDefs;static formAssociated=!1;static observedAttributes=[];_component;_pendingTeardown=null;_stylesApplied=!1;constructor(){super();let e=this.constructor._definition;e?.shadow!==!1&&this.attachShadow({mode:`open`,...e?.shadow}),this._component=m()}connectedCallback(){f(()=>{this._pendingTeardown===null?(this._component.phase===p.UNINITIALIZED&&this._runSetup(),this._init()):(this._pendingTeardown=null,this._component.phase=p.SETUP_DONE,this._scheduleMountCallbacks())})}attributeChangedCallback(e,t,n){if(t===n)return;let r=a(this,e);if(!r)return;let i=r.parse(n);Object.is(f(()=>r.signal.value),i)||(r.signal.value=i)}disconnectedCallback(){this._component.phase=p.UNMOUNTED;let e={};this._pendingTeardown=e,queueMicrotask(()=>{this._pendingTeardown===e&&(this._pendingTeardown=null,this._component.generation++,this._resetSetupState())})}_resetSetupState(){this._component.scope.dispose(),this._component.formResetCallbacks=[],this._component.mountCallbacks=[],this._component.phase=p.UNINITIALIZED,this._component.scope=d(),this._component.templateResult=null}formResetCallback(){for(let e of this._component.formResetCallbacks)try{e()}catch(e){this._reportLifecycleError(e,`form-reset`)}}_reportLifecycleError(e,t){let i=e instanceof Error?e:Error(String(e)),a=new n(`<${this.localName}> failed during ${this._component.phase} (${t})`,{cause:i,component:this.localName,phase:t});r(a,this)}_runSetup(){this._component.phase=p.SETUP_RUNNING;let n=this.constructor._definition,r=this.constructor._normalizedPropDefs,a=s(this);try{let o;if(this._component.scope.run(()=>{o=l(a,()=>{let e=r?i(this,r):{};return n.setup(e)})}),this._component.mountCallbacks.push(...a.mountCallbacks),this._component.formResetCallbacks.push(...a.formResetCallbacks),h(o))throw new t(e.asyncSetupUnsupported);this._component.templateResult=o??null,this._component.phase=p.SETUP_DONE}catch(e){throw this._reportLifecycleError(e,`setup`),this._resetSetupState(),e}}_isStale(e){return this._component.generation!==e||!this.isConnected}_applyResult(e){if(!e)return;let t=this.shadowRoot??this,n=s(this);t.replaceChildren(),this._component.scope.run(()=>{l(n,()=>{e.mount(t,null,c)})})}_init(){this._applyStyles(),this._mountTemplate(),this._component.phase===p.SETUP_DONE&&this._scheduleMountCallbacks()}_applyStyles(){if(this._stylesApplied)return;let e=this.constructor._definition;this.shadowRoot&&e?.styles?.length&&(this.shadowRoot.adoptedStyleSheets=e.styles.map(u),this._stylesApplied=!0)}_mountTemplate(){let e=this._component.templateResult;e&&this._applyResult(e)}_scheduleMountCallbacks(){if(this._component.mountCallbacks.length===0)return;let e=this._component.generation,t=o();queueMicrotask(()=>{try{if(this._isStale(e))return;let t=this._component.mountCallbacks.splice(0);for(let e=0;e<t.length;e++){let n=t[e];try{let e=s(this);this._component.scope.run(()=>{l(e,()=>{let e=n();typeof e==`function`&&c(e)})}),e.mountCallbacks.length>0&&t.push(...e.mountCallbacks),e.formResetCallbacks.length>0&&this._component.formResetCallbacks.push(...e.formResetCallbacks)}catch(e){this._reportLifecycleError(e,`mounted`)}}}finally{t()}})}};export{g as BaseElement};
|
|
2
2
|
//# sourceMappingURL=base-element.js.map
|
package/dist/base-element.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-element.js","names":["ComponentPhase","createComponentState","createScope","isPromiseLike","value","BaseElement","def","untrack","oldValue","newValue","propMeta","getPropMeta","name","parsed","callback","error","err","oreError","OreLifecycleError","phase","reportRuntimeError","normalizedPropDefs","ctx","createRuntimeContext","setupResult","runWithContext","setupProps","createProps","OreApiError","ORE_ERRORS","capturedGeneration","result","host","context","onCleanup","loadStylesheet","endWork","beginPendingWork","batch","i","nestedCtx","cleanup"],"sources":["../src/base-element.ts"],"sourcesContent":["import { createScope, type Scope, untrack } from '@vielzeug/ripple';\n\nimport type { ComponentDefinition } from './component-types';\n\nimport { ORE_ERRORS, OreApiError, type OreErrorPhase, OreLifecycleError, reportRuntimeError } from './errors';\nimport { createProps, getPropMeta, type InferProps, type PropInputDefs, type PropsDef } from './props';\nimport {\n beginPendingWork,\n createRuntimeContext,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n runWithContext,\n} from './runtime';\nimport type { HTMLResult } from './template/result';\nimport { loadStylesheet } from './utils/css';\n\n// ─── Component phases ─────────────────────────────────────────────────────────\n// Internal to BaseElement: the only state machine in the package.\n\nconst ComponentPhase = {\n SETUP_DONE: 'setup_done',\n SETUP_RUNNING: 'setup_running',\n UNINITIALIZED: 'uninitialized',\n UNMOUNTED: 'unmounted',\n} as const;\n\ntype ComponentPhase = (typeof ComponentPhase)[keyof typeof ComponentPhase];\n\n// ─── Internal component state ─────────────────────────────────────────────────\n\ntype ComponentState = {\n /** Registered via `onFormReset()`: persists across mount callbacks, unlike `mountCallbacks`. */\n formResetCallbacks: OnFormResetCallback[];\n /** Incremented on every disconnect: invalidates queued mount callbacks. */\n generation: number;\n mountCallbacks: OnMountedCallback[];\n phase: ComponentPhase;\n scope: Scope;\n templateResult: HTMLResult | null;\n};\n\nconst createComponentState = (): ComponentState => ({\n formResetCallbacks: [],\n generation: 0,\n mountCallbacks: [],\n phase: ComponentPhase.UNINITIALIZED,\n scope: createScope(),\n templateResult: null,\n});\n\nconst isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function';\n\n// ─── BaseElement ──────────────────────────────────────────────────────────────\n\n/**\n * Phase transitions:\n *\n * ```\n * UNINITIALIZED ──_runSetup()──► SETUP_DONE\n * SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(reset)──► UNINITIALIZED\n * ```\n *\n * `generation` increments on every disconnect. Scheduled mount callbacks capture\n * it so callbacks belonging to a disconnected instance cannot run after a\n * reconnect (see `_isStale`).\n *\n * Why this lives on the class instead of a standalone pure reducer: every\n * transition here is triggered by running actual user code (`def.setup()`,\n * `onMounted` callbacks) inside a reactive `scope.run()` + `runWithContext()`\n *: there is no meaningful \"decide the next phase\" step that can be separated\n * from \"run the side-effecting thing that produces the phase change\" without\n * introducing a data-only effect-description layer that this package has no\n * other use for. That's why the methods below stay as direct, readable\n * procedural steps instead of a reducer + effect interpreter.\n */\nexport class BaseElement extends HTMLElement {\n static _definition: ComponentDefinition;\n static _normalizedPropDefs: PropsDef<Record<never, never>> | undefined;\n static formAssociated = false;\n static observedAttributes: string[] = [];\n\n private _component: ComponentState;\n\n constructor() {\n super();\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (def?.shadow !== false) {\n this.attachShadow({ mode: 'open', ...(def?.shadow as Partial<ShadowRootInit> | undefined) });\n }\n\n this._component = createComponentState();\n }\n\n connectedCallback(): void {\n untrack(() => {\n if (this._component.phase === ComponentPhase.UNINITIALIZED) this._runSetup();\n\n this._init();\n });\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n\n const propMeta = getPropMeta(this, name);\n\n if (!propMeta) return;\n\n const parsed = propMeta.parse(newValue);\n\n if (\n !Object.is(\n untrack(() => propMeta.signal.value),\n parsed,\n )\n )\n propMeta.signal.value = parsed as never;\n }\n\n disconnectedCallback(): void {\n this._component.generation++;\n this._component.phase = ComponentPhase.UNMOUNTED;\n this._resetSetupState();\n }\n\n /** Dispose one connection's resources before its state can be rebuilt. */\n private _resetSetupState(): void {\n this._component.scope.dispose();\n // Reset mutable fields for next connection, keeping the same object for stable references.\n this._component.formResetCallbacks = [];\n this._component.mountCallbacks = [];\n this._component.phase = ComponentPhase.UNINITIALIZED;\n this._component.scope = createScope();\n this._component.templateResult = null;\n }\n\n /**\n * Native form-association lifecycle callback: the browser calls this on every\n * `formAssociated: true` element inside a `<form>` when that form is reset.\n * Runs every `onFormReset()` callback registered during `setup()`.\n */\n formResetCallback(): void {\n for (const callback of this._component.formResetCallbacks) {\n try {\n callback();\n } catch (error) {\n this._reportLifecycleError(error, 'form-reset');\n }\n }\n }\n\n private _reportLifecycleError(error: unknown, phase: OreErrorPhase): void {\n const err = error instanceof Error ? error : new Error(String(error));\n const oreError = new OreLifecycleError(`<${this.localName}> failed during ${this._component.phase} (${phase})`, {\n cause: err,\n component: this.localName,\n phase,\n });\n\n reportRuntimeError(oreError, this);\n }\n\n private _runSetup(): void {\n this._component.phase = ComponentPhase.SETUP_RUNNING;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n const normalizedPropDefs = (this.constructor as typeof BaseElement)._normalizedPropDefs;\n const ctx = createRuntimeContext(this);\n\n try {\n let setupResult: HTMLResult | null | undefined;\n\n this._component.scope.run(() => {\n setupResult = runWithContext(ctx, () => {\n const setupProps = normalizedPropDefs\n ? createProps(this, normalizedPropDefs)\n : ({} as InferProps<PropInputDefs>);\n\n return def.setup(setupProps as InferProps<PropInputDefs>);\n });\n });\n this._component.mountCallbacks.push(...ctx.mountCallbacks);\n this._component.formResetCallbacks.push(...ctx.formResetCallbacks);\n\n if (isPromiseLike(setupResult)) throw new OreApiError(ORE_ERRORS.asyncSetupUnsupported);\n\n this._component.templateResult = setupResult ?? null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n } catch (error) {\n this._reportLifecycleError(error, 'setup');\n // Setup is atomic: a failed run must not leave partial effects or cleanups\n // live until a later disconnect.\n this._resetSetupState();\n throw error;\n }\n }\n\n private _isStale(capturedGeneration: number): boolean {\n return this._component.generation !== capturedGeneration || !this.isConnected;\n }\n\n private _applyResult(result: HTMLResult | null): void {\n if (!result) return;\n\n const host: Element | ShadowRoot = this.shadowRoot ?? this;\n\n // Mounting can register component cleanup, so preserve a runtime context here too.\n const context = createRuntimeContext(this);\n\n host.replaceChildren();\n this._component.scope.run(() => {\n runWithContext(context, () => {\n result.mount(host, null, onCleanup);\n });\n });\n }\n\n private _init(): void {\n this._applyStyles();\n this._mountTemplate();\n\n // Setup completes before the template mounts, so callbacks always observe live DOM.\n if (this._component.phase === ComponentPhase.SETUP_DONE) this._scheduleMountCallbacks();\n }\n\n private _applyStyles(): void {\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (this.shadowRoot && def?.styles?.length) {\n this.shadowRoot.adoptedStyleSheets = def.styles.map(loadStylesheet);\n }\n }\n\n private _mountTemplate(): void {\n const result = this._component.templateResult;\n\n if (!result) return;\n\n this._applyResult(result);\n }\n\n private _scheduleMountCallbacks(): void {\n if (this._component.mountCallbacks.length === 0) return;\n\n const capturedGeneration = this._component.generation;\n // Tracked as pending work for the duration of this microtask: ended in a `finally`\n // so a thrown callback (already caught per-callback below, but defensive here too)\n // never leaves the counter stuck above zero. See runtime.ts's beginPendingWork().\n const endWork = beginPendingWork();\n\n queueMicrotask(() => {\n try {\n if (this._isStale(capturedGeneration)) return;\n\n // Snapshot callbacks so in-loop registrations don't extend this iteration.\n // Nested onMounted registrations are appended to `batch` and run in the same\n // microtask: no recursive scheduling. Index-based loop because the array\n // grows as nested callbacks are discovered.\n const batch = this._component.mountCallbacks.splice(0);\n\n for (let i = 0; i < batch.length; i++) {\n const callback = batch[i];\n\n try {\n const nestedCtx = createRuntimeContext(this);\n\n this._component.scope.run(() => {\n runWithContext(nestedCtx, () => {\n const cleanup = callback();\n\n if (typeof cleanup === 'function') onCleanup(cleanup);\n });\n });\n\n if (nestedCtx.mountCallbacks.length > 0) {\n batch.push(...nestedCtx.mountCallbacks);\n }\n\n if (nestedCtx.formResetCallbacks.length > 0) {\n this._component.formResetCallbacks.push(...nestedCtx.formResetCallbacks);\n }\n } catch (error) {\n this._reportLifecycleError(error, 'mounted');\n }\n }\n } finally {\n endWork();\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"base-element.js","names":["ComponentPhase","createComponentState","createScope","isPromiseLike","value","BaseElement","def","untrack","oldValue","newValue","propMeta","getPropMeta","name","parsed","token","callback","error","err","oreError","OreLifecycleError","phase","reportRuntimeError","normalizedPropDefs","ctx","createRuntimeContext","setupResult","runWithContext","setupProps","createProps","OreApiError","ORE_ERRORS","capturedGeneration","result","host","context","onCleanup","loadStylesheet","endWork","beginPendingWork","batch","i","nestedCtx","cleanup"],"sources":["../src/base-element.ts"],"sourcesContent":["import { createScope, type Scope, untrack } from '@vielzeug/ripple';\n\nimport type { ComponentDefinition } from './component-types';\n\nimport { ORE_ERRORS, OreApiError, type OreErrorPhase, OreLifecycleError, reportRuntimeError } from './errors';\nimport { createProps, getPropMeta, type InferProps, type PropInputDefs, type PropsDef } from './props';\nimport {\n beginPendingWork,\n createRuntimeContext,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n runWithContext,\n} from './runtime';\nimport type { HTMLResult } from './template/result';\nimport { loadStylesheet } from './utils/css';\n\n// ─── Component phases ─────────────────────────────────────────────────────────\n// Internal to BaseElement: the only state machine in the package.\n\nconst ComponentPhase = {\n SETUP_DONE: 'setup_done',\n SETUP_RUNNING: 'setup_running',\n UNINITIALIZED: 'uninitialized',\n UNMOUNTED: 'unmounted',\n} as const;\n\ntype ComponentPhase = (typeof ComponentPhase)[keyof typeof ComponentPhase];\n\n// ─── Internal component state ─────────────────────────────────────────────────\n\ntype ComponentState = {\n /** Registered via `onFormReset()`: persists across mount callbacks, unlike `mountCallbacks`. */\n formResetCallbacks: OnFormResetCallback[];\n /** Incremented on every disconnect: invalidates queued mount callbacks. */\n generation: number;\n mountCallbacks: OnMountedCallback[];\n phase: ComponentPhase;\n scope: Scope;\n templateResult: HTMLResult | null;\n};\n\nconst createComponentState = (): ComponentState => ({\n formResetCallbacks: [],\n generation: 0,\n mountCallbacks: [],\n phase: ComponentPhase.UNINITIALIZED,\n scope: createScope(),\n templateResult: null,\n});\n\nconst isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function';\n\n// ─── BaseElement ──────────────────────────────────────────────────────────────\n\n/**\n * Phase transitions:\n *\n * ```\n * UNINITIALIZED ──_runSetup()──► SETUP_DONE\n * SETUP_DONE ──disconnectedCallback()──► UNMOUNTED ──(teardown microtask)──► UNINITIALIZED\n * UNMOUNTED ──connectedCallback() before the teardown runs──► SETUP_DONE (same-turn move)\n * ```\n *\n * `generation` increments on every completed teardown. Scheduled mount callbacks\n * capture it so callbacks belonging to a torn-down connection cannot run after a\n * later connection (see `_isStale`; its `isConnected` check covers the window\n * while a teardown is still pending).\n *\n * Why this lives on the class instead of a standalone pure reducer: every\n * transition here is triggered by running actual user code (`def.setup()`,\n * `onMounted` callbacks) inside a reactive `scope.run()` + `runWithContext()`\n *: there is no meaningful \"decide the next phase\" step that can be separated\n * from \"run the side-effecting thing that produces the phase change\" without\n * introducing a data-only effect-description layer that this package has no\n * other use for. That's why the methods below stay as direct, readable\n * procedural steps instead of a reducer + effect interpreter.\n */\nexport class BaseElement extends HTMLElement {\n static _definition: ComponentDefinition;\n static _normalizedPropDefs: PropsDef<Record<never, never>> | undefined;\n static formAssociated = false;\n static observedAttributes: string[] = [];\n\n private _component: ComponentState;\n /** Cancellation token for the teardown microtask; `null` when none is pending. */\n private _pendingTeardown: object | null = null;\n private _stylesApplied = false;\n\n constructor() {\n super();\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (def?.shadow !== false) {\n this.attachShadow({ mode: 'open', ...(def?.shadow as Partial<ShadowRootInit> | undefined) });\n }\n\n this._component = createComponentState();\n }\n\n connectedCallback(): void {\n untrack(() => {\n // Move detection: `disconnectedCallback()` deferred its teardown to the\n // next microtask and nothing ran it yet, so this connect is the second\n // half of a same-turn node move (reparent, `insertBefore` reordering).\n // The scope, mounted template, and every piece of component state are\n // still intact, and the shadow root kept its style sheets and DOM while\n // detached: re-running setup or re-mounting the template would destroy\n // and rebuild all of it. Skip straight back to the mounted phase.\n if (this._pendingTeardown !== null) {\n this._pendingTeardown = null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n // Mount callbacks scheduled before the move were skipped as stale while\n // detached (the microtask checks `isConnected`); run them now.\n this._scheduleMountCallbacks();\n\n return;\n }\n\n if (this._component.phase === ComponentPhase.UNINITIALIZED) this._runSetup();\n\n this._init();\n });\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n\n const propMeta = getPropMeta(this, name);\n\n if (!propMeta) return;\n\n const parsed = propMeta.parse(newValue);\n\n if (\n !Object.is(\n untrack(() => propMeta.signal.value),\n parsed,\n )\n )\n propMeta.signal.value = parsed as never;\n }\n\n /**\n * Teardown is deferred one microtask so a same-turn node *move* is not a\n * lifecycle boundary: `connectedCallback()` cancels the pending teardown and\n * the component keeps its scope, state, and mounted template (see\n * `connectedCallback`). A real disconnect disposes everything one microtask\n * later; a reconnection after that point is a full remount, exactly as a\n * late re-add always has been.\n */\n disconnectedCallback(): void {\n this._component.phase = ComponentPhase.UNMOUNTED;\n\n const token = {};\n\n this._pendingTeardown = token;\n\n queueMicrotask(() => {\n if (this._pendingTeardown !== token) return;\n\n this._pendingTeardown = null;\n this._component.generation++;\n this._resetSetupState();\n });\n }\n\n /** Dispose one connection's resources before its state can be rebuilt. */\n private _resetSetupState(): void {\n this._component.scope.dispose();\n // Reset mutable fields for next connection, keeping the same object for stable references.\n this._component.formResetCallbacks = [];\n this._component.mountCallbacks = [];\n this._component.phase = ComponentPhase.UNINITIALIZED;\n this._component.scope = createScope();\n this._component.templateResult = null;\n }\n\n /**\n * Native form-association lifecycle callback: the browser calls this on every\n * `formAssociated: true` element inside a `<form>` when that form is reset.\n * Runs every `onFormReset()` callback registered during `setup()`.\n */\n formResetCallback(): void {\n for (const callback of this._component.formResetCallbacks) {\n try {\n callback();\n } catch (error) {\n this._reportLifecycleError(error, 'form-reset');\n }\n }\n }\n\n private _reportLifecycleError(error: unknown, phase: OreErrorPhase): void {\n const err = error instanceof Error ? error : new Error(String(error));\n const oreError = new OreLifecycleError(`<${this.localName}> failed during ${this._component.phase} (${phase})`, {\n cause: err,\n component: this.localName,\n phase,\n });\n\n reportRuntimeError(oreError, this);\n }\n\n private _runSetup(): void {\n this._component.phase = ComponentPhase.SETUP_RUNNING;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n const normalizedPropDefs = (this.constructor as typeof BaseElement)._normalizedPropDefs;\n const ctx = createRuntimeContext(this);\n\n try {\n let setupResult: HTMLResult | null | undefined;\n\n this._component.scope.run(() => {\n setupResult = runWithContext(ctx, () => {\n const setupProps = normalizedPropDefs\n ? createProps(this, normalizedPropDefs)\n : ({} as InferProps<PropInputDefs>);\n\n return def.setup(setupProps as InferProps<PropInputDefs>);\n });\n });\n this._component.mountCallbacks.push(...ctx.mountCallbacks);\n this._component.formResetCallbacks.push(...ctx.formResetCallbacks);\n\n if (isPromiseLike(setupResult)) throw new OreApiError(ORE_ERRORS.asyncSetupUnsupported);\n\n this._component.templateResult = setupResult ?? null;\n this._component.phase = ComponentPhase.SETUP_DONE;\n } catch (error) {\n this._reportLifecycleError(error, 'setup');\n // Setup is atomic: a failed run must not leave partial effects or cleanups\n // live until a later disconnect.\n this._resetSetupState();\n throw error;\n }\n }\n\n private _isStale(capturedGeneration: number): boolean {\n return this._component.generation !== capturedGeneration || !this.isConnected;\n }\n\n private _applyResult(result: HTMLResult | null): void {\n if (!result) return;\n\n const host: Element | ShadowRoot = this.shadowRoot ?? this;\n\n // Mounting can register component cleanup, so preserve a runtime context here too.\n const context = createRuntimeContext(this);\n\n host.replaceChildren();\n this._component.scope.run(() => {\n runWithContext(context, () => {\n result.mount(host, null, onCleanup);\n });\n });\n }\n\n private _init(): void {\n this._applyStyles();\n this._mountTemplate();\n\n // Setup completes before the template mounts, so callbacks always observe live DOM.\n if (this._component.phase === ComponentPhase.SETUP_DONE) this._scheduleMountCallbacks();\n }\n\n private _applyStyles(): void {\n // `adoptedStyleSheets` lives on the shadow root, which outlives every\n // disconnect/reconnect: assign once per instance, never re-assign churn.\n if (this._stylesApplied) return;\n\n const def = (this.constructor as typeof BaseElement)._definition;\n\n if (this.shadowRoot && def?.styles?.length) {\n this.shadowRoot.adoptedStyleSheets = def.styles.map(loadStylesheet);\n this._stylesApplied = true;\n }\n }\n\n private _mountTemplate(): void {\n const result = this._component.templateResult;\n\n if (!result) return;\n\n this._applyResult(result);\n }\n\n private _scheduleMountCallbacks(): void {\n if (this._component.mountCallbacks.length === 0) return;\n\n const capturedGeneration = this._component.generation;\n // Tracked as pending work for the duration of this microtask: ended in a `finally`\n // so a thrown callback (already caught per-callback below, but defensive here too)\n // never leaves the counter stuck above zero. See runtime.ts's beginPendingWork().\n const endWork = beginPendingWork();\n\n queueMicrotask(() => {\n try {\n if (this._isStale(capturedGeneration)) return;\n\n // Snapshot callbacks so in-loop registrations don't extend this iteration.\n // Nested onMounted registrations are appended to `batch` and run in the same\n // microtask: no recursive scheduling. Index-based loop because the array\n // grows as nested callbacks are discovered.\n const batch = this._component.mountCallbacks.splice(0);\n\n for (let i = 0; i < batch.length; i++) {\n const callback = batch[i];\n\n try {\n const nestedCtx = createRuntimeContext(this);\n\n this._component.scope.run(() => {\n runWithContext(nestedCtx, () => {\n const cleanup = callback();\n\n if (typeof cleanup === 'function') onCleanup(cleanup);\n });\n });\n\n if (nestedCtx.mountCallbacks.length > 0) {\n batch.push(...nestedCtx.mountCallbacks);\n }\n\n if (nestedCtx.formResetCallbacks.length > 0) {\n this._component.formResetCallbacks.push(...nestedCtx.formResetCallbacks);\n }\n } catch (error) {\n this._reportLifecycleError(error, 'mounted');\n }\n }\n } finally {\n endWork();\n }\n });\n }\n}\n"],"mappings":"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"}
|
package/dist/directives/each.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=(e,t,n,i
|
|
1
|
+
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=(e,t,n,i)=>{let a=(0,r.signal)(e),o=(0,r.signal)(t),s=(0,r.createScope)(),c=[],l=[],u=document.createComment(`each/item-start`);i.appendChild(u),s.run(()=>{l=n(a,o).mount(i,null,e=>c.push(e))});let d=document.createComment(`each/item`);return i.appendChild(d),l=[u,...l,d],{cleanups:c,data:a,index:o,nodes:l,scope:s}},a=e=>{e.scope.dispose(),n.runAll(e.cleanups),n.removeNodes(e.nodes)},o=(e,t)=>{let n=new Map;for(let e=0;e<t.length;e++)n.set(t[e],e);let r=-1;for(let t of e){let e=n.get(t);if(e!==void 0){if(e<r)return!1;r=e}}return!0},s=(t,n,s,c,l,u,d,f)=>{let p=document.createDocumentFragment();for(let e=0;e<n.length;e++){let a=s[e];if(t.has(a))continue;let o=(0,r.untrack)(()=>i(n[e],e,c,p));d.push(o),t.set(a,o)}let m=[];(0,r.batch)(()=>{for(let r=0;r<n.length;r++){let i=t.get(s[r]);e.invariant(i,`each() reconcile phase 2 lost an entry created in phase 1`),i.data.value=n[r],i.index.value=r,m.push(i)}});let h=o(m,f),g=new Set(d),_=u;for(let e=m.length-1;e>=0;e--){let t=m[e],n=t.nodes[0],r=t.nodes[t.nodes.length-1];if((!h||g.has(t))&&r!==_.previousSibling){let e=n;for(;e;){let t=e===r?null:e.nextSibling;if(l.insertBefore(e,_),e===r)break;e=t}}_=n}let v=new Set(s);for(let[e,n]of t)v.has(e)||(a(n),t.delete(e));return m},c=(e,t)=>{for(let n of t){a(n);for(let[t,r]of e)if(r===n){e.delete(t);break}}},l=(t,n)=>{let r=[],i=new Set;for(let a=0;a<t.length;a++){let o=n(t[a],a);if(i.has(o))throw new e.OreApiError(e.ORE_ERRORS.eachDuplicateKey(String(o),a));i.add(o),r.push(o)}return r},u=(t,n)=>{let r=t instanceof Error?t:Error(String(t));e.reportRuntimeError(new e.OreLifecycleError(`each() failed to reconcile a list update: ${r.message}`,{cause:r,component:`each()`,phase:`each-reconcile`}),n)};function d(i,o,d,f){let p=Array.isArray(i)?(0,r.signal)(i):typeof i==`function`?(0,r.computed)(i):i;return t.createDirectiveResult((t,i)=>{let m=t.parentNode;e.invariant(m,`each() anchor comment has no parent node`);let h=document.createComment(`each/end`);m.insertBefore(h,t.nextSibling);let g=()=>h.parentNode,_=new Map,v=[],y=null,b=[],x=()=>{if(!f)return;let e=g();e&&(y=f().mount(e,h,e=>b.push(e)))},S=()=>{y&&(n.runAll(b),n.removeNodes(y),y=null,b=[])},C=(0,r.effect)(()=>{let e=p.value??[];if(e.length===0){for(let e of(0,r.untrack)(()=>v))a(e);_=new Map,v=[],y||(0,r.untrack)(x);return}let n;try{n=(0,r.untrack)(()=>l(e,o))}catch(e){u(e,t);return}S();let i=g();if(!i)return;let f=[];try{v=(0,r.untrack)(()=>s(_,e,n,d,i,h,f,v))}catch(e){u(e,t),(0,r.untrack)(()=>c(_,f))}});i(()=>C.dispose()),i(()=>{S();for(let e of v)a(e);h.remove()})})}exports.each=d;
|
|
2
2
|
//# sourceMappingURL=each.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"each.cjs","names":["signal","createScope","untrack","OreApiError","ORE_ERRORS","OreLifecycleError","computed","createDirectiveResult","rawEffect","require_errors","require_result","require_dom","_vielzeug_ripple","createItem","dataSignal","item","indexSignal","index","scope","cleanups","nodes","render","parent","insertBefore","fn","removeItem","entry","reconcileItems","i","next","key","nextKeys","itemsMap","endMarker","createdThisPass","nextOrdered","cursor","j","firstNode","lastNode","node","nextKeySet","rollbackCreated","created","candidate","collectKeys","keys","seen","keyFn","reportEachError","cause","error","anchor","each","listSignal","list","itemsOrdered","fallbackNodes","fallbackCleanups","mountFallback","fallback","clearFallback","sub","nextList","registerCleanup"],"sources":["../../src/directives/each.ts"],"sourcesContent":["import {\n batch,\n computed,\n createScope,\n type Readable,\n effect as rawEffect,\n type Scope,\n type Signal,\n signal,\n untrack,\n} from '@vielzeug/ripple';\n\nimport { invariant, ORE_ERRORS, OreApiError, OreLifecycleError, reportRuntimeError } from '../errors';\nimport { createDirectiveResult, type DirectiveResult, type HTMLResult } from '../template/result';\nimport { type MaybeReactive, removeNodes, runAll } from '../utils/dom';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype MaybeReactiveArray<T> = MaybeReactive<T[]>;\ntype ItemKey = number | string;\n\ntype ItemEntry<T> = {\n cleanups: (() => void)[];\n data: Signal<T>;\n index: Signal<number>;\n nodes: Node[];\n scope: Scope;\n};\n\n// ─── Item lifecycle ───────────────────────────────────────────────────────────\n\nconst createItem = <T>(\n item: T,\n index: number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n insertBefore: Node,\n): ItemEntry<T> => {\n const dataSignal: Signal<T> = signal(item);\n const indexSignal: Signal<number> = signal(index);\n const scope = createScope();\n const cleanups: (() => void)[] = [];\n let nodes: Node[] = [];\n\n scope.run(() => {\n const result = render(dataSignal, indexSignal);\n\n nodes = result.mount(parent, insertBefore, (fn) => cleanups.push(fn));\n });\n\n return { cleanups, data: dataSignal, index: indexSignal, nodes, scope };\n};\n\nconst removeItem = <T>(entry: ItemEntry<T>): void => {\n entry.scope.dispose();\n runAll(entry.cleanups);\n removeNodes(entry.nodes);\n};\n\n// ─── Reconciler ───────────────────────────────────────────────────────────────\n\n/**\n * Reconciles the live item map against the next array. The pass is transactional:\n * new entries are created first, existing entries are updated in-place via signal\n * writes, and stale entries are destroyed only after the whole pass has succeeded.\n * If creation throws (user render code), the caller rolls back the entries created\n * during this pass and the previous list state survives: the last good render stays\n * on screen. Returns the ordered list of entries matching nextList.\n */\nconst reconcileItems = <T>(\n itemsMap: Map<ItemKey, ItemEntry<T>>,\n next: T[],\n nextKeys: ItemKey[],\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n endMarker: Node,\n createdThisPass: ItemEntry<T>[],\n): ItemEntry<T>[] => {\n // Phase 1: create every missing entry. This is the only step that can throw\n // (user render code), and it records what it created so a failure is undoable.\n for (let i = 0; i < next.length; i++) {\n const key = nextKeys[i];\n\n if (itemsMap.has(key)) continue;\n\n const entry = untrack(() => createItem(next[i], i, render, parent, endMarker));\n\n createdThisPass.push(entry);\n itemsMap.set(key, entry);\n }\n\n // Phase 2: ordered list + in-place updates for reused entries (signal writes).\n const nextOrdered: ItemEntry<T>[] = [];\n\n batch(() => {\n for (let i = 0; i < next.length; i++) {\n const entry = itemsMap.get(nextKeys[i]);\n\n invariant(entry, 'each() reconcile phase 2 lost an entry created in phase 1');\n\n entry.data.value = next[i];\n entry.index.value = i;\n nextOrdered.push(entry);\n }\n });\n\n // DOM ordering: right-to-left pass: move any item not already adjacent to cursor.\n // An item is in place when its last node directly precedes the cursor; items may\n // span several nodes (whitespace text around an element), so compare the last one.\n // Moving an already-placed item would disconnect and reconnect its custom elements.\n // O(n) DOM operations in the worst case; optimal for the typical small list sizes\n // encountered in UI components (tabs, options, menu items).\n let cursor: Node = endMarker;\n\n for (let j = nextOrdered.length - 1; j >= 0; j--) {\n const entry = nextOrdered[j];\n const firstNode = entry.nodes[0];\n const lastNode = entry.nodes[entry.nodes.length - 1];\n\n if (firstNode && lastNode !== cursor.previousSibling) {\n for (const node of entry.nodes) parent.insertBefore(node, cursor);\n }\n\n cursor = firstNode ?? cursor;\n }\n\n // Phase 3: commit: destroy stale entries only now that the pass has succeeded.\n const nextKeySet = new Set(nextKeys);\n\n for (const [key, entry] of itemsMap) {\n if (!nextKeySet.has(key)) {\n removeItem(entry);\n itemsMap.delete(key);\n }\n }\n\n return nextOrdered;\n};\n\n/** Undo a failed reconcile pass: destroy entries created during it, restoring the last good list. */\nconst rollbackCreated = <T>(itemsMap: Map<ItemKey, ItemEntry<T>>, created: ItemEntry<T>[]): void => {\n for (const entry of created) {\n removeItem(entry);\n\n for (const [key, candidate] of itemsMap) {\n if (candidate === entry) {\n itemsMap.delete(key);\n break;\n }\n }\n }\n};\n\nconst collectKeys = <T>(next: T[], keyFn: (item: T, index: number) => ItemKey): ItemKey[] => {\n const keys: ItemKey[] = [];\n const seen = new Set<ItemKey>();\n\n for (let i = 0; i < next.length; i++) {\n const key = keyFn(next[i], i);\n\n if (seen.has(key)) throw new OreApiError(ORE_ERRORS.eachDuplicateKey(String(key), i));\n\n seen.add(key);\n keys.push(key);\n }\n\n return keys;\n};\n\nconst reportEachError = (error: unknown, anchor: Comment): void => {\n const cause = error instanceof Error ? error : new Error(String(error));\n\n reportRuntimeError(\n new OreLifecycleError(`each() failed to reconcile a list update: ${cause.message}`, {\n cause,\n component: 'each()',\n phase: 'each-reconcile',\n }),\n anchor,\n );\n};\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Renders a keyed list of items as a `DirectiveResult`.\n *\n * Each item is rendered by the provided render function.\n * Items are reused by key when the list changes; only stale items are destroyed.\n *\n * The render function receives a `Readable<T>` signal and a `Readable<number>` index\n * signal. Use `item.value` to read the current item inside reactive expressions:\n *\n * ```ts\n * html`${each(items, (item) => item.id, (item) => html`<li>${() => item.value.name}</li>`)}`\n * ```\n *\n * **Plain array:** when a plain `T[]` is passed (not a signal or getter), it is\n * treated as a one-time static render. Mutations to the original array are not\n * tracked. Pass a `Signal<T[]>` or `() => T[]` for reactive lists.\n *\n * **Optional fallback:** the fourth argument renders when the list is empty.\n *\n * **Key choice:** pass a stable item identifier (e.g. `item.id`), never the\n * array index: an index-based key reassigns to a different item whenever the\n * list is reordered or an item is inserted/removed before it, causing full\n * item teardown/recreation instead of the in-place update `each()` is built\n * for.\n *\n * **Duplicate keys:** a reconciliation failure (e.g. duplicate keys, see `eachDuplicateKey`)\n * does not throw past this function: an uncaught exception inside the reactive effect that\n * drives `each()` would risk corrupting unrelated effects scheduled in the same update batch.\n * Instead the last valid list is preserved and the failure is reported via the `ore:error` DOM event\n * (see `OreLifecycleError`, phase `'each-reconcile'`) plus a dev-only console log: listen for\n * `ore:error` on `document`/`window` to observe this in every build, including production.\n */\nexport function each<T>(\n list: MaybeReactiveArray<T>,\n keyFn: (item: T, index: number) => string | number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n fallback?: () => HTMLResult,\n): DirectiveResult {\n const listSignal = Array.isArray(list)\n ? signal(list as T[])\n : typeof list === 'function'\n ? computed(list as () => T[])\n : list;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const parent = anchor.parentNode;\n\n invariant(parent, 'each() anchor comment has no parent node');\n\n const endMarker = document.createComment('each/end');\n\n parent.insertBefore(endMarker, anchor.nextSibling);\n\n let itemsMap = new Map<ItemKey, ItemEntry<T>>();\n let itemsOrdered: ItemEntry<T>[] = [];\n let fallbackNodes: Node[] | null = null;\n let fallbackCleanups: (() => void)[] = [];\n\n const mountFallback = (): void => {\n if (!fallback) return;\n\n const result = fallback();\n\n fallbackNodes = result.mount(parent, endMarker, (fn) => fallbackCleanups.push(fn));\n };\n\n const clearFallback = (): void => {\n if (fallbackNodes) {\n runAll(fallbackCleanups);\n removeNodes(fallbackNodes);\n fallbackNodes = null;\n fallbackCleanups = [];\n }\n };\n\n const sub = rawEffect(() => {\n const nextList = listSignal.value ?? [];\n\n if (nextList.length === 0) {\n for (const entry of untrack(() => itemsOrdered)) removeItem(entry);\n itemsMap = new Map();\n itemsOrdered = [];\n\n if (!fallbackNodes) untrack(mountFallback);\n\n return;\n }\n\n let nextKeys: ItemKey[];\n\n try {\n nextKeys = untrack(() => collectKeys(nextList, keyFn));\n } catch (error) {\n reportEachError(error, anchor);\n return;\n }\n\n clearFallback();\n\n const createdThisPass: ItemEntry<T>[] = [];\n\n try {\n itemsOrdered = untrack(() =>\n reconcileItems(itemsMap, nextList, nextKeys, render, parent, endMarker, createdThisPass),\n );\n } catch (error) {\n // Undo this pass instead of tearing the list down: entries created before the\n // throw are removed, everything that was on screen stays exactly as it was.\n reportEachError(error, anchor);\n untrack(() => rollbackCreated(itemsMap, createdThisPass));\n }\n });\n\n registerCleanup(() => sub.dispose());\n registerCleanup(() => {\n clearFallback();\n for (const entry of itemsOrdered) removeItem(entry);\n endMarker.remove();\n });\n });\n}\n"],"mappings":"A,qIA+BA,IAAMa,GACJ,EACA,EACA,EACA,EACA,IACiB,CACjB,IAAMC,GAAA,EAAwBd,EAAAA,OAAAA,CAAOe,CAAI,EACnCC,GAAA,EAA8BhB,EAAAA,OAAAA,CAAOiB,CAAK,EAC1CC,GAAA,EAAQjB,EAAAA,YAAAA,CAAY,EACpBkB,EAA2B,CAAC,EAC9BC,EAAgB,CAAC,EAQrB,OANAF,EAAM,QAAU,CAGd,EAFeG,EAAOP,EAAYE,CAE1B,CAAA,CAAO,MAAMM,EAAQC,EAAe,GAAOJ,EAAS,KAAKK,CAAE,CAAC,CACtE,CAAC,EAEM,CAAE,WAAU,KAAMV,EAAY,MAAOE,EAAa,QAAO,OAAM,CACxE,EAEMS,EAAiB,GAA8B,CACnDC,EAAM,MAAM,QAAQ,EACpBf,EAAA,OAAOe,EAAM,QAAQ,EACrBf,EAAA,YAAYe,EAAM,KAAK,CACzB,EAYMC,GACJ,EACA,EACA,EACA,EACA,EACA,EACA,IACmB,CAGnB,IAAK,IAAIC,EAAI,EAAGA,EAAIC,EAAK,OAAQ,IAAK,CACpC,IAAMC,EAAMC,EAASH,GAErB,GAAII,EAAS,IAAIF,CAAG,EAAG,SAEvB,IAAMJ,GAAA,EAAQxB,EAAAA,QAAAA,KAAcW,EAAWgB,EAAKD,GAAIA,EAAGP,EAAQC,EAAQW,CAAS,CAAC,EAE7EC,EAAgB,KAAKR,CAAK,EAC1BM,EAAS,IAAIF,EAAKJ,CAAK,CACzB,CAGA,IAAMS,EAA8B,CAAC,GAErC,EAAAvB,EAAA,MAAA,KAAY,CACV,IAAK,IAAIgB,EAAI,EAAGA,EAAIC,EAAK,OAAQ,IAAK,CACpC,IAAMH,EAAQM,EAAS,IAAID,EAASH,EAAE,EAEtCnB,EAAA,UAAUiB,EAAO,2DAA2D,EAE5E,EAAM,KAAK,MAAQG,EAAKD,GACxB,EAAM,MAAM,MAAQA,EACpBO,EAAY,KAAKT,CAAK,CACxB,CACF,CAAC,EAQD,IAAIU,EAAeH,EAEnB,IAAK,IAAII,EAAIF,EAAY,OAAS,EAAGE,GAAK,EAAG,IAAK,CAChD,IAAMX,EAAQS,EAAYE,GACpBC,EAAYZ,EAAM,MAAM,GACxBa,EAAWb,EAAM,MAAMA,EAAM,MAAM,OAAS,GAElD,GAAIY,GAAaC,IAAaH,EAAO,gBACnC,IAAK,IAAMI,KAAQd,EAAM,MAAOJ,EAAO,aAAakB,EAAMJ,CAAM,EAGlE,EAASE,GAAaF,CACxB,CAGA,IAAMK,EAAa,IAAI,IAAIV,CAAQ,EAEnC,IAAK,GAAM,CAACD,EAAKJ,KAAUM,EACpBS,EAAW,IAAIX,CAAG,IACrBL,EAAWC,CAAK,EAChBM,EAAS,OAAOF,CAAG,GAIvB,OAAOK,CACT,EAGMO,GAAsB,EAAsC,IAAkC,CAClG,IAAK,IAAMhB,KAASiB,EAAS,CAC3BlB,EAAWC,CAAK,EAEhB,IAAK,GAAM,CAACI,EAAKc,KAAcZ,EAC7B,GAAIY,IAAclB,EAAO,CACvBM,EAAS,OAAOF,CAAG,EACnB,KACF,CAEJ,CACF,EAEMe,GAAkB,EAAW,IAA0D,CAC3F,IAAMC,EAAkB,CAAC,EACnBC,EAAO,IAAI,IAEjB,IAAK,IAAInB,EAAI,EAAGA,EAAIC,EAAK,OAAQ,IAAK,CACpC,IAAMC,EAAMkB,EAAMnB,EAAKD,GAAIA,CAAC,EAE5B,GAAImB,EAAK,IAAIjB,CAAG,EAAG,MAAM,IAAI3B,EAAAA,YAAYC,EAAAA,WAAW,iBAAiB,OAAO0B,CAAG,EAAGF,CAAC,CAAC,EAEpFmB,EAAK,IAAIjB,CAAG,EACZgB,EAAK,KAAKhB,CAAG,CACf,CAEA,OAAOgB,CACT,EAEMG,GAAmB,EAAgB,IAA0B,CACjE,IAAMC,EAAQC,aAAiB,MAAQA,EAAY,MAAM,OAAOA,CAAK,CAAC,EAEtE1C,EAAA,mBACE,IAAIJ,EAAAA,kBAAkB,6CAA6C6C,EAAM,UAAW,CAClF,QACA,UAAW,SACX,MAAO,gBACT,CAAC,EACDE,CACF,CACF,EAoCA,SAAgBC,EACd,EACA,EACA,EACA,EACiB,CACjB,IAAMC,EAAa,MAAM,QAAQC,CAAI,GAAA,EACjCvD,EAAAA,OAAAA,CAAOuD,CAAW,EAClB,OAAOA,GAAS,YAAA,EACdjD,EAAAA,SAAAA,CAASiD,CAAiB,EAC1BA,EAEN,OAAOhD,EAAAA,uBAAuB,EAAQ,IAAoB,CACxD,IAAMe,EAAS8B,EAAO,WAEtB3C,EAAA,UAAUa,EAAQ,0CAA0C,EAE5D,IAAMW,EAAY,SAAS,cAAc,UAAU,EAEnDX,EAAO,aAAaW,EAAWmB,EAAO,WAAW,EAEjD,IAAIpB,EAAW,IAAI,IACfwB,EAA+B,CAAC,EAChCC,EAA+B,KAC/BC,EAAmC,CAAC,EAElCC,MAA4B,CAC3BC,IAIL,EAFeA,EAEC,CAAA,CAAO,MAAMtC,EAAQW,EAAY,GAAOyB,EAAiB,KAAKlC,CAAE,CAAC,EACnF,EAEMqC,MAA4B,CAC5BJ,IACF9C,EAAA,OAAO+C,CAAgB,EACvB/C,EAAA,YAAY8C,CAAa,EACzB,EAAgB,KAChB,EAAmB,CAAC,EAExB,EAEMK,GAAA,EAAMtD,EAAAA,OAAAA,KAAgB,CAC1B,IAAMuD,EAAWT,EAAW,OAAS,CAAC,EAEtC,GAAIS,EAAS,SAAW,EAAG,CACzB,IAAK,IAAMrC,KAAA,EAASxB,EAAAA,QAAAA,KAAcsD,CAAY,EAAG/B,EAAWC,CAAK,EACjE,EAAW,IAAI,IACf,EAAe,CAAC,EAEX+B,IAAe,EAAA7C,EAAA,QAAA,CAAQ+C,CAAa,EAEzC,MACF,CAEA,IAAI5B,EAEJ,GAAI,CACF,GAAA,EAAW7B,EAAAA,QAAAA,KAAc2C,EAAYkB,EAAUf,CAAK,CAAC,CACvD,OAASG,EAAO,CACdF,EAAgBE,EAAOC,CAAM,EAC7B,MACF,CAEAS,EAAc,EAEd,IAAM3B,EAAkC,CAAC,EAEzC,GAAI,CACF,GAAA,EAAehC,EAAAA,QAAAA,KACbyB,EAAeK,EAAU+B,EAAUhC,EAAUV,EAAQC,EAAQW,EAAWC,CAAe,CACzF,CACF,OAASiB,EAAO,CAGdF,EAAgBE,EAAOC,CAAM,GAC7B,EAAAxC,EAAA,QAAA,KAAc8B,EAAgBV,EAAUE,CAAe,CAAC,CAC1D,CACF,CAAC,EAED8B,MAAsBF,EAAI,QAAQ,CAAC,EACnCE,MAAsB,CACpBH,EAAc,EACd,IAAK,IAAMnC,KAAS8B,EAAc/B,EAAWC,CAAK,EAClDO,EAAU,OAAO,CACnB,CAAC,CACH,CAAC,CACH,C"}
|
|
1
|
+
{"version":3,"file":"each.cjs","names":["signal","createScope","untrack","OreApiError","ORE_ERRORS","OreLifecycleError","computed","createDirectiveResult","rawEffect","require_errors","require_result","require_dom","_vielzeug_ripple","createItem","dataSignal","item","indexSignal","index","scope","cleanups","nodes","rangeStart","staging","render","fn","rangeEnd","removeItem","entry","reusedEntriesInOrder","previousPosition","i","itemsOrdered","lastPosition","nextOrdered","position","reconcileItems","next","key","nextKeys","itemsMap","createdThisPass","reusedInOrder","created","cursor","endMarker","j","firstNode","node","parent","nextKeySet","rollbackCreated","candidate","collectKeys","keys","seen","keyFn","reportEachError","cause","error","anchor","each","listSignal","list","initialParent","container","fallbackNodes","fallbackCleanups","mountFallback","fallback","clearFallback","sub","nextList","registerCleanup"],"sources":["../../src/directives/each.ts"],"sourcesContent":["import {\n batch,\n computed,\n createScope,\n type Readable,\n effect as rawEffect,\n type Scope,\n type Signal,\n signal,\n untrack,\n} from '@vielzeug/ripple';\n\nimport { invariant, ORE_ERRORS, OreApiError, OreLifecycleError, reportRuntimeError } from '../errors';\nimport { createDirectiveResult, type DirectiveResult, type HTMLResult } from '../template/result';\nimport { type MaybeReactive, removeNodes, runAll } from '../utils/dom';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype MaybeReactiveArray<T> = MaybeReactive<T[]>;\ntype ItemKey = number | string;\n\n/**\n * A rendered list item. `nodes` is the item's contiguous sibling range,\n * bracketed by `each/item-start` and `each/item` sentinel comments: the\n * sentinels wrap the template's own nodes, and every reactive directive inside\n * the item (`when()`, nested `each()`, html bindings) inserts and re-inserts\n * its content as siblings relative to nodes between the sentinels — before or\n * after its own anchor, but never past a sentinel. Relocating the item\n * therefore means moving the whole bracketed range, node by node: never a\n * bare subset.\n */\ntype ItemEntry<T> = {\n cleanups: (() => void)[];\n data: Signal<T>;\n index: Signal<number>;\n nodes: Node[];\n scope: Scope;\n};\n\n// ─── Item lifecycle ───────────────────────────────────────────────────────────\n\n/**\n * Mounts the item into `staging` (a detached fragment), bracketing its node\n * range with the start and end sentinels. Synchronous directive content mounts\n * between the sentinels (the end sentinel only exists once the whole template\n * is mounted), so the range is complete the moment this returns.\n */\nconst createItem = <T>(\n item: T,\n index: number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n staging: DocumentFragment,\n): ItemEntry<T> => {\n const dataSignal: Signal<T> = signal(item);\n const indexSignal: Signal<number> = signal(index);\n const scope = createScope();\n const cleanups: (() => void)[] = [];\n let nodes: Node[] = [];\n\n const rangeStart = document.createComment('each/item-start');\n\n staging.appendChild(rangeStart);\n\n scope.run(() => {\n const result = render(dataSignal, indexSignal);\n\n nodes = result.mount(staging, null, (fn) => cleanups.push(fn));\n });\n\n const rangeEnd = document.createComment('each/item');\n\n staging.appendChild(rangeEnd);\n nodes = [rangeStart, ...nodes, rangeEnd];\n\n return { cleanups, data: dataSignal, index: indexSignal, nodes, scope };\n};\n\nconst removeItem = <T>(entry: ItemEntry<T>): void => {\n entry.scope.dispose();\n runAll(entry.cleanups);\n removeNodes(entry.nodes);\n};\n\n// ─── Reconciler ───────────────────────────────────────────────────────────────\n\n/**\n * True when reused entries appear in the next list in the same relative order\n * they hold in the DOM (`itemsOrdered`). Prepends, appends, interleaved\n * inserts, and content replacement all satisfy this; only genuine reorders\n * (or removes that flip survivor order) do not.\n */\nconst reusedEntriesInOrder = <T>(nextOrdered: ItemEntry<T>[], itemsOrdered: ItemEntry<T>[]): boolean => {\n const previousPosition = new Map<ItemEntry<T>, number>();\n\n for (let i = 0; i < itemsOrdered.length; i++) previousPosition.set(itemsOrdered[i], i);\n\n let lastPosition = -1;\n\n for (const entry of nextOrdered) {\n const position = previousPosition.get(entry);\n\n if (position === undefined) continue;\n\n if (position < lastPosition) return false;\n\n lastPosition = position;\n }\n\n return true;\n};\n\n/**\n * Reconciles the live item map against the next array. The pass is transactional:\n * new entries are created first, existing entries are updated in-place via signal\n * writes, and stale entries are destroyed only after the whole pass has succeeded.\n * If creation throws (user render code), the caller rolls back the entries created\n * during this pass and the previous list state survives: the last good render stays\n * on screen. Returns the ordered list of entries matching nextList.\n */\nconst reconcileItems = <T>(\n itemsMap: Map<ItemKey, ItemEntry<T>>,\n next: T[],\n nextKeys: ItemKey[],\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n endMarker: Node,\n createdThisPass: ItemEntry<T>[],\n itemsOrdered: ItemEntry<T>[],\n): ItemEntry<T>[] => {\n // Phase 1: create every missing entry. This is the only step that can throw\n // (user render code), and it records what it created so a failure is undoable.\n // New entries mount into a detached staging fragment, not the live parent: an\n // entry not already at its final position would otherwise connect at the end,\n // then disconnect again when the ordering pass relocates it (double\n // setup/teardown of any custom elements it contains). Staged nodes connect\n // exactly once, when the ordering pass moves them into place.\n const staging = document.createDocumentFragment();\n\n for (let i = 0; i < next.length; i++) {\n const key = nextKeys[i];\n\n if (itemsMap.has(key)) continue;\n\n const entry = untrack(() => createItem(next[i], i, render, staging));\n\n createdThisPass.push(entry);\n itemsMap.set(key, entry);\n }\n\n // Phase 2: ordered list + in-place updates for reused entries (signal writes).\n const nextOrdered: ItemEntry<T>[] = [];\n\n batch(() => {\n for (let i = 0; i < next.length; i++) {\n const entry = itemsMap.get(nextKeys[i]);\n\n invariant(entry, 'each() reconcile phase 2 lost an entry created in phase 1');\n\n entry.data.value = next[i];\n entry.index.value = i;\n nextOrdered.push(entry);\n }\n });\n\n // DOM ordering: one right-to-left pass with a cursor. An entry is in place\n // when its range sentinel directly precedes the cursor. Moving an entry\n // disconnects and reconnects its custom elements, so moves are kept minimal:\n // when reused entries kept their relative order (the common case: prepend,\n // append, interleaved inserts, content replacement), reused ranges are never\n // moved at all and only entries created this pass are relocated next to their\n // nearest reused neighbour. Genuine reorders fall back to moving any range the\n // cursor cannot reach. A move relocates the whole contiguous range up to and\n // including the sentinel so content inserted by directives inside the item\n // travels with it.\n const reusedInOrder = reusedEntriesInOrder(nextOrdered, itemsOrdered);\n const created = new Set<ItemEntry<T>>(createdThisPass);\n\n let cursor: Node = endMarker;\n\n for (let j = nextOrdered.length - 1; j >= 0; j--) {\n const entry = nextOrdered[j];\n const firstNode = entry.nodes[0];\n const rangeEnd = entry.nodes[entry.nodes.length - 1];\n\n // Staged nodes never satisfy the adjacency test (they live in the fragment),\n // so created entries are always moved to their final position.\n if ((!reusedInOrder || created.has(entry)) && rangeEnd !== cursor.previousSibling) {\n let node: Node | null = firstNode;\n\n while (node) {\n const next: Node | null = node === rangeEnd ? null : node.nextSibling;\n\n parent.insertBefore(node, cursor);\n\n if (node === rangeEnd) break;\n node = next;\n }\n }\n\n cursor = firstNode;\n }\n\n // Phase 3: commit: destroy stale entries only now that the pass has succeeded.\n const nextKeySet = new Set(nextKeys);\n\n for (const [key, entry] of itemsMap) {\n if (!nextKeySet.has(key)) {\n removeItem(entry);\n itemsMap.delete(key);\n }\n }\n\n return nextOrdered;\n};\n\n/** Undo a failed reconcile pass: destroy entries created during it, restoring the last good list. */\nconst rollbackCreated = <T>(itemsMap: Map<ItemKey, ItemEntry<T>>, created: ItemEntry<T>[]): void => {\n for (const entry of created) {\n removeItem(entry);\n\n for (const [key, candidate] of itemsMap) {\n if (candidate === entry) {\n itemsMap.delete(key);\n break;\n }\n }\n }\n};\n\nconst collectKeys = <T>(next: T[], keyFn: (item: T, index: number) => ItemKey): ItemKey[] => {\n const keys: ItemKey[] = [];\n const seen = new Set<ItemKey>();\n\n for (let i = 0; i < next.length; i++) {\n const key = keyFn(next[i], i);\n\n if (seen.has(key)) throw new OreApiError(ORE_ERRORS.eachDuplicateKey(String(key), i));\n\n seen.add(key);\n keys.push(key);\n }\n\n return keys;\n};\n\nconst reportEachError = (error: unknown, anchor: Comment): void => {\n const cause = error instanceof Error ? error : new Error(String(error));\n\n reportRuntimeError(\n new OreLifecycleError(`each() failed to reconcile a list update: ${cause.message}`, {\n cause,\n component: 'each()',\n phase: 'each-reconcile',\n }),\n anchor,\n );\n};\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Renders a keyed list of items as a `DirectiveResult`.\n *\n * Each item is rendered by the provided render function.\n * Items are reused by key when the list changes; only stale items are destroyed.\n * Reused items keep their DOM nodes: when the surviving items stay in the same\n * relative order (prepend, append, interleaved inserts, content replacement),\n * no reused node is ever moved, so their CSS transitions, animations, and any\n * nested component state survive the update untouched. Only genuine reorders\n * move nodes.\n *\n * The render function receives a `Readable<T>` signal and a `Readable<number>` index\n * signal. Use `item.value` to read the current item inside reactive expressions:\n *\n * ```ts\n * html`${each(items, (item) => item.id, (item) => html`<li>${() => item.value.name}</li>`)}`\n * ```\n *\n * **Plain array:** when a plain `T[]` is passed (not a signal or getter), it is\n * treated as a one-time static render. Mutations to the original array are not\n * tracked. Pass a `Signal<T[]>` or `() => T[]` for reactive lists.\n *\n * **Optional fallback:** the fourth argument renders when the list is empty.\n *\n * **Key choice:** pass a stable item identifier (e.g. `item.id`), never the\n * array index: an index-based key reassigns to a different item whenever the\n * list is reordered or an item is inserted/removed before it, causing full\n * item teardown/recreation instead of the in-place update `each()` is built\n * for.\n *\n * **Duplicate keys:** a reconciliation failure (e.g. duplicate keys, see `eachDuplicateKey`)\n * does not throw past this function: an uncaught exception inside the reactive effect that\n * drives `each()` would risk corrupting unrelated effects scheduled in the same update batch.\n * Instead the last valid list is preserved and the failure is reported via the `ore:error` DOM event\n * (see `OreLifecycleError`, phase `'each-reconcile'`) plus a dev-only console log: listen for\n * `ore:error` on `document`/`window` to observe this in every build, including production.\n */\nexport function each<T>(\n list: MaybeReactiveArray<T>,\n keyFn: (item: T, index: number) => string | number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n fallback?: () => HTMLResult,\n): DirectiveResult {\n const listSignal = Array.isArray(list)\n ? signal(list as T[])\n : typeof list === 'function'\n ? computed(list as () => T[])\n : list;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const initialParent = anchor.parentNode;\n\n invariant(initialParent, 'each() anchor comment has no parent node');\n\n const endMarker = document.createComment('each/end');\n\n initialParent.insertBefore(endMarker, anchor.nextSibling);\n\n // The parent is resolved live, never captured: a nested each()'s anchor and\n // endMarker sit inside an outer each() item's range and move with it, so\n // every insertion must target wherever the endMarker currently lives.\n const container = (): ParentNode | null => endMarker.parentNode;\n\n let itemsMap = new Map<ItemKey, ItemEntry<T>>();\n let itemsOrdered: ItemEntry<T>[] = [];\n let fallbackNodes: Node[] | null = null;\n let fallbackCleanups: (() => void)[] = [];\n\n const mountFallback = (): void => {\n if (!fallback) return;\n\n const parent = container();\n\n if (!parent) return;\n\n const result = fallback();\n\n fallbackNodes = result.mount(parent, endMarker, (fn) => fallbackCleanups.push(fn));\n };\n\n const clearFallback = (): void => {\n if (fallbackNodes) {\n runAll(fallbackCleanups);\n removeNodes(fallbackNodes);\n fallbackNodes = null;\n fallbackCleanups = [];\n }\n };\n\n const sub = rawEffect(() => {\n const nextList = listSignal.value ?? [];\n\n if (nextList.length === 0) {\n for (const entry of untrack(() => itemsOrdered)) removeItem(entry);\n itemsMap = new Map();\n itemsOrdered = [];\n\n if (!fallbackNodes) untrack(mountFallback);\n\n return;\n }\n\n let nextKeys: ItemKey[];\n\n try {\n nextKeys = untrack(() => collectKeys(nextList, keyFn));\n } catch (error) {\n reportEachError(error, anchor);\n return;\n }\n\n clearFallback();\n\n const parent = container();\n\n if (!parent) return;\n\n const createdThisPass: ItemEntry<T>[] = [];\n\n try {\n itemsOrdered = untrack(() =>\n reconcileItems(itemsMap, nextList, nextKeys, render, parent, endMarker, createdThisPass, itemsOrdered),\n );\n } catch (error) {\n // Undo this pass instead of tearing the list down: entries created before the\n // throw are removed, everything that was on screen stays exactly as it was.\n reportEachError(error, anchor);\n untrack(() => rollbackCreated(itemsMap, createdThisPass));\n }\n });\n\n registerCleanup(() => sub.dispose());\n registerCleanup(() => {\n clearFallback();\n for (const entry of itemsOrdered) removeItem(entry);\n endMarker.remove();\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -7,6 +7,11 @@ type MaybeReactiveArray<T> = MaybeReactive<T[]>;
|
|
|
7
7
|
*
|
|
8
8
|
* Each item is rendered by the provided render function.
|
|
9
9
|
* Items are reused by key when the list changes; only stale items are destroyed.
|
|
10
|
+
* Reused items keep their DOM nodes: when the surviving items stay in the same
|
|
11
|
+
* relative order (prepend, append, interleaved inserts, content replacement),
|
|
12
|
+
* no reused node is ever moved, so their CSS transitions, animations, and any
|
|
13
|
+
* nested component state survive the update untouched. Only genuine reorders
|
|
14
|
+
* move nodes.
|
|
10
15
|
*
|
|
11
16
|
* The render function receives a `Readable<T>` signal and a `Readable<number>` index
|
|
12
17
|
* signal. Use `item.value` to read the current item inside reactive expressions:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"each.d.ts","sourceRoot":"","sources":["../../src/directives/each.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAMd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,cAAc,CAAC;AAIvE,KAAK,kBAAkB,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"each.d.ts","sourceRoot":"","sources":["../../src/directives/each.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAMd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,cAAc,CAAC;AAIvE,KAAK,kBAAkB,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;AAkPhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,IAAI,CAAC,CAAC,EACpB,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAC3B,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,EAClD,MAAM,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,UAAU,EAClE,QAAQ,CAAC,EAAE,MAAM,UAAU,GAC1B,eAAe,CAgGjB"}
|
package/dist/directives/each.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ORE_ERRORS as e,OreApiError as t,OreLifecycleError as n,invariant as r,reportRuntimeError as i}from"../errors.js";import{createDirectiveResult as a}from"../template/result.js";import{removeNodes as o,runAll as s}from"../utils/dom.js";import{batch as c,computed as l,createScope as u,effect as d,signal as f,untrack as p}from"@vielzeug/ripple";var m=(e,t,n,r
|
|
1
|
+
import{ORE_ERRORS as e,OreApiError as t,OreLifecycleError as n,invariant as r,reportRuntimeError as i}from"../errors.js";import{createDirectiveResult as a}from"../template/result.js";import{removeNodes as o,runAll as s}from"../utils/dom.js";import{batch as c,computed as l,createScope as u,effect as d,signal as f,untrack as p}from"@vielzeug/ripple";var m=(e,t,n,r)=>{let i=f(e),a=f(t),o=u(),s=[],c=[],l=document.createComment(`each/item-start`);r.appendChild(l),o.run(()=>{c=n(i,a).mount(r,null,e=>s.push(e))});let d=document.createComment(`each/item`);return r.appendChild(d),c=[l,...c,d],{cleanups:s,data:i,index:a,nodes:c,scope:o}},h=e=>{e.scope.dispose(),s(e.cleanups),o(e.nodes)},g=(e,t)=>{let n=/* @__PURE__ */ new Map;for(let e=0;e<t.length;e++)n.set(t[e],e);let r=-1;for(let t of e){let e=n.get(t);if(e!==void 0){if(e<r)return!1;r=e}}return!0},_=(e,t,n,i,a,o,s,l)=>{let u=document.createDocumentFragment();for(let r=0;r<t.length;r++){let a=n[r];if(e.has(a))continue;let o=p(()=>m(t[r],r,i,u));s.push(o),e.set(a,o)}let d=[];c(()=>{for(let i=0;i<t.length;i++){let a=e.get(n[i]);r(a,`each() reconcile phase 2 lost an entry created in phase 1`),a.data.value=t[i],a.index.value=i,d.push(a)}});let f=g(d,l),_=new Set(s),v=o;for(let e=d.length-1;e>=0;e--){let t=d[e],n=t.nodes[0],r=t.nodes[t.nodes.length-1];if((!f||_.has(t))&&r!==v.previousSibling){let e=n;for(;e;){let t=e===r?null:e.nextSibling;if(a.insertBefore(e,v),e===r)break;e=t}}v=n}let y=new Set(n);for(let[t,n]of e)y.has(t)||(h(n),e.delete(t));return d},v=(e,t)=>{for(let n of t){h(n);for(let[t,r]of e)if(r===n){e.delete(t);break}}},y=(n,r)=>{let i=[],a=/* @__PURE__ */ new Set;for(let o=0;o<n.length;o++){let s=r(n[o],o);if(a.has(s))throw new t(e.eachDuplicateKey(String(s),o));a.add(s),i.push(s)}return i},b=(e,t)=>{let r=e instanceof Error?e:Error(String(e));i(new n(`each() failed to reconcile a list update: ${r.message}`,{cause:r,component:`each()`,phase:`each-reconcile`}),t)};function x(e,t,n,i){let c=Array.isArray(e)?f(e):typeof e==`function`?l(e):e;return a((e,a)=>{let l=e.parentNode;r(l,`each() anchor comment has no parent node`);let u=document.createComment(`each/end`);l.insertBefore(u,e.nextSibling);let f=()=>u.parentNode,m=/* @__PURE__ */ new Map,g=[],x=null,S=[],C=()=>{if(!i)return;let e=f();e&&(x=i().mount(e,u,e=>S.push(e)))},w=()=>{x&&(s(S),o(x),x=null,S=[])},T=d(()=>{let r=c.value??[];if(r.length===0){for(let e of p(()=>g))h(e);m=/* @__PURE__ */ new Map,g=[],x||p(C);return}let i;try{i=p(()=>y(r,t))}catch(t){b(t,e);return}w();let a=f();if(!a)return;let o=[];try{g=p(()=>_(m,r,i,n,a,u,o,g))}catch(t){b(t,e),p(()=>v(m,o))}});a(()=>T.dispose()),a(()=>{w();for(let e of g)h(e);u.remove()})})}export{x as each};
|
|
2
2
|
//# sourceMappingURL=each.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"each.js","names":["rawEffect","createItem","dataSignal","signal","item","indexSignal","index","scope","createScope","cleanups","nodes","render","parent","insertBefore","fn","removeItem","entry","runAll","removeNodes","reconcileItems","i","next","key","nextKeys","itemsMap","untrack","endMarker","createdThisPass","nextOrdered","batch","invariant","cursor","j","firstNode","lastNode","node","nextKeySet","rollbackCreated","created","candidate","collectKeys","keys","seen","keyFn","OreApiError","ORE_ERRORS","reportEachError","cause","error","reportRuntimeError","OreLifecycleError","anchor","each","listSignal","list","computed","createDirectiveResult","itemsOrdered","fallbackNodes","fallbackCleanups","mountFallback","fallback","clearFallback","sub","effect","nextList","registerCleanup"],"sources":["../../src/directives/each.ts"],"sourcesContent":["import {\n batch,\n computed,\n createScope,\n type Readable,\n effect as rawEffect,\n type Scope,\n type Signal,\n signal,\n untrack,\n} from '@vielzeug/ripple';\n\nimport { invariant, ORE_ERRORS, OreApiError, OreLifecycleError, reportRuntimeError } from '../errors';\nimport { createDirectiveResult, type DirectiveResult, type HTMLResult } from '../template/result';\nimport { type MaybeReactive, removeNodes, runAll } from '../utils/dom';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype MaybeReactiveArray<T> = MaybeReactive<T[]>;\ntype ItemKey = number | string;\n\ntype ItemEntry<T> = {\n cleanups: (() => void)[];\n data: Signal<T>;\n index: Signal<number>;\n nodes: Node[];\n scope: Scope;\n};\n\n// ─── Item lifecycle ───────────────────────────────────────────────────────────\n\nconst createItem = <T>(\n item: T,\n index: number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n insertBefore: Node,\n): ItemEntry<T> => {\n const dataSignal: Signal<T> = signal(item);\n const indexSignal: Signal<number> = signal(index);\n const scope = createScope();\n const cleanups: (() => void)[] = [];\n let nodes: Node[] = [];\n\n scope.run(() => {\n const result = render(dataSignal, indexSignal);\n\n nodes = result.mount(parent, insertBefore, (fn) => cleanups.push(fn));\n });\n\n return { cleanups, data: dataSignal, index: indexSignal, nodes, scope };\n};\n\nconst removeItem = <T>(entry: ItemEntry<T>): void => {\n entry.scope.dispose();\n runAll(entry.cleanups);\n removeNodes(entry.nodes);\n};\n\n// ─── Reconciler ───────────────────────────────────────────────────────────────\n\n/**\n * Reconciles the live item map against the next array. The pass is transactional:\n * new entries are created first, existing entries are updated in-place via signal\n * writes, and stale entries are destroyed only after the whole pass has succeeded.\n * If creation throws (user render code), the caller rolls back the entries created\n * during this pass and the previous list state survives: the last good render stays\n * on screen. Returns the ordered list of entries matching nextList.\n */\nconst reconcileItems = <T>(\n itemsMap: Map<ItemKey, ItemEntry<T>>,\n next: T[],\n nextKeys: ItemKey[],\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n endMarker: Node,\n createdThisPass: ItemEntry<T>[],\n): ItemEntry<T>[] => {\n // Phase 1: create every missing entry. This is the only step that can throw\n // (user render code), and it records what it created so a failure is undoable.\n for (let i = 0; i < next.length; i++) {\n const key = nextKeys[i];\n\n if (itemsMap.has(key)) continue;\n\n const entry = untrack(() => createItem(next[i], i, render, parent, endMarker));\n\n createdThisPass.push(entry);\n itemsMap.set(key, entry);\n }\n\n // Phase 2: ordered list + in-place updates for reused entries (signal writes).\n const nextOrdered: ItemEntry<T>[] = [];\n\n batch(() => {\n for (let i = 0; i < next.length; i++) {\n const entry = itemsMap.get(nextKeys[i]);\n\n invariant(entry, 'each() reconcile phase 2 lost an entry created in phase 1');\n\n entry.data.value = next[i];\n entry.index.value = i;\n nextOrdered.push(entry);\n }\n });\n\n // DOM ordering: right-to-left pass: move any item not already adjacent to cursor.\n // An item is in place when its last node directly precedes the cursor; items may\n // span several nodes (whitespace text around an element), so compare the last one.\n // Moving an already-placed item would disconnect and reconnect its custom elements.\n // O(n) DOM operations in the worst case; optimal for the typical small list sizes\n // encountered in UI components (tabs, options, menu items).\n let cursor: Node = endMarker;\n\n for (let j = nextOrdered.length - 1; j >= 0; j--) {\n const entry = nextOrdered[j];\n const firstNode = entry.nodes[0];\n const lastNode = entry.nodes[entry.nodes.length - 1];\n\n if (firstNode && lastNode !== cursor.previousSibling) {\n for (const node of entry.nodes) parent.insertBefore(node, cursor);\n }\n\n cursor = firstNode ?? cursor;\n }\n\n // Phase 3: commit: destroy stale entries only now that the pass has succeeded.\n const nextKeySet = new Set(nextKeys);\n\n for (const [key, entry] of itemsMap) {\n if (!nextKeySet.has(key)) {\n removeItem(entry);\n itemsMap.delete(key);\n }\n }\n\n return nextOrdered;\n};\n\n/** Undo a failed reconcile pass: destroy entries created during it, restoring the last good list. */\nconst rollbackCreated = <T>(itemsMap: Map<ItemKey, ItemEntry<T>>, created: ItemEntry<T>[]): void => {\n for (const entry of created) {\n removeItem(entry);\n\n for (const [key, candidate] of itemsMap) {\n if (candidate === entry) {\n itemsMap.delete(key);\n break;\n }\n }\n }\n};\n\nconst collectKeys = <T>(next: T[], keyFn: (item: T, index: number) => ItemKey): ItemKey[] => {\n const keys: ItemKey[] = [];\n const seen = new Set<ItemKey>();\n\n for (let i = 0; i < next.length; i++) {\n const key = keyFn(next[i], i);\n\n if (seen.has(key)) throw new OreApiError(ORE_ERRORS.eachDuplicateKey(String(key), i));\n\n seen.add(key);\n keys.push(key);\n }\n\n return keys;\n};\n\nconst reportEachError = (error: unknown, anchor: Comment): void => {\n const cause = error instanceof Error ? error : new Error(String(error));\n\n reportRuntimeError(\n new OreLifecycleError(`each() failed to reconcile a list update: ${cause.message}`, {\n cause,\n component: 'each()',\n phase: 'each-reconcile',\n }),\n anchor,\n );\n};\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Renders a keyed list of items as a `DirectiveResult`.\n *\n * Each item is rendered by the provided render function.\n * Items are reused by key when the list changes; only stale items are destroyed.\n *\n * The render function receives a `Readable<T>` signal and a `Readable<number>` index\n * signal. Use `item.value` to read the current item inside reactive expressions:\n *\n * ```ts\n * html`${each(items, (item) => item.id, (item) => html`<li>${() => item.value.name}</li>`)}`\n * ```\n *\n * **Plain array:** when a plain `T[]` is passed (not a signal or getter), it is\n * treated as a one-time static render. Mutations to the original array are not\n * tracked. Pass a `Signal<T[]>` or `() => T[]` for reactive lists.\n *\n * **Optional fallback:** the fourth argument renders when the list is empty.\n *\n * **Key choice:** pass a stable item identifier (e.g. `item.id`), never the\n * array index: an index-based key reassigns to a different item whenever the\n * list is reordered or an item is inserted/removed before it, causing full\n * item teardown/recreation instead of the in-place update `each()` is built\n * for.\n *\n * **Duplicate keys:** a reconciliation failure (e.g. duplicate keys, see `eachDuplicateKey`)\n * does not throw past this function: an uncaught exception inside the reactive effect that\n * drives `each()` would risk corrupting unrelated effects scheduled in the same update batch.\n * Instead the last valid list is preserved and the failure is reported via the `ore:error` DOM event\n * (see `OreLifecycleError`, phase `'each-reconcile'`) plus a dev-only console log: listen for\n * `ore:error` on `document`/`window` to observe this in every build, including production.\n */\nexport function each<T>(\n list: MaybeReactiveArray<T>,\n keyFn: (item: T, index: number) => string | number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n fallback?: () => HTMLResult,\n): DirectiveResult {\n const listSignal = Array.isArray(list)\n ? signal(list as T[])\n : typeof list === 'function'\n ? computed(list as () => T[])\n : list;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const parent = anchor.parentNode;\n\n invariant(parent, 'each() anchor comment has no parent node');\n\n const endMarker = document.createComment('each/end');\n\n parent.insertBefore(endMarker, anchor.nextSibling);\n\n let itemsMap = new Map<ItemKey, ItemEntry<T>>();\n let itemsOrdered: ItemEntry<T>[] = [];\n let fallbackNodes: Node[] | null = null;\n let fallbackCleanups: (() => void)[] = [];\n\n const mountFallback = (): void => {\n if (!fallback) return;\n\n const result = fallback();\n\n fallbackNodes = result.mount(parent, endMarker, (fn) => fallbackCleanups.push(fn));\n };\n\n const clearFallback = (): void => {\n if (fallbackNodes) {\n runAll(fallbackCleanups);\n removeNodes(fallbackNodes);\n fallbackNodes = null;\n fallbackCleanups = [];\n }\n };\n\n const sub = rawEffect(() => {\n const nextList = listSignal.value ?? [];\n\n if (nextList.length === 0) {\n for (const entry of untrack(() => itemsOrdered)) removeItem(entry);\n itemsMap = new Map();\n itemsOrdered = [];\n\n if (!fallbackNodes) untrack(mountFallback);\n\n return;\n }\n\n let nextKeys: ItemKey[];\n\n try {\n nextKeys = untrack(() => collectKeys(nextList, keyFn));\n } catch (error) {\n reportEachError(error, anchor);\n return;\n }\n\n clearFallback();\n\n const createdThisPass: ItemEntry<T>[] = [];\n\n try {\n itemsOrdered = untrack(() =>\n reconcileItems(itemsMap, nextList, nextKeys, render, parent, endMarker, createdThisPass),\n );\n } catch (error) {\n // Undo this pass instead of tearing the list down: entries created before the\n // throw are removed, everything that was on screen stays exactly as it was.\n reportEachError(error, anchor);\n untrack(() => rollbackCreated(itemsMap, createdThisPass));\n }\n });\n\n registerCleanup(() => sub.dispose());\n registerCleanup(() => {\n clearFallback();\n for (const entry of itemsOrdered) removeItem(entry);\n endMarker.remove();\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"each.js","names":["rawEffect","createItem","dataSignal","signal","item","indexSignal","index","scope","createScope","cleanups","nodes","rangeStart","staging","render","fn","rangeEnd","removeItem","entry","runAll","removeNodes","reusedEntriesInOrder","previousPosition","i","itemsOrdered","lastPosition","nextOrdered","position","reconcileItems","next","key","nextKeys","itemsMap","untrack","createdThisPass","batch","invariant","reusedInOrder","created","cursor","endMarker","j","firstNode","node","parent","nextKeySet","rollbackCreated","candidate","collectKeys","keys","seen","keyFn","OreApiError","ORE_ERRORS","reportEachError","cause","error","reportRuntimeError","OreLifecycleError","anchor","each","listSignal","list","computed","createDirectiveResult","initialParent","container","fallbackNodes","fallbackCleanups","mountFallback","fallback","clearFallback","sub","effect","nextList","registerCleanup"],"sources":["../../src/directives/each.ts"],"sourcesContent":["import {\n batch,\n computed,\n createScope,\n type Readable,\n effect as rawEffect,\n type Scope,\n type Signal,\n signal,\n untrack,\n} from '@vielzeug/ripple';\n\nimport { invariant, ORE_ERRORS, OreApiError, OreLifecycleError, reportRuntimeError } from '../errors';\nimport { createDirectiveResult, type DirectiveResult, type HTMLResult } from '../template/result';\nimport { type MaybeReactive, removeNodes, runAll } from '../utils/dom';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype MaybeReactiveArray<T> = MaybeReactive<T[]>;\ntype ItemKey = number | string;\n\n/**\n * A rendered list item. `nodes` is the item's contiguous sibling range,\n * bracketed by `each/item-start` and `each/item` sentinel comments: the\n * sentinels wrap the template's own nodes, and every reactive directive inside\n * the item (`when()`, nested `each()`, html bindings) inserts and re-inserts\n * its content as siblings relative to nodes between the sentinels — before or\n * after its own anchor, but never past a sentinel. Relocating the item\n * therefore means moving the whole bracketed range, node by node: never a\n * bare subset.\n */\ntype ItemEntry<T> = {\n cleanups: (() => void)[];\n data: Signal<T>;\n index: Signal<number>;\n nodes: Node[];\n scope: Scope;\n};\n\n// ─── Item lifecycle ───────────────────────────────────────────────────────────\n\n/**\n * Mounts the item into `staging` (a detached fragment), bracketing its node\n * range with the start and end sentinels. Synchronous directive content mounts\n * between the sentinels (the end sentinel only exists once the whole template\n * is mounted), so the range is complete the moment this returns.\n */\nconst createItem = <T>(\n item: T,\n index: number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n staging: DocumentFragment,\n): ItemEntry<T> => {\n const dataSignal: Signal<T> = signal(item);\n const indexSignal: Signal<number> = signal(index);\n const scope = createScope();\n const cleanups: (() => void)[] = [];\n let nodes: Node[] = [];\n\n const rangeStart = document.createComment('each/item-start');\n\n staging.appendChild(rangeStart);\n\n scope.run(() => {\n const result = render(dataSignal, indexSignal);\n\n nodes = result.mount(staging, null, (fn) => cleanups.push(fn));\n });\n\n const rangeEnd = document.createComment('each/item');\n\n staging.appendChild(rangeEnd);\n nodes = [rangeStart, ...nodes, rangeEnd];\n\n return { cleanups, data: dataSignal, index: indexSignal, nodes, scope };\n};\n\nconst removeItem = <T>(entry: ItemEntry<T>): void => {\n entry.scope.dispose();\n runAll(entry.cleanups);\n removeNodes(entry.nodes);\n};\n\n// ─── Reconciler ───────────────────────────────────────────────────────────────\n\n/**\n * True when reused entries appear in the next list in the same relative order\n * they hold in the DOM (`itemsOrdered`). Prepends, appends, interleaved\n * inserts, and content replacement all satisfy this; only genuine reorders\n * (or removes that flip survivor order) do not.\n */\nconst reusedEntriesInOrder = <T>(nextOrdered: ItemEntry<T>[], itemsOrdered: ItemEntry<T>[]): boolean => {\n const previousPosition = new Map<ItemEntry<T>, number>();\n\n for (let i = 0; i < itemsOrdered.length; i++) previousPosition.set(itemsOrdered[i], i);\n\n let lastPosition = -1;\n\n for (const entry of nextOrdered) {\n const position = previousPosition.get(entry);\n\n if (position === undefined) continue;\n\n if (position < lastPosition) return false;\n\n lastPosition = position;\n }\n\n return true;\n};\n\n/**\n * Reconciles the live item map against the next array. The pass is transactional:\n * new entries are created first, existing entries are updated in-place via signal\n * writes, and stale entries are destroyed only after the whole pass has succeeded.\n * If creation throws (user render code), the caller rolls back the entries created\n * during this pass and the previous list state survives: the last good render stays\n * on screen. Returns the ordered list of entries matching nextList.\n */\nconst reconcileItems = <T>(\n itemsMap: Map<ItemKey, ItemEntry<T>>,\n next: T[],\n nextKeys: ItemKey[],\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n parent: ParentNode,\n endMarker: Node,\n createdThisPass: ItemEntry<T>[],\n itemsOrdered: ItemEntry<T>[],\n): ItemEntry<T>[] => {\n // Phase 1: create every missing entry. This is the only step that can throw\n // (user render code), and it records what it created so a failure is undoable.\n // New entries mount into a detached staging fragment, not the live parent: an\n // entry not already at its final position would otherwise connect at the end,\n // then disconnect again when the ordering pass relocates it (double\n // setup/teardown of any custom elements it contains). Staged nodes connect\n // exactly once, when the ordering pass moves them into place.\n const staging = document.createDocumentFragment();\n\n for (let i = 0; i < next.length; i++) {\n const key = nextKeys[i];\n\n if (itemsMap.has(key)) continue;\n\n const entry = untrack(() => createItem(next[i], i, render, staging));\n\n createdThisPass.push(entry);\n itemsMap.set(key, entry);\n }\n\n // Phase 2: ordered list + in-place updates for reused entries (signal writes).\n const nextOrdered: ItemEntry<T>[] = [];\n\n batch(() => {\n for (let i = 0; i < next.length; i++) {\n const entry = itemsMap.get(nextKeys[i]);\n\n invariant(entry, 'each() reconcile phase 2 lost an entry created in phase 1');\n\n entry.data.value = next[i];\n entry.index.value = i;\n nextOrdered.push(entry);\n }\n });\n\n // DOM ordering: one right-to-left pass with a cursor. An entry is in place\n // when its range sentinel directly precedes the cursor. Moving an entry\n // disconnects and reconnects its custom elements, so moves are kept minimal:\n // when reused entries kept their relative order (the common case: prepend,\n // append, interleaved inserts, content replacement), reused ranges are never\n // moved at all and only entries created this pass are relocated next to their\n // nearest reused neighbour. Genuine reorders fall back to moving any range the\n // cursor cannot reach. A move relocates the whole contiguous range up to and\n // including the sentinel so content inserted by directives inside the item\n // travels with it.\n const reusedInOrder = reusedEntriesInOrder(nextOrdered, itemsOrdered);\n const created = new Set<ItemEntry<T>>(createdThisPass);\n\n let cursor: Node = endMarker;\n\n for (let j = nextOrdered.length - 1; j >= 0; j--) {\n const entry = nextOrdered[j];\n const firstNode = entry.nodes[0];\n const rangeEnd = entry.nodes[entry.nodes.length - 1];\n\n // Staged nodes never satisfy the adjacency test (they live in the fragment),\n // so created entries are always moved to their final position.\n if ((!reusedInOrder || created.has(entry)) && rangeEnd !== cursor.previousSibling) {\n let node: Node | null = firstNode;\n\n while (node) {\n const next: Node | null = node === rangeEnd ? null : node.nextSibling;\n\n parent.insertBefore(node, cursor);\n\n if (node === rangeEnd) break;\n node = next;\n }\n }\n\n cursor = firstNode;\n }\n\n // Phase 3: commit: destroy stale entries only now that the pass has succeeded.\n const nextKeySet = new Set(nextKeys);\n\n for (const [key, entry] of itemsMap) {\n if (!nextKeySet.has(key)) {\n removeItem(entry);\n itemsMap.delete(key);\n }\n }\n\n return nextOrdered;\n};\n\n/** Undo a failed reconcile pass: destroy entries created during it, restoring the last good list. */\nconst rollbackCreated = <T>(itemsMap: Map<ItemKey, ItemEntry<T>>, created: ItemEntry<T>[]): void => {\n for (const entry of created) {\n removeItem(entry);\n\n for (const [key, candidate] of itemsMap) {\n if (candidate === entry) {\n itemsMap.delete(key);\n break;\n }\n }\n }\n};\n\nconst collectKeys = <T>(next: T[], keyFn: (item: T, index: number) => ItemKey): ItemKey[] => {\n const keys: ItemKey[] = [];\n const seen = new Set<ItemKey>();\n\n for (let i = 0; i < next.length; i++) {\n const key = keyFn(next[i], i);\n\n if (seen.has(key)) throw new OreApiError(ORE_ERRORS.eachDuplicateKey(String(key), i));\n\n seen.add(key);\n keys.push(key);\n }\n\n return keys;\n};\n\nconst reportEachError = (error: unknown, anchor: Comment): void => {\n const cause = error instanceof Error ? error : new Error(String(error));\n\n reportRuntimeError(\n new OreLifecycleError(`each() failed to reconcile a list update: ${cause.message}`, {\n cause,\n component: 'each()',\n phase: 'each-reconcile',\n }),\n anchor,\n );\n};\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Renders a keyed list of items as a `DirectiveResult`.\n *\n * Each item is rendered by the provided render function.\n * Items are reused by key when the list changes; only stale items are destroyed.\n * Reused items keep their DOM nodes: when the surviving items stay in the same\n * relative order (prepend, append, interleaved inserts, content replacement),\n * no reused node is ever moved, so their CSS transitions, animations, and any\n * nested component state survive the update untouched. Only genuine reorders\n * move nodes.\n *\n * The render function receives a `Readable<T>` signal and a `Readable<number>` index\n * signal. Use `item.value` to read the current item inside reactive expressions:\n *\n * ```ts\n * html`${each(items, (item) => item.id, (item) => html`<li>${() => item.value.name}</li>`)}`\n * ```\n *\n * **Plain array:** when a plain `T[]` is passed (not a signal or getter), it is\n * treated as a one-time static render. Mutations to the original array are not\n * tracked. Pass a `Signal<T[]>` or `() => T[]` for reactive lists.\n *\n * **Optional fallback:** the fourth argument renders when the list is empty.\n *\n * **Key choice:** pass a stable item identifier (e.g. `item.id`), never the\n * array index: an index-based key reassigns to a different item whenever the\n * list is reordered or an item is inserted/removed before it, causing full\n * item teardown/recreation instead of the in-place update `each()` is built\n * for.\n *\n * **Duplicate keys:** a reconciliation failure (e.g. duplicate keys, see `eachDuplicateKey`)\n * does not throw past this function: an uncaught exception inside the reactive effect that\n * drives `each()` would risk corrupting unrelated effects scheduled in the same update batch.\n * Instead the last valid list is preserved and the failure is reported via the `ore:error` DOM event\n * (see `OreLifecycleError`, phase `'each-reconcile'`) plus a dev-only console log: listen for\n * `ore:error` on `document`/`window` to observe this in every build, including production.\n */\nexport function each<T>(\n list: MaybeReactiveArray<T>,\n keyFn: (item: T, index: number) => string | number,\n render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n fallback?: () => HTMLResult,\n): DirectiveResult {\n const listSignal = Array.isArray(list)\n ? signal(list as T[])\n : typeof list === 'function'\n ? computed(list as () => T[])\n : list;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const initialParent = anchor.parentNode;\n\n invariant(initialParent, 'each() anchor comment has no parent node');\n\n const endMarker = document.createComment('each/end');\n\n initialParent.insertBefore(endMarker, anchor.nextSibling);\n\n // The parent is resolved live, never captured: a nested each()'s anchor and\n // endMarker sit inside an outer each() item's range and move with it, so\n // every insertion must target wherever the endMarker currently lives.\n const container = (): ParentNode | null => endMarker.parentNode;\n\n let itemsMap = new Map<ItemKey, ItemEntry<T>>();\n let itemsOrdered: ItemEntry<T>[] = [];\n let fallbackNodes: Node[] | null = null;\n let fallbackCleanups: (() => void)[] = [];\n\n const mountFallback = (): void => {\n if (!fallback) return;\n\n const parent = container();\n\n if (!parent) return;\n\n const result = fallback();\n\n fallbackNodes = result.mount(parent, endMarker, (fn) => fallbackCleanups.push(fn));\n };\n\n const clearFallback = (): void => {\n if (fallbackNodes) {\n runAll(fallbackCleanups);\n removeNodes(fallbackNodes);\n fallbackNodes = null;\n fallbackCleanups = [];\n }\n };\n\n const sub = rawEffect(() => {\n const nextList = listSignal.value ?? [];\n\n if (nextList.length === 0) {\n for (const entry of untrack(() => itemsOrdered)) removeItem(entry);\n itemsMap = new Map();\n itemsOrdered = [];\n\n if (!fallbackNodes) untrack(mountFallback);\n\n return;\n }\n\n let nextKeys: ItemKey[];\n\n try {\n nextKeys = untrack(() => collectKeys(nextList, keyFn));\n } catch (error) {\n reportEachError(error, anchor);\n return;\n }\n\n clearFallback();\n\n const parent = container();\n\n if (!parent) return;\n\n const createdThisPass: ItemEntry<T>[] = [];\n\n try {\n itemsOrdered = untrack(() =>\n reconcileItems(itemsMap, nextList, nextKeys, render, parent, endMarker, createdThisPass, itemsOrdered),\n );\n } catch (error) {\n // Undo this pass instead of tearing the list down: entries created before the\n // throw are removed, everything that was on screen stays exactly as it was.\n reportEachError(error, anchor);\n untrack(() => rollbackCreated(itemsMap, createdThisPass));\n }\n });\n\n registerCleanup(() => sub.dispose());\n registerCleanup(() => {\n clearFallback();\n for (const entry of itemsOrdered) removeItem(entry);\n endMarker.remove();\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=(e,t,n)=>{let r=document.createElement(`template`);r.innerHTML=e;let i=Array.from(r.content.cloneNode(!0).childNodes);for(let e of i)t.insertBefore(e,n);return i};function a(a){let o=typeof a==`function`?(0,r.computed)(a):(0,r.isReactive)(a)?a:null;return t.createDirectiveResult((t,s)=>{let c=t.parentNode;e.invariant(c,`unsafeHtml() anchor comment has no parent node`);let l=document.createComment(`unsafe-html/end`);if(c.insertBefore(l,t.nextSibling),o){let e=n.createReplaceableSlot(),t=(0,r.effect)(()=>{e.clear()
|
|
1
|
+
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=(e,t,n)=>{let r=document.createElement(`template`);r.innerHTML=e;let i=Array.from(r.content.cloneNode(!0).childNodes);for(let e of i)t.insertBefore(e,n);return i};function a(a){let o=typeof a==`function`?(0,r.computed)(a):(0,r.isReactive)(a)?a:null;return t.createDirectiveResult((t,s)=>{let c=t.parentNode;e.invariant(c,`unsafeHtml() anchor comment has no parent node`);let l=document.createComment(`unsafe-html/end`);if(c.insertBefore(l,t.nextSibling),o){let e=n.createReplaceableSlot(),t=(0,r.effect)(()=>{e.clear();let t=l.parentNode;t&&e.setNodes(i(o.value,t,l))});s(()=>t.dispose()),s(()=>{e.clear(),l.remove()})}else i(a,c,l),s(()=>l.remove())})}exports.unsafeHtml=a;
|
|
2
2
|
//# sourceMappingURL=unsafe-html.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsafe-html.cjs","names":["computed","isReactive","createDirectiveResult","createReplaceableSlot","rawEffect","require_errors","require_result","require_dom","_vielzeug_ripple","parseHtml","tpl","html","nodes","node","parent","insertBefore","unsafeHtml","src","value","anchor","endMarker","slot","stop","registerCleanup"],"sources":["../../src/directives/unsafe-html.ts"],"sourcesContent":["import { computed, isReactive, type Readable, effect as rawEffect, type Signal } from '@vielzeug/ripple';\n\nimport { invariant } from '../errors';\nimport { createDirectiveResult, type DirectiveResult } from '../template/result';\nimport { createReplaceableSlot } from '../utils/dom';\n\nconst parseHtml = (html: string, parent: ParentNode, insertBefore: Node): Node[] => {\n const tpl = document.createElement('template');\n\n tpl.innerHTML = html;\n\n const nodes = Array.from(tpl.content.cloneNode(true).childNodes);\n\n for (const node of nodes) parent.insertBefore(node, insertBefore);\n\n return nodes;\n};\n\n/**\n * Renders HTML without escaping as a DirectiveResult.\n *\n * This is intentionally named `unsafeHtml`: sanitization is an application\n * boundary, not mutable process-wide framework configuration. Sanitize\n * untrusted content before passing it here.\n *\n * Supports static strings, signals, and getter functions `() => string`.\n * When reactive, the DOM is updated in-place whenever the value changes.\n */\nexport function unsafeHtml(value: (() => string) | string | Signal<string> | Readable<string>): DirectiveResult {\n const isFn = typeof value === 'function';\n const src: Readable<string> | null = isFn ? computed(value as () => string) : isReactive(value) ? value : null;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const
|
|
1
|
+
{"version":3,"file":"unsafe-html.cjs","names":["computed","isReactive","createDirectiveResult","createReplaceableSlot","rawEffect","require_errors","require_result","require_dom","_vielzeug_ripple","parseHtml","tpl","html","nodes","node","parent","insertBefore","unsafeHtml","src","value","initialParent","anchor","endMarker","slot","stop","registerCleanup"],"sources":["../../src/directives/unsafe-html.ts"],"sourcesContent":["import { computed, isReactive, type Readable, effect as rawEffect, type Signal } from '@vielzeug/ripple';\n\nimport { invariant } from '../errors';\nimport { createDirectiveResult, type DirectiveResult } from '../template/result';\nimport { createReplaceableSlot } from '../utils/dom';\n\nconst parseHtml = (html: string, parent: ParentNode, insertBefore: Node): Node[] => {\n const tpl = document.createElement('template');\n\n tpl.innerHTML = html;\n\n const nodes = Array.from(tpl.content.cloneNode(true).childNodes);\n\n for (const node of nodes) parent.insertBefore(node, insertBefore);\n\n return nodes;\n};\n\n/**\n * Renders HTML without escaping as a DirectiveResult.\n *\n * This is intentionally named `unsafeHtml`: sanitization is an application\n * boundary, not mutable process-wide framework configuration. Sanitize\n * untrusted content before passing it here.\n *\n * Supports static strings, signals, and getter functions `() => string`.\n * When reactive, the DOM is updated in-place whenever the value changes.\n */\nexport function unsafeHtml(value: (() => string) | string | Signal<string> | Readable<string>): DirectiveResult {\n const isFn = typeof value === 'function';\n const src: Readable<string> | null = isFn ? computed(value as () => string) : isReactive(value) ? value : null;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const initialParent = anchor.parentNode;\n\n invariant(initialParent, 'unsafeHtml() anchor comment has no parent node');\n\n const endMarker = document.createComment('unsafe-html/end');\n\n initialParent.insertBefore(endMarker, anchor.nextSibling);\n\n if (src) {\n const slot = createReplaceableSlot();\n\n const stop = rawEffect(() => {\n slot.clear();\n\n // Resolved live: an unsafeHtml() inside an each() item moves with the\n // item's range; the mount-time parent would be stale after a reorder.\n const parent = endMarker.parentNode;\n\n if (!parent) return;\n\n slot.setNodes(parseHtml(src.value, parent, endMarker));\n });\n\n registerCleanup(() => stop.dispose());\n registerCleanup(() => {\n slot.clear();\n endMarker.remove();\n });\n } else {\n parseHtml(value as string, initialParent, endMarker);\n\n registerCleanup(() => endMarker.remove());\n }\n });\n}\n"],"mappings":"A,qIAMA,IAAMS,GAAa,EAAc,EAAoB,IAA+B,CAClF,IAAMC,EAAM,SAAS,cAAc,UAAU,EAE7C,EAAI,UAAYC,EAEhB,IAAMC,EAAQ,MAAM,KAAKF,EAAI,QAAQ,UAAU,EAAI,CAAC,CAAC,UAAU,EAE/D,IAAK,IAAMG,KAAQD,EAAOE,EAAO,aAAaD,EAAME,CAAY,EAEhE,OAAOH,CACT,EAYA,SAAgBI,EAAW,EAAqF,CAE9G,IAAMC,EADO,OAAOC,GAAU,YAAA,EACclB,EAAAA,SAAAA,CAASkB,CAAqB,GAAA,EAAIjB,EAAAA,WAAAA,CAAWiB,CAAK,EAAIA,EAAQ,KAE1G,OAAOhB,EAAAA,uBAAuB,EAAQ,IAAoB,CACxD,IAAMiB,EAAgBC,EAAO,WAE7Bf,EAAA,UAAUc,EAAe,gDAAgD,EAEzE,IAAME,EAAY,SAAS,cAAc,iBAAiB,EAI1D,GAFAF,EAAc,aAAaE,EAAWD,EAAO,WAAW,EAEpDH,EAAK,CACP,IAAMK,EAAOnB,EAAAA,sBAAsB,EAE7BoB,GAAA,EAAOnB,EAAAA,OAAAA,KAAgB,CAC3BkB,EAAK,MAAM,EAIX,IAAMR,EAASO,EAAU,WAEpBP,GAELQ,EAAK,SAASb,EAAUQ,EAAI,MAAOH,EAAQO,CAAS,CAAC,CACvD,CAAC,EAEDG,MAAsBD,EAAK,QAAQ,CAAC,EACpCC,MAAsB,CACpBF,EAAK,MAAM,EACXD,EAAU,OAAO,CACnB,CAAC,CACH,MACEZ,EAAUS,EAAiBC,EAAeE,CAAS,EAEnDG,MAAsBH,EAAU,OAAO,CAAC,CAE5C,CAAC,CACH,C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsafe-html.d.ts","sourceRoot":"","sources":["../../src/directives/unsafe-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,QAAQ,EAAuB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGzG,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAejF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"unsafe-html.d.ts","sourceRoot":"","sources":["../../src/directives/unsafe-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,QAAQ,EAAuB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGzG,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAejF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,eAAe,CAuC9G"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{invariant as e}from"../errors.js";import{createDirectiveResult as t}from"../template/result.js";import{createReplaceableSlot as n}from"../utils/dom.js";import{computed as r,effect as i,isReactive as a}from"@vielzeug/ripple";var o=(e,t,n)=>{let r=document.createElement(`template`);r.innerHTML=e;let i=Array.from(r.content.cloneNode(!0).childNodes);for(let e of i)t.insertBefore(e,n);return i};function s(s){let c=typeof s==`function`?r(s):a(s)?s:null;return t((t,r)=>{let a=t.parentNode;e(a,`unsafeHtml() anchor comment has no parent node`);let l=document.createComment(`unsafe-html/end`);if(a.insertBefore(l,t.nextSibling),c){let e=n(),t=i(()=>{e.clear()
|
|
1
|
+
import{invariant as e}from"../errors.js";import{createDirectiveResult as t}from"../template/result.js";import{createReplaceableSlot as n}from"../utils/dom.js";import{computed as r,effect as i,isReactive as a}from"@vielzeug/ripple";var o=(e,t,n)=>{let r=document.createElement(`template`);r.innerHTML=e;let i=Array.from(r.content.cloneNode(!0).childNodes);for(let e of i)t.insertBefore(e,n);return i};function s(s){let c=typeof s==`function`?r(s):a(s)?s:null;return t((t,r)=>{let a=t.parentNode;e(a,`unsafeHtml() anchor comment has no parent node`);let l=document.createComment(`unsafe-html/end`);if(a.insertBefore(l,t.nextSibling),c){let e=n(),t=i(()=>{e.clear();let t=l.parentNode;t&&e.setNodes(o(c.value,t,l))});r(()=>t.dispose()),r(()=>{e.clear(),l.remove()})}else o(s,a,l),r(()=>l.remove())})}export{s as unsafeHtml};
|
|
2
2
|
//# sourceMappingURL=unsafe-html.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsafe-html.js","names":["rawEffect","parseHtml","tpl","html","nodes","node","parent","insertBefore","unsafeHtml","src","value","computed","isReactive","createDirectiveResult","anchor","invariant","endMarker","slot","createReplaceableSlot","stop","effect","registerCleanup"],"sources":["../../src/directives/unsafe-html.ts"],"sourcesContent":["import { computed, isReactive, type Readable, effect as rawEffect, type Signal } from '@vielzeug/ripple';\n\nimport { invariant } from '../errors';\nimport { createDirectiveResult, type DirectiveResult } from '../template/result';\nimport { createReplaceableSlot } from '../utils/dom';\n\nconst parseHtml = (html: string, parent: ParentNode, insertBefore: Node): Node[] => {\n const tpl = document.createElement('template');\n\n tpl.innerHTML = html;\n\n const nodes = Array.from(tpl.content.cloneNode(true).childNodes);\n\n for (const node of nodes) parent.insertBefore(node, insertBefore);\n\n return nodes;\n};\n\n/**\n * Renders HTML without escaping as a DirectiveResult.\n *\n * This is intentionally named `unsafeHtml`: sanitization is an application\n * boundary, not mutable process-wide framework configuration. Sanitize\n * untrusted content before passing it here.\n *\n * Supports static strings, signals, and getter functions `() => string`.\n * When reactive, the DOM is updated in-place whenever the value changes.\n */\nexport function unsafeHtml(value: (() => string) | string | Signal<string> | Readable<string>): DirectiveResult {\n const isFn = typeof value === 'function';\n const src: Readable<string> | null = isFn ? computed(value as () => string) : isReactive(value) ? value : null;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const
|
|
1
|
+
{"version":3,"file":"unsafe-html.js","names":["rawEffect","parseHtml","tpl","html","nodes","node","parent","insertBefore","unsafeHtml","src","value","computed","isReactive","createDirectiveResult","initialParent","anchor","invariant","endMarker","slot","createReplaceableSlot","stop","effect","registerCleanup"],"sources":["../../src/directives/unsafe-html.ts"],"sourcesContent":["import { computed, isReactive, type Readable, effect as rawEffect, type Signal } from '@vielzeug/ripple';\n\nimport { invariant } from '../errors';\nimport { createDirectiveResult, type DirectiveResult } from '../template/result';\nimport { createReplaceableSlot } from '../utils/dom';\n\nconst parseHtml = (html: string, parent: ParentNode, insertBefore: Node): Node[] => {\n const tpl = document.createElement('template');\n\n tpl.innerHTML = html;\n\n const nodes = Array.from(tpl.content.cloneNode(true).childNodes);\n\n for (const node of nodes) parent.insertBefore(node, insertBefore);\n\n return nodes;\n};\n\n/**\n * Renders HTML without escaping as a DirectiveResult.\n *\n * This is intentionally named `unsafeHtml`: sanitization is an application\n * boundary, not mutable process-wide framework configuration. Sanitize\n * untrusted content before passing it here.\n *\n * Supports static strings, signals, and getter functions `() => string`.\n * When reactive, the DOM is updated in-place whenever the value changes.\n */\nexport function unsafeHtml(value: (() => string) | string | Signal<string> | Readable<string>): DirectiveResult {\n const isFn = typeof value === 'function';\n const src: Readable<string> | null = isFn ? computed(value as () => string) : isReactive(value) ? value : null;\n\n return createDirectiveResult((anchor, registerCleanup) => {\n const initialParent = anchor.parentNode;\n\n invariant(initialParent, 'unsafeHtml() anchor comment has no parent node');\n\n const endMarker = document.createComment('unsafe-html/end');\n\n initialParent.insertBefore(endMarker, anchor.nextSibling);\n\n if (src) {\n const slot = createReplaceableSlot();\n\n const stop = rawEffect(() => {\n slot.clear();\n\n // Resolved live: an unsafeHtml() inside an each() item moves with the\n // item's range; the mount-time parent would be stale after a reorder.\n const parent = endMarker.parentNode;\n\n if (!parent) return;\n\n slot.setNodes(parseHtml(src.value, parent, endMarker));\n });\n\n registerCleanup(() => stop.dispose());\n registerCleanup(() => {\n slot.clear();\n endMarker.remove();\n });\n } else {\n parseHtml(value as string, initialParent, endMarker);\n\n registerCleanup(() => endMarker.remove());\n }\n });\n}\n"],"mappings":"A,uOAMA,IAAMC,GAAa,EAAc,EAAoB,IAA+B,CAClF,IAAMC,EAAM,SAAS,cAAc,UAAU,EAE7C,EAAI,UAAYC,EAEhB,IAAMC,EAAQ,MAAM,KAAKF,EAAI,QAAQ,UAAU,EAAI,CAAC,CAAC,UAAU,EAE/D,IAAK,IAAMG,KAAQD,EAAOE,EAAO,aAAaD,EAAME,CAAY,EAEhE,OAAOH,CACT,EAYA,SAAgBI,EAAW,EAAqF,CAE9G,IAAMC,EADO,OAAOC,GAAU,WACcC,EAASD,CAAqB,EAAIE,EAAWF,CAAK,EAAIA,EAAQ,KAE1G,OAAOG,GAAuB,EAAQ,IAAoB,CACxD,IAAMC,EAAgBC,EAAO,WAE7BC,EAAUF,EAAe,gDAAgD,EAEzE,IAAMG,EAAY,SAAS,cAAc,iBAAiB,EAI1D,GAFAH,EAAc,aAAaG,EAAWF,EAAO,WAAW,EAEpDN,EAAK,CACP,IAAMS,EAAOC,EAAsB,EAE7BC,EAAOpB,MAAgB,CAC3BkB,EAAK,MAAM,EAIX,IAAMZ,EAASW,EAAU,WAEpBX,GAELY,EAAK,SAASjB,EAAUQ,EAAI,MAAOH,EAAQW,CAAS,CAAC,CACvD,CAAC,EAEDK,MAAsBF,EAAK,QAAQ,CAAC,EACpCE,MAAsB,CACpBJ,EAAK,MAAM,EACXD,EAAU,OAAO,CACnB,CAAC,CACH,MACEhB,EAAUS,EAAiBI,EAAeG,CAAS,EAEnDK,MAAsBL,EAAU,OAAO,CAAC,CAE5C,CAAC,CACH,C"}
|
package/dist/directives/when.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=`when() anchor comment has no parent node`;function a(a,o,s){return typeof a!=`function`&&!(0,r.isReactive)(a)?t.createDirectiveResult((r,c)=>{let l=a?o():s?s():null;if(!l||!t.isHtmlResult(l))return;let u=r.parentNode;e.invariant(u,i);let d=l.mount(u,r,c);c(()=>n.removeNodes(d))}):t.createDirectiveResult((c,l)=>{let u=(typeof a==`function`?(0,r.computed)(a):null)??a,d=c.parentNode;e.invariant(d,i);let f=document.createComment(`when/end`);d.insertBefore(f,c.nextSibling);let p=n.createReplaceableSlot(),m=(0,r.effect)(()=>{let e=u.value;p.clear();let n=e?o():s?s():null;n
|
|
1
|
+
const e=require("../errors.cjs"),t=require("../template/result.cjs"),n=require("../utils/dom.cjs");let r=require("@vielzeug/ripple");var i=`when() anchor comment has no parent node`;function a(a,o,s){return typeof a!=`function`&&!(0,r.isReactive)(a)?t.createDirectiveResult((r,c)=>{let l=a?o():s?s():null;if(!l||!t.isHtmlResult(l))return;let u=r.parentNode;e.invariant(u,i);let d=l.mount(u,r,c);c(()=>n.removeNodes(d))}):t.createDirectiveResult((c,l)=>{let u=(typeof a==`function`?(0,r.computed)(a):null)??a,d=c.parentNode;e.invariant(d,i);let f=document.createComment(`when/end`);d.insertBefore(f,c.nextSibling);let p=n.createReplaceableSlot(),m=(0,r.effect)(()=>{let e=u.value;p.clear();let n=e?o():s?s():null;if(!n||!t.isHtmlResult(n))return;let i=f.parentNode;i&&p.setNodes((0,r.untrack)(()=>n.mount(i,f,p.registerCleanup)))});l(()=>m.dispose()),l(()=>{p.clear(),f.remove()})})}exports.when=a;
|
|
2
2
|
//# sourceMappingURL=when.cjs.map
|