Creating Custom Nodes in LitFlow
Custom nodes allow you to build complex, interactive, and data-driven elements within your flow. Since LitFlow uses Light DOM for nodes, your custom components integrate seamlessly with the @xyflow/system core.
1. Basic Structure
A custom node is a standard Lit component that follows a few conventions:
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Position } from '@xyflow/system';
import '../src/lit-handle'; // Import the handle component
@customElement('my-custom-node')
export class MyCustomNode extends LitElement {
// 1. Use Light DOM for xyflow compatibility
createRenderRoot() {
return this;
}
// 2. Required properties passed by <lit-flow>
@property({ type: String, attribute: 'data-id', reflect: true })
nodeId = '';
@property({ type: Object })
data: any = {};
@property({ type: Boolean, reflect: true })
selected = false;
render() {
return html`
<div class="node-container">
<div>${this.data.label}</div>
<!-- 3. Add handles with data-nodeid -->
<lit-handle
type="target"
data-handlepos="${Position.Top}"
data-nodeid="${this.nodeId}"
></lit-handle>
<lit-handle
type="source"
data-handlepos="${Position.Bottom}"
data-nodeid="${this.nodeId}"
></lit-handle>
</div>
`;
}
}
2. Defining Schemas (Data Contracts)
While TypeScript provides type safety, you can define a "schema" for your node's data to ensure consistency.
Example: Prompt Node Schema
interface PromptNodeData {
label: string;
prompt: string;
temperature: number;
model: 'gemini-1.5-flash' | 'gemini-1.5-pro';
}
In your component, you can use a setter to validate or transform this data:
@property({ type: Object })
set data(val: PromptNodeData) {
this._data = val;
// Perform internal updates based on new data
this.requestUpdate();
}
3. Communicating with the Flow
Custom nodes should be "dumb" regarding the overall graph state. To update data, dispatch a custom event that the parent application can listen to.
private _onInputChange(e: Event) {
const newValue = (e.target as HTMLInputElement).value;
this.dispatchEvent(new CustomEvent('node-data-change', {
bubbles: true,
composed: true,
detail: {
id: this.nodeId,
data: { prompt: newValue }
}
}));
}
4. Registering the Node
In your main application, map a type string to your custom element tag:
const flow = document.querySelector('lit-flow');
flow.nodeTypes = {
...flow.nodeTypes,
'promptNode': 'my-custom-node'
};
flow.nodes = [
{
id: '1',
type: 'promptNode',
data: { label: 'AI Prompt', prompt: 'Hello world' },
position: { x: 100, y: 100 }
}
];
5. Interactive State & Syncing
If your custom node has internal interactive state (e.g., a collapsed/expanded toggle), you must sync this state back to the data property. This ensures the state is preserved when the parent flow re-renders the nodes array.
@property({ type: Object })
set data(val: any) {
this._data = val;
// Sync internal state from data
this.collapsed = !!val.collapsed;
}
private _toggleCollapse() {
// Dispatch event to parent to update the global nodes array
this.dispatchEvent(new CustomEvent('group-collapse', {
bubbles: true,
composed: true,
detail: {
id: this.nodeId,
collapsed: !this.collapsed
}
}));
}
6. Styling
Since custom nodes use Light DOM, their styles should be defined:
- Globally in your page's
<style>tag. - Inlined in the
render()method. - In the parent
<lit-flow>component's styles (if you are extending the library).
The .xyflow__node class is automatically applied to all nodes for base positioning and selection borders.