mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
feat(extension-api): converge DOMWidget ↔ Widget via mount-lifecycle (D-widget-converge, A12)
Per D-widget-converge ACCEPTED 2026-05-18 (W6.P3.B PICK: option iii) and
new Axiom A12 (Mount-Lifecycle as the Sole DOM Seam):
- Add WidgetCleanup, WidgetMountContext, WidgetMountFn to widget.ts. The
mount context provides widget/node handles plus onUnmount /
onBeforeRemount / onAfterRemount lifecycle hooks. Cleanup fires on
destruction only; host remount uses the remount hooks.
- Replace WidgetExtensionOptions.created({render, destroy}) with optional
mount: WidgetMountFn. Authors capture host element via closure inside
mount() — there is no widget.element accessor on the handle.
- Delete DOMWidgetOptions interface from node.ts.
- Delete NodeHandle.addDOMWidget() method (replaced by defineWidget + the
same addWidget call as native widgets).
- Update WidgetHandle.setHeight() JSDoc — applies to widgets registered
with a mount() body, not "DOM widgets" as a category.
- Drop DOMWidgetOptions from public barrel; add WidgetCleanup,
WidgetMountContext, WidgetMountFn to the barrel.
Phase A gates (lint + format:check + knip) pass. typecheck/test on v2
stack continue to fail per AGENTS.md Rule #8 (parallel-paths stack).
Migration path: v1 patterns (addDOMWidget, widget.element, widget.inputEl,
addWidget('unregistered-type', …)) keep working through the v1 surface
during the D6 parallel-paths window and break deliberately at D6 Phase D
sunset. See decisions/D-widget-converge.md §Migration-guide rows.
This commit is contained in:
@@ -83,7 +83,6 @@ export type {
|
||||
NodeMode,
|
||||
Point,
|
||||
Size,
|
||||
DOMWidgetOptions,
|
||||
NodeExecutedEvent,
|
||||
NodeConnectedEvent,
|
||||
NodeDisconnectedEvent,
|
||||
@@ -101,7 +100,11 @@ export type {
|
||||
WidgetOptionChangeEvent,
|
||||
WidgetPropertyChangeEvent,
|
||||
WidgetBeforeSerializeEvent,
|
||||
WidgetBeforeQueueEvent
|
||||
WidgetBeforeQueueEvent,
|
||||
// Mount-lifecycle surface per D-widget-converge / Axiom A12 ────────────
|
||||
WidgetCleanup,
|
||||
WidgetMountContext,
|
||||
WidgetMountFn
|
||||
} from './widget'
|
||||
|
||||
export type { Handler, AsyncHandler, Unsubscribe } from './events'
|
||||
|
||||
@@ -202,20 +202,6 @@ export interface NodeBeforeSerializeEvent {
|
||||
replace(fn: (orig: Record<string, unknown>) => Record<string, unknown>): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `NodeHandle.addDOMWidget()`.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export interface DOMWidgetOptions {
|
||||
/** Unique widget name within this node. */
|
||||
name: string
|
||||
/** The DOM element to embed in the node widget area. */
|
||||
element: HTMLElement
|
||||
/** Reserved height in pixels. Defaults to `element.offsetHeight` at mount time. */
|
||||
height?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Controlled surface for node access. Reads query the ECS World; writes
|
||||
* dispatch commands. Events are Vue-reactive watches on World components.
|
||||
@@ -418,24 +404,12 @@ export interface NodeHandle {
|
||||
options?: Partial<WidgetOptions>
|
||||
): WidgetHandle
|
||||
|
||||
/**
|
||||
* Adds a DOM-backed widget to this node.
|
||||
*
|
||||
* Replaces the v1 `node.addDOMWidget(name, type, element, opts)` pattern.
|
||||
* The runtime automatically:
|
||||
* - Reserves node height for the element (via auto-computeSize integration).
|
||||
* - Removes the element from the DOM when the node is removed.
|
||||
* - Includes the widget in `NodeHandle.getWidgets()`.
|
||||
*
|
||||
* Use `WidgetHandle.setHeight(px)` to resize the reservation after initial mount.
|
||||
*
|
||||
* @param opts.name - Unique widget name on this node.
|
||||
* @param opts.element - The DOM element to embed.
|
||||
* @param opts.height - Initial reserved height in pixels. Defaults to `element.offsetHeight`.
|
||||
* @returns A `WidgetHandle` for the registered DOM widget.
|
||||
* @stability experimental
|
||||
*/
|
||||
addDOMWidget(opts: DOMWidgetOptions): WidgetHandle
|
||||
// NOTE: `addDOMWidget(opts)` was removed per D-widget-converge / Axiom A12.
|
||||
// Custom DOM widgets are now registered via `defineWidget({type, mount})`
|
||||
// and instantiated through the same `addWidget(type, name, …)` call as
|
||||
// every other widget. The runtime invokes the registered `mount(host, ctx)`
|
||||
// hook against a per-widget host `<div>` it owns. See `WidgetMountFn` and
|
||||
// `WidgetMountContext` in `./widget` for the lifecycle contract.
|
||||
|
||||
// ── SLOTS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import type { NodeHandle } from './node'
|
||||
import type { WidgetHandle } from './widget'
|
||||
import type { WidgetMountFn } from './widget'
|
||||
|
||||
/**
|
||||
* Options for `defineNode`. Describes an extension that reacts to
|
||||
@@ -118,8 +118,18 @@ export interface ExtensionOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `defineWidget`. Describes an extension that provides a
|
||||
* custom widget type with its own DOM rendering.
|
||||
* Options for `defineWidget`. Registers a custom widget type that renders
|
||||
* through the mount-lifecycle seam (Axiom A12 / D-widget-converge).
|
||||
*
|
||||
* Once registered, the widget can be instantiated on any node via
|
||||
* `node.addWidget(type, name, defaultValue, opts?)`. The runtime allocates
|
||||
* a per-widget host `<div>` and invokes the registered `mount(host, ctx)`
|
||||
* hook against it. The widget's mount body captures the host (and any DOM
|
||||
* it constructs) via closure — there is no `widget.element` accessor on
|
||||
* the handle.
|
||||
*
|
||||
* `mount` is optional: omit it for value-only widgets (numeric, combo, etc.)
|
||||
* that render through the native widget renderer with no custom DOM.
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
@@ -130,13 +140,14 @@ export interface ExtensionOptions {
|
||||
* name: 'my-org.color-picker',
|
||||
* type: 'COLOR_PICKER',
|
||||
*
|
||||
* created(widget, node) {
|
||||
* return {
|
||||
* // mount color picker DOM
|
||||
* render(container) {},
|
||||
* // cleanup
|
||||
* destroy() {}
|
||||
* }
|
||||
* mount(host, ctx) {
|
||||
* const input = document.createElement('input')
|
||||
* input.type = 'color'
|
||||
* input.value = String(ctx.widget.getValue() ?? '#000000')
|
||||
* input.addEventListener('input', () => ctx.widget.setValue(input.value))
|
||||
* host.appendChild(input)
|
||||
* // Optional cleanup — fires once on widget destruction.
|
||||
* return () => input.remove()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
@@ -148,13 +159,15 @@ export interface WidgetExtensionOptions {
|
||||
type: string
|
||||
|
||||
/**
|
||||
* Called once per widget instance. Return a `{ render, destroy }` pair for
|
||||
* custom DOM rendering, or `void` for non-visual widgets.
|
||||
* Mount lifecycle hook — the **sole** DOM seam per Axiom A12. Called once
|
||||
* per widget instance when the widget is first attached to its node host
|
||||
* in the DOM. May return a `WidgetCleanup` function that fires on widget
|
||||
* destruction (host remount does NOT fire cleanup; see
|
||||
* `WidgetMountContext.onBeforeRemount` / `onAfterRemount`).
|
||||
*
|
||||
* Omit entirely for value-only widgets that need no custom DOM.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
created?(
|
||||
widget: WidgetHandle,
|
||||
parentNode: NodeHandle | null
|
||||
): { render(container: HTMLElement): void; destroy?(): void } | void
|
||||
mount?: WidgetMountFn
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { AsyncHandler, Handler, Unsubscribe } from './events'
|
||||
import type { NodeHandle } from './node'
|
||||
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
/**
|
||||
@@ -345,13 +346,16 @@ export interface WidgetHandle<T = WidgetValue> {
|
||||
*/
|
||||
readonly label: string
|
||||
|
||||
// ── HEIGHT — DOM widgets only ─────────────────────────────────────────────
|
||||
// ── HEIGHT — reserved layout slot for mount-lifecycle widgets ────────────
|
||||
|
||||
/**
|
||||
* Updates the reserved height for this DOM widget and triggers a node relayout.
|
||||
* Updates the reserved height for this widget and triggers a node relayout.
|
||||
*
|
||||
* Only meaningful for widgets registered via `NodeHandle.addDOMWidget()`.
|
||||
* For non-DOM widgets this is a no-op.
|
||||
* Meaningful for widgets registered via {@link defineWidget} with a
|
||||
* {@link WidgetMountFn} `mount()` body — the reserved height bounds the
|
||||
* runtime-owned host `<div>` that the mount body renders into. For widgets
|
||||
* that render through the native widget renderer (no `mount`), this is a
|
||||
* no-op.
|
||||
*
|
||||
* Replaces the v1 pattern of re-assigning `node.computeSize` to return a new
|
||||
* height whenever the embedded element resizes.
|
||||
@@ -388,14 +392,14 @@ export interface WidgetHandle<T = WidgetValue> {
|
||||
* object is `Readonly<WidgetOptions>` — `widget.options.min = 0`,
|
||||
* `widget.options = {...}`, and `widget.options.values = [...]` all raise
|
||||
* TypeScript errors at compile time. To mutate, use
|
||||
* {@link WidgetHandle.setOption} per-key. Bulk `setOptions(opts)` is
|
||||
* tracked under W6.P8.UNMIGRATABLE / D-widget-converge.
|
||||
* {@link WidgetHandle.setOption} per-key.
|
||||
*
|
||||
* Note: this is an accessor pair on the v2 surface. Reading is free; the
|
||||
* setter intentionally does not exist on the public type. v1 patterns like
|
||||
* `widget.options.serialize = false` should migrate to
|
||||
* {@link WidgetHandle.setSerializeEnabled}; `widget.options.values = [...]`
|
||||
* (combo refresh) migrates to a future `setValues` mutator (W6.P3).
|
||||
* (combo refresh) migrates to a future `setValues` mutator (tracked
|
||||
* under W6.P8.UNMIGRATABLE).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@@ -554,6 +558,81 @@ export interface WidgetHandle<T = WidgetValue> {
|
||||
): Unsubscribe
|
||||
}
|
||||
|
||||
// ── MOUNT LIFECYCLE — the sole DOM seam per D-widget-converge / Axiom A12 ──
|
||||
|
||||
/**
|
||||
* Cleanup function returned from a widget's `mount()`. Fires exactly once,
|
||||
* when the widget entity is destroyed. **Does NOT fire on host remount**
|
||||
* (graph↔app mode, subgraph promotion, `<KeepAlive>` shuffle) — use
|
||||
* {@link WidgetMountContext.onBeforeRemount} / {@link WidgetMountContext.onAfterRemount}
|
||||
* for those.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export type WidgetCleanup = () => void
|
||||
|
||||
/**
|
||||
* Context passed to a widget's `mount()` function.
|
||||
*
|
||||
* Per **Axiom A12** (Mount-Lifecycle as the Sole DOM Seam), this is the only
|
||||
* surface through which DOM enters a widget. Authors capture the host element
|
||||
* and any constructed DOM via closure inside `mount()` — there is no
|
||||
* `widget.element` / `widget.inputEl` accessor on the handle.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export interface WidgetMountContext {
|
||||
/** The widget being mounted. Use for `getValue` / `setValue` / `on(...)`. */
|
||||
readonly widget: WidgetHandle
|
||||
/** The node hosting this widget. */
|
||||
readonly node: NodeHandle
|
||||
|
||||
/**
|
||||
* Register a callback that fires when the widget entity is destroyed.
|
||||
* Equivalent to returning a cleanup function from `mount()`; provided as
|
||||
* a hook for composition (e.g. inside helpers that own their own
|
||||
* sub-resources).
|
||||
*/
|
||||
onUnmount(fn: () => void): void
|
||||
|
||||
/**
|
||||
* Register a callback that fires immediately **before** the widget's host
|
||||
* `<div>` is moved to a new location (graph↔app mode, subgraph promotion,
|
||||
* Vue `<KeepAlive>` shuffle). Use to detach observers, pause animations,
|
||||
* or capture scroll position before the move.
|
||||
*
|
||||
* The widget's mount body is NOT re-invoked across a remount; only
|
||||
* `onBeforeRemount` then `onAfterRemount` fire.
|
||||
*/
|
||||
onBeforeRemount(fn: () => void): void
|
||||
|
||||
/**
|
||||
* Register a callback that fires immediately **after** the widget's host
|
||||
* `<div>` has been moved to a new location. Receives the new host element
|
||||
* so authors can re-attach observers, restore scroll position, etc.
|
||||
*/
|
||||
onAfterRemount(fn: (newHost: HTMLElement) => void): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount function for a widget. Called once when the widget is first attached
|
||||
* to a node host in the DOM. Returns an optional cleanup function that fires
|
||||
* on widget destruction.
|
||||
*
|
||||
* @param host - A runtime-owned empty `<div>` for the widget to mount into.
|
||||
* The widget MAY append children, set inline styles, attach event listeners,
|
||||
* etc. It MUST NOT replace or remove the host itself.
|
||||
* @param ctx - Mount context with the widget/node handles and remount hooks.
|
||||
* @returns Optional cleanup function called on widget destruction. Host
|
||||
* remount fires `ctx.onBeforeRemount` / `ctx.onAfterRemount` instead.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export type WidgetMountFn = (
|
||||
host: HTMLElement,
|
||||
ctx: WidgetMountContext
|
||||
) => void | WidgetCleanup
|
||||
|
||||
/**
|
||||
* Options passed to `node.addWidget()` when creating a new widget.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user