mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
docs: add comprehensive guide for creating Noodl nodes
Documents the complete process learned from building the HTTP Request node: - Node file structure and required properties - Registration in noodl-runtime.js - Adding to nodelibraryexport.js for Node Picker visibility - Port types, dynamic ports, signals, async operations - Common issues and troubleshooting
This commit is contained in:
450
dev-docs/reference/LEARNINGS-NODE-CREATION.md
Normal file
450
dev-docs/reference/LEARNINGS-NODE-CREATION.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# Creating Nodes in OpenNoodl
|
||||||
|
|
||||||
|
This guide documents the complete process for creating new nodes in the OpenNoodl runtime, based on learnings from building the HTTP Request node.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Nodes in Noodl are defined in the `noodl-runtime` package and need to be:
|
||||||
|
|
||||||
|
1. **Created** - Define the node in a `.js` file
|
||||||
|
2. **Registered** - Add to `noodl-runtime.js`
|
||||||
|
3. **Indexed** - Add to `nodelibraryexport.js` for Node Picker visibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Create the Node File
|
||||||
|
|
||||||
|
Create a new file in the appropriate category folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/noodl-runtime/src/nodes/std-library/
|
||||||
|
├── data/ # Data nodes (REST, HTTP, collections)
|
||||||
|
├── variables/ # Variable nodes (string, number, boolean)
|
||||||
|
├── user/ # User authentication nodes
|
||||||
|
└── *.js # General utility nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Node Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MyNode = {
|
||||||
|
// REQUIRED: Unique identifier for the node
|
||||||
|
name: 'net.noodl.MyNode',
|
||||||
|
|
||||||
|
// REQUIRED: Display name in Node Picker and canvas
|
||||||
|
displayNodeName: 'My Node',
|
||||||
|
|
||||||
|
// OPTIONAL: Documentation URL
|
||||||
|
docs: 'https://docs.noodl.net/nodes/category/my-node',
|
||||||
|
|
||||||
|
// REQUIRED: Category for organization (Data, Visual, Logic, etc.)
|
||||||
|
category: 'Data',
|
||||||
|
|
||||||
|
// OPTIONAL: Node color theme
|
||||||
|
// Options: 'data' (green), 'visual' (blue), 'component' (purple), 'javascript' (pink), 'default' (gray)
|
||||||
|
color: 'data',
|
||||||
|
|
||||||
|
// OPTIONAL: Search keywords for Node Picker
|
||||||
|
searchTags: ['my', 'node', 'custom', 'example'],
|
||||||
|
|
||||||
|
// OPTIONAL: Called when node instance is created
|
||||||
|
initialize: function () {
|
||||||
|
this._internal.myData = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// OPTIONAL: Data shown in debug inspector
|
||||||
|
getInspectInfo() {
|
||||||
|
return this._internal.inspectData;
|
||||||
|
},
|
||||||
|
|
||||||
|
// REQUIRED: Define input ports
|
||||||
|
inputs: {
|
||||||
|
inputName: {
|
||||||
|
type: 'string', // See "Port Types" section below
|
||||||
|
displayName: 'Input Name',
|
||||||
|
group: 'General', // Group in property panel
|
||||||
|
default: 'default value'
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: 'signal',
|
||||||
|
displayName: 'Do Action',
|
||||||
|
group: 'Actions'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// REQUIRED: Define output ports
|
||||||
|
outputs: {
|
||||||
|
outputValue: {
|
||||||
|
type: 'string',
|
||||||
|
displayName: 'Output Value',
|
||||||
|
group: 'Results'
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: 'signal',
|
||||||
|
displayName: 'Success',
|
||||||
|
group: 'Events'
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
type: 'signal',
|
||||||
|
displayName: 'Failure',
|
||||||
|
group: 'Events'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// OPTIONAL: Methods to handle input changes
|
||||||
|
methods: {
|
||||||
|
setInputName: function (value) {
|
||||||
|
this._internal.inputName = value;
|
||||||
|
// Optionally trigger output update
|
||||||
|
this.flagOutputDirty('outputValue');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Signal handler - name must match input name with 'Trigger' suffix
|
||||||
|
doActionTrigger: function () {
|
||||||
|
// Perform the action
|
||||||
|
const result = this.processInput(this._internal.inputName);
|
||||||
|
this._internal.outputValue = result;
|
||||||
|
|
||||||
|
// Update outputs
|
||||||
|
this.flagOutputDirty('outputValue');
|
||||||
|
this.sendSignalOnOutput('success');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// OPTIONAL: Return output values
|
||||||
|
getOutputValue: function (name) {
|
||||||
|
if (name === 'outputValue') {
|
||||||
|
return this._internal.outputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// REQUIRED: Export the node
|
||||||
|
module.exports = {
|
||||||
|
node: MyNode,
|
||||||
|
|
||||||
|
// OPTIONAL: Setup function for dynamic ports
|
||||||
|
setup: function (context, graphModel) {
|
||||||
|
// See "Dynamic Ports" section below
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Register the Node
|
||||||
|
|
||||||
|
Add the node to the `registerNodes` function in `packages/noodl-runtime/noodl-runtime.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function registerNodes(noodlRuntime) {
|
||||||
|
[
|
||||||
|
// ... existing nodes ...
|
||||||
|
|
||||||
|
// Add your new node
|
||||||
|
require('./src/nodes/std-library/data/mynode'),
|
||||||
|
|
||||||
|
// ... more nodes ...
|
||||||
|
].forEach((node) => noodlRuntime.registerNode(node));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** The order in this array doesn't matter, but group related nodes together for readability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Add to Node Picker Index
|
||||||
|
|
||||||
|
**CRITICAL:** This step is often forgotten! Without it, the node won't appear in the Node Picker.
|
||||||
|
|
||||||
|
Edit `packages/noodl-runtime/src/nodelibraryexport.js` and add your node to the appropriate category in the `coreNodes` array:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const coreNodes = [
|
||||||
|
// ... other categories ...
|
||||||
|
{
|
||||||
|
name: 'Read & Write Data',
|
||||||
|
description: 'Arrays, objects, cloud data',
|
||||||
|
type: 'data',
|
||||||
|
subCategories: [
|
||||||
|
// ... other subcategories ...
|
||||||
|
{
|
||||||
|
name: 'External Data',
|
||||||
|
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// ... more categories ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Port Types
|
||||||
|
|
||||||
|
### Common Input/Output Types
|
||||||
|
|
||||||
|
| Type | Description | Example Use |
|
||||||
|
|------|-------------|-------------|
|
||||||
|
| `string` | Text value | URLs, names, content |
|
||||||
|
| `number` | Numeric value | Counts, sizes, coordinates |
|
||||||
|
| `boolean` | True/false | Toggles, conditions |
|
||||||
|
| `signal` | Trigger without data | Action buttons, events |
|
||||||
|
| `object` | JSON object | API responses, data structures |
|
||||||
|
| `array` | List of items | Collections, results |
|
||||||
|
| `color` | Color value | Styling |
|
||||||
|
| `*` | Any type | Generic ports |
|
||||||
|
|
||||||
|
### Input-Specific Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
|
||||||
|
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
|
||||||
|
| `{ name: 'number', min, max }` | Number with constraints |
|
||||||
|
|
||||||
|
### Example Enum Input
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
inputs: {
|
||||||
|
method: {
|
||||||
|
type: {
|
||||||
|
name: 'enum',
|
||||||
|
enums: [
|
||||||
|
{ value: 'GET', label: 'GET' },
|
||||||
|
{ value: 'POST', label: 'POST' },
|
||||||
|
{ value: 'PUT', label: 'PUT' },
|
||||||
|
{ value: 'DELETE', label: 'DELETE' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
displayName: 'Method',
|
||||||
|
default: 'GET',
|
||||||
|
group: 'Request'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dynamic Ports
|
||||||
|
|
||||||
|
Dynamic ports are created at runtime based on configuration. This is useful when the number or names of ports depend on user settings.
|
||||||
|
|
||||||
|
### Setup Function Pattern
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
node: MyNode,
|
||||||
|
|
||||||
|
setup: function (context, graphModel) {
|
||||||
|
// Only run in editor, not deployed
|
||||||
|
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [];
|
||||||
|
|
||||||
|
// Always include base ports from node definition
|
||||||
|
// Add dynamic ports based on parameters
|
||||||
|
if (parameters.items) {
|
||||||
|
parameters.items.split(',').forEach((item) => {
|
||||||
|
ports.push({
|
||||||
|
name: 'item-' + item.trim(),
|
||||||
|
displayName: item.trim(),
|
||||||
|
type: 'string',
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Items'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send ports to editor
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
|
||||||
|
function managePortsForNode(node) {
|
||||||
|
updatePorts(node.id, node.parameters || {}, context.editorConnection);
|
||||||
|
|
||||||
|
node.on('parameterUpdated', function (event) {
|
||||||
|
if (event.name === 'items') {
|
||||||
|
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for graph import completion
|
||||||
|
graphModel.on('editorImportComplete', () => {
|
||||||
|
// Listen for new nodes of this type
|
||||||
|
graphModel.on('nodeAdded.net.noodl.MyNode', function (node) {
|
||||||
|
managePortsForNode(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle existing nodes
|
||||||
|
for (const node of graphModel.getNodesWithType('net.noodl.MyNode')) {
|
||||||
|
managePortsForNode(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handling Signals
|
||||||
|
|
||||||
|
Signals are trigger-based ports (no data, just an event).
|
||||||
|
|
||||||
|
### Receiving Signals (Input)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In methods object
|
||||||
|
methods: {
|
||||||
|
// Pattern: inputName + 'Trigger'
|
||||||
|
fetchTrigger: function () {
|
||||||
|
// Called when 'fetch' signal is triggered
|
||||||
|
this.doFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending Signals (Output)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Send a signal pulse
|
||||||
|
this.sendSignalOnOutput('success');
|
||||||
|
this.sendSignalOnOutput('failure');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating Outputs
|
||||||
|
|
||||||
|
When an output value changes, you must flag it as dirty:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Flag a single output
|
||||||
|
this.flagOutputDirty('outputValue');
|
||||||
|
|
||||||
|
// Flag multiple outputs
|
||||||
|
this.flagOutputDirty('response');
|
||||||
|
this.flagOutputDirty('statusCode');
|
||||||
|
|
||||||
|
// Then send signal if needed
|
||||||
|
this.sendSignalOnOutput('complete');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Async Operations
|
||||||
|
|
||||||
|
For asynchronous operations (API calls, file I/O), use standard async patterns:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
methods: {
|
||||||
|
fetchTrigger: function () {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
fetch(this._internal.url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
self._internal.response = data;
|
||||||
|
self.flagOutputDirty('response');
|
||||||
|
self.sendSignalOnOutput('success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
self._internal.error = error.message;
|
||||||
|
self.flagOutputDirty('error');
|
||||||
|
self.sendSignalOnOutput('failure');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Inspector
|
||||||
|
|
||||||
|
Provide data for the debug inspector popup:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
getInspectInfo() {
|
||||||
|
// Return an array of objects with type and value
|
||||||
|
return [
|
||||||
|
{ type: 'text', value: 'Status: ' + this._internal.status },
|
||||||
|
{ type: 'value', value: this._internal.response }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Your Node
|
||||||
|
|
||||||
|
1. Start the dev server: `npm run dev`
|
||||||
|
2. Open the Node Picker (click in the node graph)
|
||||||
|
3. Search for your node by name or search tags
|
||||||
|
4. Navigate to the category to verify placement
|
||||||
|
5. Add the node and test inputs/outputs
|
||||||
|
6. Check console for any errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Node Not Appearing in Node Picker
|
||||||
|
|
||||||
|
**Cause:** Node not added to `nodelibraryexport.js` coreNodes array.
|
||||||
|
|
||||||
|
**Fix:** Add the node name to the appropriate subcategory items array.
|
||||||
|
|
||||||
|
### "Cannot read property of undefined" Errors
|
||||||
|
|
||||||
|
**Cause:** Accessing `this._internal` before `initialize()` runs.
|
||||||
|
|
||||||
|
**Fix:** Always check for undefined or initialize values in `initialize()`.
|
||||||
|
|
||||||
|
### Outputs Not Updating
|
||||||
|
|
||||||
|
**Cause:** Forgot to call `flagOutputDirty()`.
|
||||||
|
|
||||||
|
**Fix:** Call `this.flagOutputDirty('portName')` after setting internal value.
|
||||||
|
|
||||||
|
### Signal Not Firing
|
||||||
|
|
||||||
|
**Cause:** Method name doesn't match pattern `inputName + 'Trigger'`.
|
||||||
|
|
||||||
|
**Fix:** Ensure signal handler method is named correctly (e.g., `fetchTrigger` for input `fetch`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Checklist for New Nodes
|
||||||
|
|
||||||
|
- [ ] Create node file in `packages/noodl-runtime/src/nodes/std-library/[category]/`
|
||||||
|
- [ ] Add `require()` to `packages/noodl-runtime/noodl-runtime.js`
|
||||||
|
- [ ] Add node name to `packages/noodl-runtime/src/nodelibraryexport.js` coreNodes
|
||||||
|
- [ ] Test node appears in Node Picker
|
||||||
|
- [ ] Test all inputs/outputs work correctly
|
||||||
|
- [ ] Verify debug inspector shows useful info
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
|
||||||
|
When creating new nodes, reference these existing nodes for patterns:
|
||||||
|
|
||||||
|
| Node | File | Good Example Of |
|
||||||
|
|------|------|-----------------|
|
||||||
|
| REST | `data/restnode.js` | Full-featured data node with scripts |
|
||||||
|
| HTTP | `data/httpnode.js` | Dynamic ports, configuration |
|
||||||
|
| String | `variables/string.js` | Simple variable node |
|
||||||
|
| Counter | `counter.js` | Stateful logic node |
|
||||||
|
| Condition | `condition.js` | Boolean logic |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: December 2024*
|
||||||
Reference in New Issue
Block a user