diff --git a/dev-docs/reference/LEARNINGS-NODE-CREATION.md b/dev-docs/reference/LEARNINGS-NODE-CREATION.md new file mode 100644 index 0000000..74a8301 --- /dev/null +++ b/dev-docs/reference/LEARNINGS-NODE-CREATION.md @@ -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*