/** * Host-integration hooks. * * A host renderer embedding GXT (e.g. the Ember dual-backend integration) * historically extended the runtime through optional `globalThis.__gxt*` * slots — mutable cross-realm state that masks dual-module-copy bugs and is * invisible to bundlers. The hooks live here as module-local slots behind an * explicit registration API instead. Every runtime call site reads the hook * slot FIRST and falls back to the historical global, so hosts running the * legacy wiring keep working unchanged. * * Register once at host module init: * * ```ts * import { registerHostHooks } from '@lifeart/gxt'; * registerHostHooks({ toBool: emberToBool, scheduleRevalidate: syncNow }); * ``` */ /** * Metadata passed to {@link HostHooks.onConditionBranchSwap} alongside the * freshly-created per-branch render ctx. */ export interface ConditionBranchMeta { /** * The owning `IfCondition` — the SAME object `onRowContextCreated` fires once * for at if-construction (the whole-`{{#if}}` render scope). Lets the host * correlate a branch ctx back to its conditional. */ if: object; /** * Which branch this ctx belongs to: `true` = the truthy (then) branch, * `false` = the falsy (else / `{{#unless}}`) branch. Lets the host select the * matching per-branch teardown bucket (e.g. `trueBranchHelpers` vs * `falseBranchHelpers`). */ value: boolean; } export interface HostHooks { /** * Truthiness override for `{{#if}}`-style conditionals (e.g. Ember's * toBool semantics: `[]` is falsy, `isHTMLSafe('')` is truthy, …). * Replaces `globalThis.__gxtToBool`. */ toBool?: (value: unknown) => boolean; /** * Coerce a template-driven attribute / property value at the single point * where EVERY such write flows through — the `$attr` / `$prop` effect in * `dom.ts`, for BOTH the initial (static) write AND every reactive * (cell-driven) re-apply, across HTML/SVG/MathML namespaces. * * Lets a host (e.g. the Ember dual-backend) implement attribute semantics * GXT core does not model: * - SafeString values (`{ toHTML() } / { toString() }`) → their string; * - Symbol / null-prototype / arbitrary object coercion; * - boolean attributes: `disabled={{false}}` must REMOVE the attribute * (return the remove sentinel), not write the string `"false"`; * - `undefined` → remove the attribute; * - URL sanitization (`href` / `src` / …). * * The host owns any table it needs (e.g. the HTML boolean-attribute set) — * GXT core stays table-free and just hands the host the ability to return * the remove sentinel. * * @param name attribute / property name being written. * @param value the resolved value (already de-celled) about to be applied. * @param element the target element — namespace-correct (HTML/SVG/MathML). * @param isProp `true` for a PROPERTY write (`$prop`: `style`, `className`, * form-control `value`/`checked`, …), `false` for an * ATTRIBUTE write (`$attr`). Hosts typically only need * SafeString→string on the prop side. * @returns the value to apply. On the ATTR side, returning the remove * sentinel `undefined` REMOVES the attribute instead of writing it; any * other value (including `null` / `''`) is written via the namespace api. * On the PROP side the returned value is assigned as-is (no remove). * * HOTTEST host seam — see the `hostNormalizeAttrValue` hot-path cache below. * Replaces the Ember bridge's ~445-line `$_tag` attr-fixup wrapper + its * global `Element.prototype.setAttribute` monkey-patch. * * COMPILE-FLAG GATED: the dom.ts consult sites read the hook as * `WITH_EMBER_INTEGRATION ? hostNormalizeAttrValue : null`, so ONLY * host-integration builds carry the seam (and the removeAttribute sentinel * branch); standalone builds fold the entire consult away at the consumer's * define step — zero branches, zero bytes. Registering the hook in a * standalone (flag-off) build is a no-op by design. */ normalizeAttrValue?: (name: string, value: unknown, element: Element, isProp: boolean) => unknown; /** * When installed, the runtime delegates revalidation scheduling to the * host, which becomes responsible for calling `syncDom()` at the right * time; the built-in async scheduler is bypassed. Replaces * `globalThis.__gxtExternalSchedule` (see also `takeRenderingControl`). */ scheduleRevalidate?: () => void; /** * Observe keyed-list / const-if anchor markers as they are created so the * host can re-associate row state across re-renders. Replaces * `globalThis.__gxtRegisterListMarker`. */ registerListMarker?: (marker: Comment) => void; /** * Unregister keyed-list anchor markers on list teardown (the converse of * `registerListMarker`). Replaces `globalThis.__gxtUnregisterListMarker`. */ unregisterListMarker?: (marker: Comment) => void; /** * Re-bind a keyed row's block param to a NEW object in place when a * ref-swap reused the row by a stale key — preserves DOM identity. * Replaces `globalThis.__gxtRebindEachItem`. */ rebindEachItem?: (oldItem: unknown, newItem: unknown) => void; /** * Register a leaf object held by a tracked cell as a value-owner of that * cell (host reverse-lookup so `set(leafObj, key, …)` can reach the * cell). Replaces `globalThis.__gxtRegisterObjectValueOwner`. */ registerObjectValueOwner?: (value: object, relatedObj: object, relatedKey: string) => void; /** * The `this` of the currently-evaluating runtime-compiled template, used * to materialize absent-path cells. Replaces * `globalThis.__gxtCurrentTemplateThis` (which the host had to mutate * around every template evaluation — with the hook the host keeps that * state module-local). */ getCurrentTemplateThis?: () => unknown; /** * Brand check / brand mark for host "functional helpers" — plain * functions invoked as `(positional, named) => value` rather than * spread-args. Replaces the `EmberFunctionalHelpers` global Set. */ isFunctionalHelper?: (fn: unknown) => boolean; markFunctionalHelper?: (fn: unknown) => void; /** * Dynamic-eval fallback for compiled-template identifier resolution when * the render context doesn't carry `$_eval` (initial render). Replaces * the `globalThis.$_eval` fallback read. */ dynamicEval?: (value: unknown) => unknown; /** * Observe a freshly-created per-row / per-branch render context so the host * can attach its own pre-destroy work to it via `registerDestructor(ctx, …)`. * * Fired: * - once per keyed-`{{#each}}` row, immediately after the row's destructor- * owner ctx is allocated + added to the tree (BEFORE the row body renders); * - once per `{{#if}}`/`{{#unless}}` `IfCondition`, at construction (the * branch render scope), BEFORE the first branch renders. * * Because the runtime fires the row ctx's destructors BEFORE the row DOM is * removed (per-row `destroyItem`→`destroyRowCtx`, and the reordered bulk * `fastCleanup`), a destructor the host registers here runs while the row DOM * is still connected — letting the host run teardown/lifecycle hooks at the * Ember-correct moment without re-implementing row ordering. No-op by default * (standalone GXT never registers it, so behavior is unchanged). */ onRowContextCreated?: (ctx: object) => void; /** * The per-BRANCH twin of `onRowContextCreated`. Observe a freshly-created * per-branch render ctx — a distinct lightweight object created ONCE for * every `{{#if}}`/`{{#unless}}` branch RENDER (initial render AND every * true↔false swap) — so the host can attach that branch's pre-swap teardown * via `registerDestructor(branchCtx, …)`. * * Fired from `IfCondition.renderState`, right after the branch ctx is * allocated and BEFORE the branch body renders (mirroring how * `onRowContextCreated` fires before a row body renders). `branchCtx` is the * value `registerDestructor(branchCtx, …)` accepts. * * The runtime runs that ctx's destructors (via `destroySync`) at the START of * the OUTGOING branch's teardown — inside `destroyBranchSync` / * `destroyBranch`, BEFORE `destroyElementSync`/`destroyElement` removes the * branch DOM — so a destructor the host registers here fires while the * outgoing branch's element is still connected. It fires: * - on a true↔false SWAP: the outgoing branch's ctx destructors run, then a * FRESH branch ctx is created for the incoming branch (the hook fires * again); * - on whole-`{{#if}}` DESTROY: the currently-rendered branch's ctx * destructors run (the async `destroy`→`destroyBranch` path and the * sync-cascade self destructor both route through the same teardown). * * Interplay with `onRowContextCreated`: these are DISTINCT ctxs and never * double-fire. `onRowContextCreated` fires ONCE per `IfCondition` (at * construction — the whole-if scope; a destructor registered there runs on * whole-if destroy). `onConditionBranchSwap` fires once per BRANCH render on a * separate `branchCtx`; a destructor registered there runs at the NEXT swap * (or whole-if destroy) — the per-branch granularity the whole-if seam cannot * express. A host can use either or both. No-op by default (standalone GXT * never registers it, so the branch-render path stays a single hook-presence * branch and no branch ctx is allocated). * * @param branchCtx a fresh per-branch destructor-owner object. * @param meta which branch (`value`) + the owning `IfCondition` (`if`). */ onConditionBranchSwap?: (branchCtx: object, meta: ConditionBranchMeta) => void; /** * The COMPONENT twin of `onRowContextCreated`: observe a freshly-created * COMPONENT render context so the host can attach its own pre-detach work to * it via `registerDestructor(ctx, …)`. This lets an Ember host retire its own * component-liveness re-derivation (pool scan + live-instance set + reattach * machinery) and instead register one pre-detach destructor per component, * exactly as the per-row `onRowContextCreated` delegation already works. * * Fired once per component instance render — class-based AND template-only — * from `$_GET_ARGS` (the seam every compiled component template runs first), * AFTER the ctx is allocated (`COMPONENT_ID`, `[$args]`, empty * `RENDERED_NODES`) and added to the render tree, but BEFORE the component's * own subtree renders. `ctx` is the value `registerDestructor(ctx, …)` / * `addToTree(…, ctx)` accept. * * Ordering: a parent component's hook fires BEFORE any of its child * components' hooks (the parent's `$_GET_ARGS` runs before it renders the * children that call `$_c`). It does NOT fire for: * - each-`{{#each}}` rows or `{{#if}}`/`{{#unless}}` branches — those have * the dedicated `onRowContextCreated` seam and never call `$_GET_ARGS`; * - internal block wrappers (`$_ucw`/`$_inElement`, marked with the * `gxt-block-wrapper` symbol) — they call `$_GET_ARGS` but are excluded. * * Destructors the host registers here fire synchronously during teardown * (`runDestructorsSync` runs a node's destructors BEFORE removing that node's * DOM), so a component that is itself the destroy root (single-component * teardown, `{{#if}}` collapse, per-row removal) runs its destructor while its * element is still connected. In a whole-subtree cascade (route transition) * teardown is deterministic parent-before-child; a descendant's own DOM has * already been detached with its ancestor by the time its destructor fires. * No-op by default (standalone GXT never registers it, so behavior is * unchanged and the create hot path stays a single hook-presence branch). */ onComponentContextCreated?: (ctx: object) => void; /** * Notify the host that the runtime pushed `ctx` onto its render-scope * (parent-context) stack — i.e. children rendered until the matching * `onLeaveRenderScope` attach to `ctx` as their tree parent. Fired from every * `setParentContext`/`pushParentContext` push (e.g. `{{#each}}` row render, * `{{#if}}` branch render, the inverse block). Lets an Ember host ride GXT's * scope stack for its own parentView hierarchy instead of re-pushing manually. * No-op by default. */ onEnterRenderScope?: (ctx: object) => void; /** * The converse of `onEnterRenderScope`: the runtime popped the top render * scope (`setParentContext(null)`/`popParentContext`). Enter/leave are always * balanced, so the host can mirror the stack with a simple push/pop. No-op by * default. */ onLeaveRenderScope?: () => void; /** * Resolve a HUMAN-MEANINGFUL name for a component instance, consulted by the * render-tree serializer (`captureRenderTree` / `componentToRenderTree`) that * feeds the Ember Inspector. * * GXT core only has minification-fragile names for a component: `debugName` * (DEV-only, itself just `constructor.name`) and `constructor.name` (a * minified runtime class name like `xe` / `Factory` in a production build). * The stable, registered name (e.g. Ember's `"hello-world"` / * `"my-app@component:foo/bar"`) is COMPILE-TIME / registry information that * only the HOST owns. This hook lets the host supply it. * * Consulted FIRST in name resolution: when it returns a non-empty string that * becomes the node `name`; when it returns `undefined` / `null` / `''` the * serializer falls back to `debugName` → `constructor.name` → `'(unknown)'`. * * Contract: return `undefined` for components the host does NOT own — in * particular GXT-internal control-flow / wrapper instances (`IfCondition`, * `SyncListComponent` / `AsyncListComponent`, slot contexts, `SVGProvider`, * `UnstableChildWrapper`). The serializer relies on their `debugName` prefixes * to reclassify them into the `'if'` / `'each'` / `'yield'` / `'svg'` keyword * names, so a host name for them would mask that mapping. * * No-op by default (standalone GXT never registers it, so the render-tree * name resolution is unchanged). * * @param component the component instance being serialized. * @returns the meaningful name, or `undefined` / `null` to fall back. */ resolveComponentName?: (component: object) => string | undefined | null; } /** * Internal read path for runtime call sites. Intentionally a plain mutable * object (not getters) — these slots sit on hot paths (per-row list-marker * registration, per-conditional toBool). */ export declare const HOST_HOOKS: HostHooks; /** * Hot-path cache for {@link HostHooks.normalizeAttrValue}. * * Attribute / property application is the HOTTEST path in GXT — it runs * per-attribute, per-element, on every static write AND every reactive * re-apply (krausest-sensitive). So the read at the call site must NOT be an * object-property lookup on `HOST_HOOKS`: it is this module-local slot, `null` * unless a host registered the hook. Standalone GXT therefore pays exactly one * predictable `hook === null` branch per write — no allocation, no call — and * falls straight through to the original `api.attr` / `api.prop` write. A * registered host pays one branch + one hook call. `registerHostHooks` keeps * this in lock-step with `HOST_HOOKS.normalizeAttrValue` (see below), so tests * that clear `HOST_HOOKS` and re-`registerHostHooks({})` reset it to `null`. */ export declare let hostNormalizeAttrValue: NonNullable | null; /** * Merge the given hooks into the active slot table. Later registrations * override earlier ones per-key; passing `undefined` for a key is ignored * (use an explicit no-op function to disable a default the host set * earlier). */ export declare function registerHostHooks(hooks: HostHooks): void; /** * Functional-helper brand check, hook-first with the legacy * `EmberFunctionalHelpers` bare-global Set as fallback. Hosts should * register `isFunctionalHelper` and `markFunctionalHelper` together. */ export declare function isHostFunctionalHelper(fn: unknown): boolean; export declare function markHostFunctionalHelper(fn: unknown): void;