First piloted slice of the closed architecture: a runtime that interprets
the morfo declaration and writes mutable attrs to the DOM via dom.apply,
removing the need for the provider to hand-roll resolveMorfoProps bindings
on every part.
src/uix/soma/morfo/
- resolver.ts — pure morfo → attrs resolver, extracted from Provider so
both legacy Provider (still in place) and the new runtime can share it
- runtime.svelte.ts — `createMorfoRuntime(morfo, sources)` with V1 surface:
- registerPart(part, opts) — registers per-instance id/ref/sources and
sets up a $effect that reapplies attrs whenever sources change
- partProps(part) — returns ONLY identity (id + marker + ref attachment),
never mutable attrs (those go through dom.apply)
- runtime.svelte.test.ts — 4 tests verifying initial application, reactive
reapplication, and unknown-part rejection
ARIA stateRef refinement
The shared resolver now stringifies ARIA stateRef values to 'true'/'false'.
Boolean false on aria-* must mean "explicitly unpressed/unexpanded", not
"attribute absent" — the previous boolean return wired into dom.apply's
`false → removeAttribute` semantics, which is correct for data-* presence
flags but wrong for ARIA. Distinct contracts; resolver decides at the
boundary, not dom.apply.
Toggle morfo
Added explicit `value` sources to all data entries. The legacy provider
hard-coded data-state etc.; the runtime needs the morfo to declare where
each attr's value comes from. Now data-state references stateRef('on') and
data-disabled / data-readonly / data-required / data-invalid reference
their corresponding propRef with prop-truthy conditions.
Toggle provider migration
- ToggleProvider creates a MorfoRuntime in its constructor with Soma's dom.
- registerPart('provider', { id, ref }) wires the runtime to the button.
- props derivation now returns only:
- runtime.partProps('provider') — id + data-toggle + ref attachment
- disabled (HTML attr; not in morfo by design — primitive button concern)
- onclick handler
- All morfo-declared attrs (data-state, aria-pressed, data-disabled,
aria-disabled, aria-label, etc.) are written by the runtime's effect via
dom.apply.
Verification
- Unit tests: 4/4 morfo runtime + full focused suite green.
- morfo:check: 66/66 — Toggle's emitted DOM matches the morfo declaration
end-to-end now that data-* sources are pinned.
- smoke: 68/68 routes 200 OK.
- svelte-check: no new errors.
V2 (deferred — not part of this pilot):
- runtime.keydown(part, event) for morfo.keyboard dispatch.
- runtime.trigger(eventName) — prewrite + semantic.emit + handler chain
(Toast will be the first component to exercise this).
- OptsFromProps + bindProps helper to absorb the props bridge boilerplate.
morfo-runtime
parent
02fa963cb8
commit
8ede535305
@ -0,0 +1,17 @@
|
|||||||
|
export {
|
||||||
|
createMorfoRuntime,
|
||||||
|
type MorfoRuntime,
|
||||||
|
type MorfoRuntimeSources,
|
||||||
|
type RegisterPartOpts,
|
||||||
|
type SourceMap
|
||||||
|
} from './runtime.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
findMorfoPart,
|
||||||
|
resolvePartAttrs,
|
||||||
|
resolveMorfoSource,
|
||||||
|
resolveMorfoDataValue,
|
||||||
|
resolveMorfoAriaValue,
|
||||||
|
shouldEmitMorfoEntry,
|
||||||
|
type MorfoBindings
|
||||||
|
} from './resolver';
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Pure morfo → attrs resolver.
|
||||||
|
*
|
||||||
|
* Walks a `MorfoPart`'s `data` and `aria` declarations, evaluates conditions,
|
||||||
|
* resolves value sources against the supplied bindings, and returns a flat
|
||||||
|
* attribute map ready to render or to pass to `dom.apply`.
|
||||||
|
*
|
||||||
|
* No reactivity, no side-effects, no DOM. The runtime layer wraps these in
|
||||||
|
* effects; the Provider base class wraps them in a `$derived.by`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
MorfoAriaEntry,
|
||||||
|
MorfoCondition,
|
||||||
|
MorfoData,
|
||||||
|
MorfoPart,
|
||||||
|
MorfoPrimitiveValueSource,
|
||||||
|
MorfoValueSource
|
||||||
|
} from '../../morfo/types';
|
||||||
|
|
||||||
|
export interface MorfoBindings {
|
||||||
|
props?: Record<string, unknown>;
|
||||||
|
states?: Record<string, unknown>;
|
||||||
|
parts?: Record<string, unknown>;
|
||||||
|
translations?: (key: string) => string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMorfoPart(parts: readonly MorfoPart[], target: string): MorfoPart | undefined {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.kebab === target) return part;
|
||||||
|
if (part.parts) {
|
||||||
|
const nested = findMorfoPart(part.parts, target);
|
||||||
|
if (nested) return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldEmitMorfoEntry(
|
||||||
|
condition: MorfoCondition | undefined,
|
||||||
|
bindings: MorfoBindings
|
||||||
|
): boolean {
|
||||||
|
if (!condition || condition === 'always') return true;
|
||||||
|
if (condition.when === 'part-present') return Boolean(bindings.parts?.[condition.part]);
|
||||||
|
if (condition.when === 'state-equals') return bindings.states?.[condition.state] === condition.value;
|
||||||
|
if (condition.when === 'prop-truthy') return Boolean(bindings.props?.[condition.prop]);
|
||||||
|
if (condition.when === 'prop-falsy') return !bindings.props?.[condition.prop];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMorfoPrimitiveSource(
|
||||||
|
source: MorfoPrimitiveValueSource,
|
||||||
|
bindings: MorfoBindings
|
||||||
|
): unknown {
|
||||||
|
if (source.kind === 'literal') return source.value;
|
||||||
|
if (source.kind === 'stateRef') return bindings.states?.[source.state];
|
||||||
|
if (source.kind === 'partRef') return bindings.parts?.[source.target];
|
||||||
|
if (source.kind === 'propRef') return bindings.props?.[source.prop];
|
||||||
|
if (source.kind === 'translationRef') return bindings.translations?.(source.key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMorfoSource(source: MorfoValueSource, bindings: MorfoBindings): unknown {
|
||||||
|
if (source.kind !== 'mapRef') {
|
||||||
|
return resolveMorfoPrimitiveSource(source, bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = resolveMorfoPrimitiveSource(source.source, bindings);
|
||||||
|
if (raw === undefined || raw === null) {
|
||||||
|
return source.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = source.map[String(raw)];
|
||||||
|
return mapped ?? source.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMorfoDataValue(data: MorfoData, bindings: MorfoBindings): unknown {
|
||||||
|
const source = data.value;
|
||||||
|
if (!source) return undefined;
|
||||||
|
const raw = resolveMorfoSource(source, bindings);
|
||||||
|
|
||||||
|
if (!data.values || data.values.length === 0) {
|
||||||
|
return raw ? '' : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.kind === 'stateRef' && typeof raw === 'boolean') {
|
||||||
|
if (raw) return source.state;
|
||||||
|
return data.values.find((value) => value !== source.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMorfoAriaValue(entry: MorfoAriaEntry, bindings: MorfoBindings): unknown {
|
||||||
|
const raw = resolveMorfoSource(entry.value, bindings);
|
||||||
|
|
||||||
|
if (entry.value.kind === 'stateRef') {
|
||||||
|
// ARIA boolean attrs require literal 'true'/'false' strings, not the
|
||||||
|
// HTML boolean attribute semantics. `aria-pressed="false"` is a valid
|
||||||
|
// state distinct from the attribute being absent — never remove it
|
||||||
|
// based on falsy values.
|
||||||
|
return Boolean(raw) ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a part's full attribute map (role + data-* + aria-*) against the
|
||||||
|
* given bindings. Returns `undefined`-valued entries for declared attrs that
|
||||||
|
* shouldn't be emitted in the current state — the caller decides whether to
|
||||||
|
* skip them (Svelte does it automatically; `dom.apply` removes them).
|
||||||
|
*/
|
||||||
|
export function resolvePartAttrs(
|
||||||
|
partMeta: MorfoPart,
|
||||||
|
bindings: MorfoBindings
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (partMeta.role) {
|
||||||
|
result.role = partMeta.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const data of partMeta.data) {
|
||||||
|
if (!data.value) continue;
|
||||||
|
if (!shouldEmitMorfoEntry(data.condition, bindings)) continue;
|
||||||
|
const value = resolveMorfoDataValue(data, bindings);
|
||||||
|
if (value !== undefined) {
|
||||||
|
result[data.attr] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const aria of partMeta.aria) {
|
||||||
|
if (!shouldEmitMorfoEntry(aria.condition, bindings)) continue;
|
||||||
|
const value = resolveMorfoAriaValue(aria, bindings);
|
||||||
|
if (value !== undefined) {
|
||||||
|
result[aria.attr] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { flushSync } from 'svelte'
|
||||||
|
|
||||||
|
import { createActiveDom } from '$uix/adom'
|
||||||
|
|
||||||
|
import { state } from '../reactive'
|
||||||
|
import { toggleMorfo } from '../../morfo/components/toggle'
|
||||||
|
import { createMorfoRuntime } from './runtime.svelte'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect roots are normally provided by Svelte components. For unit tests we
|
||||||
|
* wrap the runtime construction in `$effect.root` so `$effect` calls inside
|
||||||
|
* `registerPart` have a parent to attach to.
|
||||||
|
*/
|
||||||
|
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
|
||||||
|
let result!: T
|
||||||
|
const cleanup = $effect.root(() => {
|
||||||
|
result = fn()
|
||||||
|
})
|
||||||
|
return { result, cleanup }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createMorfoRuntime', () => {
|
||||||
|
let target: HTMLElement
|
||||||
|
let dom: ReturnType<typeof createActiveDom>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
target = document.createElement('button')
|
||||||
|
target.id = 'tog-1'
|
||||||
|
document.body.appendChild(target)
|
||||||
|
dom = createActiveDom()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
it('partProps returns identity (id + marker) without mutable attrs', () => {
|
||||||
|
const { result: runtime, cleanup } = withEffectRoot(() => {
|
||||||
|
const id = state('tog-1')
|
||||||
|
const ref = state<HTMLElement | null>(null)
|
||||||
|
const r = createMorfoRuntime(toggleMorfo, {
|
||||||
|
dom,
|
||||||
|
states: { on: () => false }
|
||||||
|
})
|
||||||
|
r.registerPart('provider', { id, ref })
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = runtime.partProps('provider')
|
||||||
|
expect(props.id).toBe('tog-1')
|
||||||
|
expect(props['data-toggle']).toBe('')
|
||||||
|
// Mutable attrs are NOT in partProps; they're written via dom.apply by the effect.
|
||||||
|
expect(props['data-state']).toBeUndefined()
|
||||||
|
expect(props['aria-pressed']).toBeUndefined()
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes morfo-derived attrs to DOM when ref attaches', () => {
|
||||||
|
const ref = state<HTMLElement | null>(null)
|
||||||
|
const { cleanup } = withEffectRoot(() => {
|
||||||
|
const id = state('tog-1')
|
||||||
|
const r = createMorfoRuntime(toggleMorfo, {
|
||||||
|
dom,
|
||||||
|
states: { on: () => false },
|
||||||
|
props: {
|
||||||
|
disabled: () => false,
|
||||||
|
readonly: () => false,
|
||||||
|
required: () => false,
|
||||||
|
invalid: () => false,
|
||||||
|
ariaLabel: () => undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
r.registerPart('provider', { id, ref })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate Svelte's attachment firing — the runtime registered a setter
|
||||||
|
// on the ref via attachRef, but in tests we mutate the ref directly.
|
||||||
|
ref.current = target
|
||||||
|
flushSync()
|
||||||
|
|
||||||
|
expect(target.getAttribute('data-state')).toBe('off')
|
||||||
|
expect(target.getAttribute('aria-pressed')).toBe('false')
|
||||||
|
expect(target.getAttribute('type')).toBe('button')
|
||||||
|
expect(target.hasAttribute('data-disabled')).toBe(false)
|
||||||
|
expect(target.hasAttribute('aria-disabled')).toBe(false)
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reapplies attrs when sources change', () => {
|
||||||
|
const ref = state<HTMLElement | null>(null)
|
||||||
|
const pressed = state(false)
|
||||||
|
const disabled = state(false)
|
||||||
|
const { cleanup } = withEffectRoot(() => {
|
||||||
|
const id = state('tog-1')
|
||||||
|
const r = createMorfoRuntime(toggleMorfo, {
|
||||||
|
dom,
|
||||||
|
states: { on: () => pressed.current },
|
||||||
|
props: {
|
||||||
|
disabled: () => disabled.current,
|
||||||
|
readonly: () => false,
|
||||||
|
required: () => false,
|
||||||
|
invalid: () => false,
|
||||||
|
ariaLabel: () => undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
r.registerPart('provider', { id, ref })
|
||||||
|
})
|
||||||
|
|
||||||
|
ref.current = target
|
||||||
|
flushSync()
|
||||||
|
expect(target.getAttribute('data-state')).toBe('off')
|
||||||
|
|
||||||
|
pressed.current = true
|
||||||
|
flushSync()
|
||||||
|
expect(target.getAttribute('data-state')).toBe('on')
|
||||||
|
expect(target.getAttribute('aria-pressed')).toBe('true')
|
||||||
|
|
||||||
|
disabled.current = true
|
||||||
|
flushSync()
|
||||||
|
expect(target.getAttribute('aria-disabled')).toBe('true')
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when registering an unknown part', () => {
|
||||||
|
const { cleanup } = withEffectRoot(() => {
|
||||||
|
const r = createMorfoRuntime(toggleMorfo, { dom })
|
||||||
|
expect(() => r.registerPart('nonexistent', { id: state('x') })).toThrow(/not declared/)
|
||||||
|
})
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* MorfoRuntime — interpreter of a component's morfo within Soma.
|
||||||
|
*
|
||||||
|
* V1 surface:
|
||||||
|
* - `registerPart(part, opts)` — the part-provider declares its instance to
|
||||||
|
* the runtime (id source, optional ref, optional per-part sources). Sets up
|
||||||
|
* an internal `$effect` that recomputes the part's mutable attrs whenever
|
||||||
|
* any of its sources change and applies them via `dom.apply`.
|
||||||
|
* - `partProps(part)` — returns ONLY the part's static identity (id, marker,
|
||||||
|
* ref attachment). Mutable attrs (data-state, aria-*, etc.) are written to
|
||||||
|
* the DOM by the runtime's effect, never via Svelte render.
|
||||||
|
*
|
||||||
|
* Operational rules:
|
||||||
|
* - `partProps` must not include any state-derived attr — that would race
|
||||||
|
* with `dom.apply`. The boundary is identity vs. derivation.
|
||||||
|
* - `registerPart` must be called inside an effect root (a Svelte component
|
||||||
|
* scope or a class constructor invoked during component init), because the
|
||||||
|
* per-part effect uses `$effect`.
|
||||||
|
* - Effects are best-effort idempotent: each tick reapplies the full attr
|
||||||
|
* map for that part. Missing attrs are removed via `dom.apply`'s
|
||||||
|
* `false/null/undefined` handling.
|
||||||
|
*
|
||||||
|
* Not in V1 (planned for V2):
|
||||||
|
* - `keydown(part, event)` — dispatch from `morfo.keyboard`.
|
||||||
|
* - `trigger(eventName)` — orchestrates prewrite + semantic.emit + handler.
|
||||||
|
* - `OptsFromProps` + `bindProps` helper to absorb the props bridge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
import type { ActiveDom, DomAttrValue } from '$uix/adom';
|
||||||
|
|
||||||
|
import type { Morfo, MorfoPart } from '../../morfo/types';
|
||||||
|
import { createAttrs } from '../attrs';
|
||||||
|
import { attachRef, type RefAttachment } from '../provider/provider.svelte';
|
||||||
|
import type { Active, State } from '../reactive';
|
||||||
|
|
||||||
|
import {
|
||||||
|
findMorfoPart,
|
||||||
|
resolvePartAttrs,
|
||||||
|
type MorfoBindings
|
||||||
|
} from './resolver';
|
||||||
|
|
||||||
|
export type SourceMap = Record<string, () => unknown>;
|
||||||
|
|
||||||
|
export interface MorfoRuntimeSources {
|
||||||
|
/** DOM service. Required — the runtime applies attrs through it. */
|
||||||
|
dom: ActiveDom;
|
||||||
|
/** Component-wide state sources. Read by `stateRef` declarations. */
|
||||||
|
states?: SourceMap;
|
||||||
|
/** Component-wide prop sources. Read by `propRef` declarations. */
|
||||||
|
props?: SourceMap;
|
||||||
|
/** Component-wide part-id sources. Read by `partRef` declarations. */
|
||||||
|
parts?: SourceMap;
|
||||||
|
/** Translation lookup. Read by `translationRef` declarations. */
|
||||||
|
translate?: (key: string) => string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterPartOpts {
|
||||||
|
/** Per-instance id of this part (from the part-provider's opts). */
|
||||||
|
id: Active<string>;
|
||||||
|
/** Per-instance ref. Required for parts with DOM. */
|
||||||
|
ref?: State<HTMLElement | null>;
|
||||||
|
/** Optional callback when the ref attaches/detaches. */
|
||||||
|
onRefChange?: (el: HTMLElement | null) => void;
|
||||||
|
/** Per-part state sources (override component-level for this part only). */
|
||||||
|
states?: SourceMap;
|
||||||
|
/** Per-part prop sources (override component-level for this part only). */
|
||||||
|
props?: SourceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MorfoRuntime {
|
||||||
|
registerPart(part: string, opts: RegisterPartOpts): void;
|
||||||
|
partProps(part: string): Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PartRegistration {
|
||||||
|
id: Active<string>;
|
||||||
|
ref: State<HTMLElement | null> | undefined;
|
||||||
|
attachment: RefAttachment | undefined;
|
||||||
|
states: SourceMap | undefined;
|
||||||
|
props: SourceMap | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBindings(
|
||||||
|
reg: PartRegistration,
|
||||||
|
sources: MorfoRuntimeSources
|
||||||
|
): MorfoBindings {
|
||||||
|
const states: Record<string, unknown> = {};
|
||||||
|
const props: Record<string, unknown> = {};
|
||||||
|
const parts: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (sources.states) {
|
||||||
|
for (const key in sources.states) states[key] = sources.states[key]();
|
||||||
|
}
|
||||||
|
if (sources.props) {
|
||||||
|
for (const key in sources.props) props[key] = sources.props[key]();
|
||||||
|
}
|
||||||
|
if (sources.parts) {
|
||||||
|
for (const key in sources.parts) parts[key] = sources.parts[key]();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reg.states) {
|
||||||
|
for (const key in reg.states) states[key] = reg.states[key]();
|
||||||
|
}
|
||||||
|
if (reg.props) {
|
||||||
|
for (const key in reg.props) props[key] = reg.props[key]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { states, props, parts, translations: sources.translate };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMorfoRuntime(
|
||||||
|
morfo: Morfo,
|
||||||
|
sources: MorfoRuntimeSources
|
||||||
|
): MorfoRuntime {
|
||||||
|
const attrs = createAttrs(morfo);
|
||||||
|
const registrations = new Map<string, PartRegistration>();
|
||||||
|
|
||||||
|
function registerPart(part: string, opts: RegisterPartOpts): void {
|
||||||
|
const partMeta = findMorfoPart(morfo.parts as readonly MorfoPart[], part);
|
||||||
|
if (!partMeta) {
|
||||||
|
throw new Error(
|
||||||
|
`[morfo-runtime] Part "${part}" not declared in morfo "${morfo.kebab}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reg: PartRegistration = {
|
||||||
|
id: opts.id,
|
||||||
|
ref: opts.ref,
|
||||||
|
attachment: undefined,
|
||||||
|
states: opts.states,
|
||||||
|
props: opts.props
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.ref) {
|
||||||
|
reg.attachment = attachRef(opts.ref, (el) => {
|
||||||
|
untrack(() => opts.onRefChange?.(el));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registrations.set(part, reg);
|
||||||
|
|
||||||
|
// Effect: whenever the part's ref attaches OR any source changes,
|
||||||
|
// recompute the morfo-derived attrs and write them via dom.apply.
|
||||||
|
// Reading `reg.ref.current` is the reactive subscription point — the
|
||||||
|
// `attachRef` setter mutates it when Svelte's attachment callback fires.
|
||||||
|
$effect(() => {
|
||||||
|
const target = reg.ref ? reg.ref.current : null;
|
||||||
|
if (!target) return;
|
||||||
|
const bindings = readBindings(reg, sources);
|
||||||
|
const resolved = resolvePartAttrs(partMeta, bindings);
|
||||||
|
sources.dom.apply({ target, attrs: resolved as Record<string, DomAttrValue> });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function partProps(part: string): Record<string, unknown> {
|
||||||
|
const reg = registrations.get(part);
|
||||||
|
if (!reg) return {};
|
||||||
|
|
||||||
|
const marker = attrs[part];
|
||||||
|
const props: Record<string, unknown> = {
|
||||||
|
id: reg.id.current
|
||||||
|
};
|
||||||
|
if (marker) props[marker] = '';
|
||||||
|
if (reg.attachment) Object.assign(props, reg.attachment);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { registerPart, partProps };
|
||||||
|
}
|
||||||
Loading…
Reference in new issue