mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
9.0 KiB
9.0 KiB
Node Patterns Reference
How to create and modify nodes in OpenNoodl.
Node Types
There are two main types of nodes:
- Runtime Nodes (
noodl-runtime) - Logic, data, utilities - Visual Nodes (
noodl-viewer-react) - React components for UI
Basic Node Structure
Runtime Node (JavaScript)
Location: packages/noodl-runtime/src/nodes/
'use strict';
const MyNode = {
// === METADATA ===
name: 'My.Custom.Node', // Unique identifier
displayName: 'My Custom Node', // Shown in UI
category: 'Custom', // Node picker category
color: 'data', // Node color theme
docs: 'https://docs.example.com', // Documentation link
// === INITIALIZATION ===
initialize() {
// Called when node is created
this._internal.myValue = '';
this._internal.callbacks = [];
},
// === INPUTS ===
inputs: {
// Simple input
textInput: {
type: 'string',
displayName: 'Text Input',
group: 'General',
default: '',
set(value) {
this._internal.textInput = value;
this.flagOutputDirty('result');
}
},
// Number with validation
numberInput: {
type: 'number',
displayName: 'Number',
group: 'General',
default: 0,
set(value) {
if (typeof value !== 'number') return;
this._internal.numberInput = value;
this.flagOutputDirty('result');
}
},
// Signal input (trigger)
doAction: {
type: 'signal',
displayName: 'Do Action',
group: 'Actions',
valueChangedToTrue() {
// Called when signal received
this.performAction();
}
},
// Boolean toggle
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set(value) {
this._internal.enabled = value;
}
},
// Dropdown/enum
mode: {
type: {
name: 'enum',
enums: [
{ value: 'mode1', label: 'Mode 1' },
{ value: 'mode2', label: 'Mode 2' }
]
},
displayName: 'Mode',
group: 'General',
default: 'mode1',
set(value) {
this._internal.mode = value;
}
}
},
// === OUTPUTS ===
outputs: {
// Value output
result: {
type: 'string',
displayName: 'Result',
group: 'General',
getter() {
return this._internal.result;
}
},
// Signal output
completed: {
type: 'signal',
displayName: 'Completed',
group: 'Events'
},
// Error output
error: {
type: 'string',
displayName: 'Error',
group: 'Error',
getter() {
return this._internal.error;
}
}
},
// === METHODS ===
methods: {
performAction() {
if (!this._internal.enabled) return;
try {
// Do something
this._internal.result = 'Success';
this.flagOutputDirty('result');
this.sendSignalOnOutput('completed');
} catch (e) {
this._internal.error = e.message;
this.flagOutputDirty('error');
}
},
// Called when node is deleted
_onNodeDeleted() {
// Cleanup
this._internal.callbacks = [];
}
},
// === INSPECTOR (Debug Panel) ===
getInspectInfo() {
return {
type: 'text',
value: `Current: ${this._internal.result}`
};
}
};
module.exports = {
node: MyNode
};
Visual Node (React)
Location: packages/noodl-viewer-react/src/nodes/
'use strict';
const { Node } = require('@noodl/noodl-runtime');
const MyVisualNode = {
name: 'My.Visual.Node',
displayName: 'My Visual Node',
category: 'UI Elements',
// Visual nodes need these
allowChildren: true, // Can have child nodes
allowChildrenWithCategory: ['UI Elements'], // Restrict child types
getReactComponent() {
return MyReactComponent;
},
// Frame updates for animations
frame: {
// Called every frame if registered
update(context) {
// Animation logic
}
},
inputs: {
// Standard style inputs
backgroundColor: {
type: 'color',
displayName: 'Background Color',
group: 'Style',
default: 'transparent',
set(value) {
this.props.style.backgroundColor = value;
this.forceUpdate();
}
},
// Dimension with units
width: {
type: {
name: 'number',
units: ['px', '%', 'vw'],
defaultUnit: 'px'
},
displayName: 'Width',
group: 'Dimensions',
set(value) {
this.props.style.width = value.value + value.unit;
this.forceUpdate();
}
}
},
outputs: {
// DOM event outputs
onClick: {
type: 'signal',
displayName: 'Click',
group: 'Events'
}
},
methods: {
// Called when mounted
didMount() {
// Setup
},
// Called when unmounted
willUnmount() {
// Cleanup
}
}
};
// React component
function MyReactComponent(props) {
const handleClick = () => {
props.noodlNode.sendSignalOnOutput('onClick');
};
return (
<div style={props.style} onClick={handleClick}>
{props.children}
</div>
);
}
module.exports = {
node: MyVisualNode
};
Common Patterns
Scheduled Updates
Batch multiple input changes before processing:
inputs: {
value1: {
set(value) {
this._internal.value1 = value;
this.scheduleProcess();
}
},
value2: {
set(value) {
this._internal.value2 = value;
this.scheduleProcess();
}
}
},
methods: {
scheduleProcess() {
if (this._internal.scheduled) return;
this._internal.scheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
this._internal.scheduled = false;
this.processValues();
});
},
processValues() {
// Process both values together
}
}
Async Operations
Handle promises and async work:
inputs: {
fetch: {
type: 'signal',
valueChangedToTrue() {
this.doFetch();
}
}
},
methods: {
async doFetch() {
try {
const response = await fetch(this._internal.url);
const data = await response.json();
this._internal.result = data;
this.flagOutputDirty('result');
this.sendSignalOnOutput('success');
} catch (error) {
this._internal.error = error.message;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
}
}
}
Collection/Model Binding
Work with Noodl's data system:
const Collection = require('../../../collection');
const Model = require('../../../model');
inputs: {
items: {
type: 'array',
set(value) {
this.bindCollection(value);
}
}
},
methods: {
bindCollection(collection) {
// Unbind previous
if (this._internal.collection) {
this._internal.collection.off('change', this._internal.onChange);
}
this._internal.collection = collection;
if (collection) {
this._internal.onChange = () => {
this.flagOutputDirty('count');
};
collection.on('change', this._internal.onChange);
}
}
}
Dynamic Ports
Add ports based on configuration:
inputs: {
properties: {
type: { name: 'stringlist', allowEditOnly: true },
displayName: 'Properties',
set(value) {
// Register dynamic inputs/outputs based on list
value.forEach(prop => {
if (!this.hasInput('prop-' + prop)) {
this.registerInput('prop-' + prop, {
set(val) {
this._internal.values[prop] = val;
}
});
}
});
}
}
}
Input Types Reference
| Type | Description | Example |
|---|---|---|
string |
Text input | type: 'string' |
number |
Numeric input | type: 'number' |
boolean |
Toggle | type: 'boolean' |
color |
Color picker | type: 'color' |
signal |
Trigger/event | type: 'signal' |
array |
Array/collection | type: 'array' |
object |
Object/model | type: 'object' |
component |
Component reference | type: 'component' |
enum |
Dropdown selection | type: { name: 'enum', enums: [...] } |
stringlist |
Editable list | type: { name: 'stringlist' } |
number with units |
Dimension | type: { name: 'number', units: [...] } |
Node Colors
Available color themes for nodes:
data- Blue (data operations)logic- Purple (logic/control)visual- Green (UI elements)component- Orange (component utilities)default- Gray
Registering Nodes
Add to the node library export:
// In packages/noodl-runtime/src/nodelibraryexport.js
const MyNode = require('./nodes/my-node');
// Add to appropriate category in coreNodes array
Testing Nodes
// Example test structure
describe('MyNode', () => {
it('should process input correctly', () => {
const node = createNode('My.Custom.Node');
node.setInput('textInput', 'hello');
expect(node.getOutput('result')).toBe('HELLO');
});
});