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.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.getNode(id)?.state ?? {}) }),
}
} <script setup lang="ts">
// Vue — method/auth are panel state. setWidgetValue makes displayOptions.show re-evaluate.
import { ref } from 'vue'
import { XenolithGraph, XenolithPanel } from '@xenolithengine/graph-vue'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupConditionalWidgets, CONDITIONAL_WIDGETS_NODE_ID } from '@xenolithengine/demo/conditional-widgets'
type Method = 'GET' | 'POST' | 'PUT'
type Auth = 'none' | 'basic' | 'bearer'
const editor = ref<XenolithEditor | null>(null)
const method = ref<Method>('GET')
const auth = ref<Auth>('none')
function onReady(e: XenolithEditor): void {
editor.value = e
setupConditionalWidgets(e)
}
function onMethod(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Method
method.value = next
editor.value?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'method', next)
}
function onAuth(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Auth
auth.value = next
editor.value?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'auth', next)
}
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="onReady">
<XenolithPanel position="top-left">
<div style="display:flex;gap:8px;align-items:center;">
<span style="font-size:12px;">method</span>
<select :value="method" style="font:inherit;font-size:12px;padding:3px 6px;border-radius:4px;border:1px solid var(--xeno-border);background:transparent;color:var(--xeno-text);" @change="onMethod">
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span style="font-size:12px;margin-left:8px;">auth</span>
<select :value="auth" style="font:inherit;font-size:12px;padding:3px 6px;border-radius:4px;border:1px solid var(--xeno-border);background:transparent;color:var(--xeno-text);" @change="onAuth">
<option>none</option><option>basic</option><option>bearer</option>
</select>
</div>
</XenolithPanel>
</XenolithGraph>
</div>
</template> // 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.getNode(id)?.state ?? {}) }),
}
} <!-- Svelte — method/auth are component state. setWidgetValue makes displayOptions.show re-evaluate. -->
<script lang="ts">
import { xenolith, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel } from '@xenolithengine/graph-svelte/components'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupConditionalWidgets, CONDITIONAL_WIDGETS_NODE_ID } from '@xenolithengine/demo/conditional-widgets'
type Method = 'GET' | 'POST' | 'PUT'
type Auth = 'none' | 'basic' | 'bearer'
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
let method = $state<Method>('GET')
let auth = $state<Auth>('none')
const sel = 'font:inherit;font-size:12px;padding:3px 6px;border-radius:4px;border:1px solid var(--xeno-border);background:transparent;color:var(--xeno-text);'
function onMethod(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Method
method = next
editor?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'method', next)
}
function onAuth(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Auth
auth = next
editor?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'auth', next)
}
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); setupConditionalWidgets(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithPanel position="top-left">
<div style="display:flex;gap:8px;align-items:center;">
<span style="font-size:12px;">method</span>
<select style={sel} value={method} onchange={onMethod}>
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span style="font-size:12px;margin-left:8px;">auth</span>
<select style={sel} value={auth} onchange={onAuth}>
<option>none</option><option>basic</option><option>bearer</option>
</select>
</div>
</XenolithPanel> // 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.getNode(id)?.state ?? {}) }),
}
} // Solid — method/auth are signals. setWidgetValue makes displayOptions.show re-evaluate.
import { createSignal } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupConditionalWidgets, CONDITIONAL_WIDGETS_NODE_ID } from '@xenolithengine/demo/conditional-widgets'
type Method = 'GET' | 'POST' | 'PUT'
type Auth = 'none' | 'basic' | 'bearer'
const sel = 'font:inherit;font-size:12px;padding:3px 6px;border-radius:4px;border:1px solid var(--xeno-border);background:transparent;color:var(--xeno-text);'
export function ConditionalWidgetsDemo() {
const [editor, setEditor] = createSignal<XenolithEditor | null>(null)
const [method, setMethod] = createSignal<Method>('GET')
const [auth, setAuth] = createSignal<Auth>('none')
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => { setEditor(e.detail); setupConditionalWidgets(e.detail) }}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:8px;align-items:center;padding:8px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;color:var(--xeno-text);font:13px system-ui,sans-serif;">
<span style="font-size:12px;">method</span>
<select
style={sel}
value={method()}
onChange={(ev) => {
const next = ev.currentTarget.value as Method
setMethod(next)
editor()?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'method', next)
}}
>
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span style="font-size:12px;margin-left:8px;">auth</span>
<select
style={sel}
value={auth()}
onChange={(ev) => {
const next = ev.currentTarget.value as Auth
setAuth(next)
editor()?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'auth', next)
}}
>
<option>none</option><option>basic</option><option>bearer</option>
</select>
</div>
</div>
)
} // 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.getNode(id)?.state ?? {}) }),
}
} // Angular — method/auth are signals. setWidgetValue makes displayOptions.show re-evaluate.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { setupConditionalWidgets, CONDITIONAL_WIDGETS_NODE_ID } from '@xenolithengine/demo/conditional-widgets'
type Method = 'GET' | 'POST' | 'PUT'
type Auth = 'none' | 'basic' | 'bearer'
@Component({
selector: 'conditional-widgets-demo',
standalone: true,
providers: [XenolithGraphService],
template: `
<div style="position:absolute;inset:0;">
<div #host class="xeno" style="position:absolute;inset:0;"></div>
<div class="card">
<span>method</span>
<select [value]="method()" (change)="onMethod($event)">
<option>GET</option><option>POST</option><option>PUT</option>
</select>
<span class="gap">auth</span>
<select [value]="auth()" (change)="onAuth($event)">
<option>none</option><option>basic</option><option>bearer</option>
</select>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; gap:8px; align-items:center;
padding:8px; background:var(--xeno-panel,#1d1d1d); border:1px solid var(--xeno-border,#333);
border-radius:10px; color:var(--xeno-text,#cfcfcf); font:12px system-ui,sans-serif; }
.gap { margin-left:8px; }
select { font:inherit; font-size:12px; padding:3px 6px; border-radius:4px;
border:1px solid var(--xeno-border,#333); background:transparent; color:inherit; }
`],
})
export class ConditionalWidgetsDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
method = signal<Method>('GET')
auth = signal<Auth>('none')
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
setupConditionalWidgets(editor)
}
onMethod(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Method
this.method.set(next)
this.graph.editor?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'method', next)
}
onAuth(ev: Event): void {
const next = (ev.target as HTMLSelectElement).value as Auth
this.auth.set(next)
this.graph.editor?.setWidgetValue(CONDITIONAL_WIDGETS_NODE_ID, 'auth', next)
}
ngOnDestroy(): void { this.graph.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.getNode(id)?.state ?? {}) }),
}
}