Conditional widgets
Declarative `displayOptions.show(state)` — n8n-style. One HTTP Request node hides `body` until the method needs one, and `token` until auth is `bearer`. Pure schema: no `setNodeWidgets` plumbing in the host. The node re-layouts and edges stay attached as widgets appear and disappear.
// Vanilla mount for A1 — conditional widgets. Switch the combos in the toolbar; the node
// re-layouts as `body` / `token` show or hide. The same widgets remain editable inline on the
// node — the toolbar exists only to keep the demo's narrative obvious without the user fishing
// for the right combo.
import { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildConditionalWidgets } from '@xenolithengine/demo/conditional-widgets'
export async function mount(target: HTMLElement): Promise<() => void> {
const editor = await XenolithEditor.init(target, { resizeToWindow: false, minimap: false })
const scene = buildConditionalWidgets(editor)
const panel = document.createElement('div')
panel.setAttribute('data-xeno-panel', '')
panel.style.cssText = 'position:absolute;pointer-events:auto;top:12px;left:12px;display:flex;gap:8px;align-items:center;padding:8px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;color:var(--xeno-text,#cfcfcf);z-index:5;'
panel.innerHTML = `
<span>method</span>
<select data-method style="background:transparent;color:inherit;border:1px solid var(--xeno-border,#333);border-radius:4px;padding:3px 6px;">
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span style="margin-left:8px;">auth</span>
<select data-auth style="background:transparent;color:inherit;border:1px solid var(--xeno-border,#333);border-radius:4px;padding:3px 6px;">
<option>none</option><option>basic</option><option>bearer</option>
</select>`
const methodSel = panel.querySelector<HTMLSelectElement>('[data-method]')!
const authSel = panel.querySelector<HTMLSelectElement>('[data-auth]')!
methodSel.addEventListener('change', () => scene.setMethod(methodSel.value as 'GET' | 'POST' | 'PUT'))
authSel.addEventListener('change', () => scene.setAuth(authSel.value as 'none' | 'basic' | 'bearer'))
editor.overlayRoot.appendChild(panel)
return () => { panel.remove(); editor.destroy() }
} // A1 showcase — conditional widgets (n8n-style `displayOptions.show`). One HTTP Request node
// declares 5 widgets; two of them only appear when other widgets hold specific values:
// - `body` is visible only when `method` ≠ 'GET' (GET requests have no body)
// - `token` is visible only when `auth` = 'bearer' (other auth modes don't need a token)
// Switch the combos: the node grows/shrinks, widget rects relayout, edges stay attached.
// Pure schema — no `setNodeWidgets` plumbing in the host.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import type { NodeId, PinId } from '@xenolithengine/graph-core'
export interface ConditionalWidgetsScene {
nodeId: NodeId
setMethod: (m: 'GET' | 'POST' | 'PUT') => void
setAuth: (a: 'none' | 'basic' | 'bearer') => void
state: () => Record<string, unknown>
}
const inPin = (key: string, type: string): { id: PinId; kind: 'data'; direction: 'in'; type: string; multiple: false; label: string } =>
({ id: `req_${key}` as PinId, kind: 'data', direction: 'in', type, multiple: false, label: key })
/** The HTTPRequest node id this demo loads. */
export const CONDITIONAL_WIDGETS_NODE_ID = 'request' as NodeId
/** Idempotent setup: load the HTTPRequest node. */
export function setupConditionalWidgets(editor: XenolithEditor): void { void buildConditionalWidgets(editor) }
/** @deprecated Prefer `setupConditionalWidgets` + `editor.setWidgetValue`. Kept for vanilla examples. */
export function buildConditionalWidgets(editor: XenolithEditor): ConditionalWidgetsScene {
const id = 'request' as NodeId
editor.loadJSON({
version: 'xenolith.v1',
nodes: [
{
id, type: 'HTTPRequest', position: { x: 80, y: 80 },
state: {
url: 'https://api.example.com/users',
method: 'GET',
body: '{ "name": "Ada" }',
auth: 'none',
token: '',
},
render: { title: 'HTTP Request', category: 'logic' },
pins: [
inPin('url', 'string'),
inPin('method', 'string'),
inPin('body', 'string'),
inPin('auth', 'string'),
inPin('token', 'string'),
{ id: 'req_out' as PinId, kind: 'data', direction: 'out', type: 'object', multiple: true, label: 'response' },
],
widgets: [
{ id: 'url', type: 'text', key: 'url', label: '' },
{ id: 'method', type: 'combo', key: 'method', label: '', values: ['GET', 'POST', 'PUT'] },
// displayOptions.show — the whole point of this showcase. When false, the widget hides
// AND so does its pin-row (label + dot) — core understands a hidden widget's pin is also
// hidden. The pin still exists for serialization & connectivity rules; it just doesn't
// render. Re-evaluated after every setWidgetValue.
{ id: 'body', type: 'text', key: 'body', label: '',
displayOptions: { show: (s: Readonly<Record<string, unknown>>) => s['method'] !== 'GET' } },
{ id: 'auth', type: 'combo', key: 'auth', label: '', values: ['none', 'basic', 'bearer'] },
{ id: 'token', type: 'text', key: 'token', label: '', placeholder: 'Bearer token…',
displayOptions: { show: (s: Readonly<Record<string, unknown>>) => s['auth'] === 'bearer' } },
],
},
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
nodeId: id,
setMethod: (m) => editor.setWidgetValue(id, 'method', m),
setAuth: (a) => editor.setWidgetValue(id, 'auth', a),
state: () => ({ ...(editor.graph.getNode(id)?.state ?? {}) }),
}
} import { useState } from 'react'
import { XenolithGraph, XenolithPanel, useEditor } from '@xenolithengine/graph-react'
import { setupConditionalWidgets, CONDITIONAL_WIDGETS_NODE_ID } from '@xenolithengine/demo/conditional-widgets'
import { DemoStage } from '../Layout.js'
type Method = 'GET' | 'POST' | 'PUT'
type Auth = 'none' | 'basic' | 'bearer'
// Canon: method/auth are panel-local state. The panel writes them directly to the editor's
// widget values via `useEditor()` — no scene plumbing, no setNodeWidgets gymnastics. The node's
// `displayOptions.show` predicates re-evaluate after every setWidgetValue.
function ConditionalPanel() {
const editor = useEditor()
const [method, setMethod] = useState<Method>('GET')
const [auth, setAuth] = useState<Auth>('none')
const onMethod = (m: Method): void => {
setMethod(m)
editor.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'method', m)
}
const onAuth = (a: Auth): void => {
setAuth(a)
editor.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'auth', a)
}
return (
<XenolithPanel position="top-left" style={{ display: 'flex', gap: 8, alignItems: 'center', padding: 8 }}>
<span style={{ fontSize: 12, color: 'var(--xeno-text, #cfcfcf)' }}>method</span>
<select value={method} onChange={(e) => onMethod(e.target.value as Method)} style={sel}>
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span style={{ fontSize: 12, color: 'var(--xeno-text, #cfcfcf)', marginLeft: 8 }}>auth</span>
<select value={auth} onChange={(e) => onAuth(e.target.value as Auth)} style={sel}>
<option>none</option><option>basic</option><option>bearer</option>
</select>
</XenolithPanel>
)
}
/** A1 — declarative conditional widgets (n8n parity). */
export function ConditionalWidgetsDemo() {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={setupConditionalWidgets}>
<ConditionalPanel />
</XenolithGraph>
</DemoStage>
)
}
const sel: React.CSSProperties = {
background: 'transparent',
color: 'var(--xeno-text, #cfcfcf)',
border: '1px solid var(--xeno-border, #333)',
borderRadius: 4,
padding: '3px 6px',
fontSize: 12,
} // A1 showcase — conditional widgets (n8n-style `displayOptions.show`). One HTTP Request node
// declares 5 widgets; two of them only appear when other widgets hold specific values:
// - `body` is visible only when `method` ≠ 'GET' (GET requests have no body)
// - `token` is visible only when `auth` = 'bearer' (other auth modes don't need a token)
// Switch the combos: the node grows/shrinks, widget rects relayout, edges stay attached.
// Pure schema — no `setNodeWidgets` plumbing in the host.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import type { NodeId, PinId } from '@xenolithengine/graph-core'
export interface ConditionalWidgetsScene {
nodeId: NodeId
setMethod: (m: 'GET' | 'POST' | 'PUT') => void
setAuth: (a: 'none' | 'basic' | 'bearer') => void
state: () => Record<string, unknown>
}
const inPin = (key: string, type: string): { id: PinId; kind: 'data'; direction: 'in'; type: string; multiple: false; label: string } =>
({ id: `req_${key}` as PinId, kind: 'data', direction: 'in', type, multiple: false, label: key })
/** The HTTPRequest node id this demo loads. */
export const CONDITIONAL_WIDGETS_NODE_ID = 'request' as NodeId
/** Idempotent setup: load the HTTPRequest node. */
export function setupConditionalWidgets(editor: XenolithEditor): void { void buildConditionalWidgets(editor) }
/** @deprecated Prefer `setupConditionalWidgets` + `editor.setWidgetValue`. Kept for vanilla examples. */
export function buildConditionalWidgets(editor: XenolithEditor): ConditionalWidgetsScene {
const id = 'request' as NodeId
editor.loadJSON({
version: 'xenolith.v1',
nodes: [
{
id, type: 'HTTPRequest', position: { x: 80, y: 80 },
state: {
url: 'https://api.example.com/users',
method: 'GET',
body: '{ "name": "Ada" }',
auth: 'none',
token: '',
},
render: { title: 'HTTP Request', category: 'logic' },
pins: [
inPin('url', 'string'),
inPin('method', 'string'),
inPin('body', 'string'),
inPin('auth', 'string'),
inPin('token', 'string'),
{ id: 'req_out' as PinId, kind: 'data', direction: 'out', type: 'object', multiple: true, label: 'response' },
],
widgets: [
{ id: 'url', type: 'text', key: 'url', label: '' },
{ id: 'method', type: 'combo', key: 'method', label: '', values: ['GET', 'POST', 'PUT'] },
// displayOptions.show — the whole point of this showcase. When false, the widget hides
// AND so does its pin-row (label + dot) — core understands a hidden widget's pin is also
// hidden. The pin still exists for serialization & connectivity rules; it just doesn't
// render. Re-evaluated after every setWidgetValue.
{ id: 'body', type: 'text', key: 'body', label: '',
displayOptions: { show: (s: Readonly<Record<string, unknown>>) => s['method'] !== 'GET' } },
{ id: 'auth', type: 'combo', key: 'auth', label: '', values: ['none', 'basic', 'bearer'] },
{ id: 'token', type: 'text', key: 'token', label: '', placeholder: 'Bearer token…',
displayOptions: { show: (s: Readonly<Record<string, unknown>>) => s['auth'] === 'bearer' } },
],
},
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
nodeId: id,
setMethod: (m) => editor.setWidgetValue(id, 'method', m),
setAuth: (a) => editor.setWidgetValue(id, 'auth', a),
state: () => ({ ...(editor.graph.getNode(id)?.state ?? {}) }),
}
}