mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
Finished HTTP node creation and extensive node creation documentation in project
This commit is contained in:
@@ -34,39 +34,39 @@ packages/noodl-runtime/src/nodes/std-library/
|
||||
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
|
||||
type: 'string', // See "Port Types" section below
|
||||
displayName: 'Input Name',
|
||||
group: 'General', // Group in property panel
|
||||
group: 'General', // Group in property panel
|
||||
default: 'default value'
|
||||
},
|
||||
doAction: {
|
||||
@@ -75,7 +75,7 @@ var MyNode = {
|
||||
group: 'Actions'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// REQUIRED: Define output ports
|
||||
outputs: {
|
||||
outputValue: {
|
||||
@@ -94,7 +94,7 @@ var MyNode = {
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// OPTIONAL: Methods to handle input changes
|
||||
methods: {
|
||||
setInputName: function (value) {
|
||||
@@ -102,19 +102,19 @@ var MyNode = {
|
||||
// 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') {
|
||||
@@ -126,7 +126,7 @@ var MyNode = {
|
||||
// REQUIRED: Export the node
|
||||
module.exports = {
|
||||
node: MyNode,
|
||||
|
||||
|
||||
// OPTIONAL: Setup function for dynamic ports
|
||||
setup: function (context, graphModel) {
|
||||
// See "Dynamic Ports" section below
|
||||
@@ -144,10 +144,10 @@ Add the node to the `registerNodes` function in `packages/noodl-runtime/noodl-ru
|
||||
function registerNodes(noodlRuntime) {
|
||||
[
|
||||
// ... existing nodes ...
|
||||
|
||||
|
||||
// Add your new node
|
||||
require('./src/nodes/std-library/data/mynode'),
|
||||
|
||||
require('./src/nodes/std-library/data/mynode')
|
||||
|
||||
// ... more nodes ...
|
||||
].forEach((node) => noodlRuntime.registerNode(node));
|
||||
}
|
||||
@@ -174,10 +174,10 @@ const coreNodes = [
|
||||
// ... other subcategories ...
|
||||
{
|
||||
name: 'External Data',
|
||||
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
||||
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
// ... more categories ...
|
||||
];
|
||||
```
|
||||
@@ -188,24 +188,24 @@ const coreNodes = [
|
||||
|
||||
### 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 |
|
||||
| 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 |
|
||||
| Type | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
|
||||
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
|
||||
| `{ name: 'number', min, max }` | Number with constraints |
|
||||
|
||||
### Example Enum Input
|
||||
|
||||
@@ -239,7 +239,7 @@ Dynamic ports are created at runtime based on configuration. This is useful when
|
||||
```javascript
|
||||
module.exports = {
|
||||
node: MyNode,
|
||||
|
||||
|
||||
setup: function (context, graphModel) {
|
||||
// Only run in editor, not deployed
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
@@ -248,7 +248,7 @@ module.exports = {
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [];
|
||||
|
||||
|
||||
// Always include base ports from node definition
|
||||
// Add dynamic ports based on parameters
|
||||
if (parameters.items) {
|
||||
@@ -262,7 +262,7 @@ module.exports = {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Send ports to editor
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
@@ -348,7 +348,7 @@ For asynchronous operations (API calls, file I/O), use standard async patterns:
|
||||
methods: {
|
||||
fetchTrigger: function () {
|
||||
const self = this;
|
||||
|
||||
|
||||
fetch(this._internal.url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -394,6 +394,348 @@ getInspectInfo() {
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CRITICAL GOTCHAS - READ BEFORE CREATING NODES
|
||||
|
||||
These issues will cause silent failures with NO error messages. They were discovered during the HTTP node debugging session (December 2024) and cost hours of debugging time.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 GOTCHA #1: Never Override `setInputValue` in prototypeExtensions
|
||||
|
||||
**THE BUG:**
|
||||
|
||||
```javascript
|
||||
// ❌ DEADLY - This silently breaks ALL signal inputs
|
||||
prototypeExtensions: {
|
||||
setInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value; // Signal setters NEVER called!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**WHY IT BREAKS:**
|
||||
|
||||
- `prototypeExtensions.setInputValue` **completely overrides** `Node.prototype.setInputValue`
|
||||
- The base method contains `input.set.call(this, value)` which triggers signal callbacks
|
||||
- Without it, signals never fire - NO errors, just silent failure
|
||||
|
||||
**THE FIX:**
|
||||
|
||||
```javascript
|
||||
// ✅ SAFE - Use a different method name for storing dynamic values
|
||||
prototypeExtensions: {
|
||||
_storeInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
},
|
||||
|
||||
registerInputIfNeeded: function (name) {
|
||||
// Register dynamic inputs with _storeInputValue, not setInputValue
|
||||
if (name.startsWith('body-')) {
|
||||
return this.registerInput(name, {
|
||||
set: this._storeInputValue.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 GOTCHA #2: Dynamic Ports REPLACE Static Ports
|
||||
|
||||
**THE BUG:**
|
||||
|
||||
```javascript
|
||||
// Node has static inputs defined in inputs: {}
|
||||
inputs: {
|
||||
url: { type: 'string', set: function(v) {...} },
|
||||
fetch: { type: 'signal', valueChangedToTrue: function() {...} }
|
||||
},
|
||||
|
||||
// But setup function only sends dynamic ports
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [
|
||||
{ name: 'headers', ... },
|
||||
{ name: 'queryParams', ... }
|
||||
];
|
||||
// ❌ MISSING url, fetch - they won't appear in editor!
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
```
|
||||
|
||||
**WHY IT BREAKS:**
|
||||
|
||||
- `sendDynamicPorts()` tells the editor "these are ALL the ports for this node"
|
||||
- Static `inputs` are NOT automatically merged
|
||||
- The editor only shows dynamic ports, connections to static ports fail
|
||||
|
||||
**THE FIX:**
|
||||
|
||||
```javascript
|
||||
// ✅ SAFE - Include ALL ports in dynamic ports array
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [];
|
||||
|
||||
// Dynamic configuration ports
|
||||
ports.push({ name: 'headers', type: {...}, plug: 'input' });
|
||||
ports.push({ name: 'queryParams', type: {...}, plug: 'input' });
|
||||
|
||||
// MUST include static ports too!
|
||||
ports.push({ name: 'url', type: 'string', plug: 'input', group: 'Request' });
|
||||
ports.push({ name: 'fetch', type: 'signal', plug: 'input', group: 'Actions' });
|
||||
ports.push({ name: 'cancel', type: 'signal', plug: 'input', group: 'Actions' });
|
||||
|
||||
// Include outputs too
|
||||
ports.push({ name: 'success', type: 'signal', plug: 'output', group: 'Events' });
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 GOTCHA #3: Configuration Inputs Need Explicit Registration
|
||||
|
||||
**THE BUG:**
|
||||
|
||||
```javascript
|
||||
// Dynamic ports send method, bodyType, etc. to editor
|
||||
ports.push({ name: 'method', type: { name: 'enum', ... } });
|
||||
ports.push({ name: 'bodyType', type: { name: 'enum', ... } });
|
||||
|
||||
// ❌ Values never reach runtime - no setter registered!
|
||||
// User selects POST in editor, but doFetch() always uses GET
|
||||
doFetch: function() {
|
||||
const method = this._internal.method || 'GET'; // Always undefined!
|
||||
}
|
||||
```
|
||||
|
||||
**WHY IT BREAKS:**
|
||||
|
||||
- Dynamic port values are sent to runtime as input values via `setInputValue`
|
||||
- But `registerInputIfNeeded` is only called for ports not in static `inputs`
|
||||
- If there's no setter, the value is lost
|
||||
|
||||
**THE FIX:**
|
||||
|
||||
```javascript
|
||||
// ✅ SAFE - Register setters for all config inputs
|
||||
prototypeExtensions: {
|
||||
// Setter methods
|
||||
setMethod: function (value) { this._internal.method = value || 'GET'; },
|
||||
setBodyType: function (value) { this._internal.bodyType = value; },
|
||||
setHeaders: function (value) { this._internal.headers = value || ''; },
|
||||
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) return;
|
||||
|
||||
// Configuration inputs - bind to their setters
|
||||
const configSetters = {
|
||||
method: this.setMethod.bind(this),
|
||||
bodyType: this.setBodyType.bind(this),
|
||||
headers: this.setHeaders.bind(this),
|
||||
timeout: this.setTimeout.bind(this)
|
||||
};
|
||||
|
||||
if (configSetters[name]) {
|
||||
return this.registerInput(name, { set: configSetters[name] });
|
||||
}
|
||||
|
||||
// Dynamic inputs for prefixed ports
|
||||
if (name.startsWith('body-') || name.startsWith('header-')) {
|
||||
return this.registerInput(name, {
|
||||
set: this._storeInputValue.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 GOTCHA #4: Signal Inputs Use `valueChangedToTrue`, Not `set`
|
||||
|
||||
**THE BUG:**
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - This won't trigger on signal
|
||||
inputs: {
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
set: function() {
|
||||
this.doFetch(); // Never called!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**WHY IT BREAKS:**
|
||||
|
||||
- Signal inputs don't use `set` - they use `valueChangedToTrue`
|
||||
- The runtime wraps signals with `EdgeTriggeredInput.createSetter()` which tracks state transitions
|
||||
- Signals only fire on FALSE → TRUE transition
|
||||
|
||||
**THE FIX:**
|
||||
|
||||
```javascript
|
||||
// ✅ CORRECT - Use valueChangedToTrue for signals
|
||||
inputs: {
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
type: 'signal',
|
||||
displayName: 'Cancel',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.cancelFetch();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 GOTCHA #5: Node Registration Path Matters
|
||||
|
||||
**THE BUG:**
|
||||
|
||||
- Nodes in `noodl-runtime/noodl-runtime.js` → Go through `defineNode()`
|
||||
- Nodes in `noodl-viewer-react/register-nodes.js` → Go through `defineNode()`
|
||||
- Raw node object passed directly → Does NOT go through `defineNode()`
|
||||
|
||||
**WHY IT MATTERS:**
|
||||
|
||||
- `defineNode()` in `nodedefinition.js` wraps signal inputs with `EdgeTriggeredInput.createSetter()`
|
||||
- Without `defineNode()`, signals are registered but never fire
|
||||
- The `{node, setup}` export format automatically calls `defineNode()`
|
||||
|
||||
**THE FIX:**
|
||||
|
||||
```javascript
|
||||
// ✅ CORRECT - Always export with {node, setup} format
|
||||
module.exports = {
|
||||
node: MyNode, // Goes through defineNode()
|
||||
setup: function (context, graphModel) {
|
||||
// Dynamic port setup
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ ALSO CORRECT - Call defineNode explicitly
|
||||
const NodeDefinition = require('./nodedefinition');
|
||||
module.exports = NodeDefinition.defineNode(MyNode);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Working Pattern (HTTP Node Reference)
|
||||
|
||||
Here's the proven pattern from the HTTP node that handles all gotchas:
|
||||
|
||||
```javascript
|
||||
var MyNode = {
|
||||
name: 'net.noodl.MyNode',
|
||||
displayNodeName: 'My Node',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {}; // For dynamic input storage
|
||||
this._internal.method = 'GET'; // Config defaults
|
||||
},
|
||||
|
||||
// Static inputs - signals and essential ports
|
||||
inputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
set: function (value) { this._internal.url = value; }
|
||||
},
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () { this.scheduleFetch(); }
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: { type: '*', getter: function() { return this._internal.response; } },
|
||||
success: { type: 'signal' },
|
||||
failure: { type: 'signal' }
|
||||
},
|
||||
|
||||
prototypeExtensions: {
|
||||
// Store dynamic values WITHOUT overriding setInputValue
|
||||
_storeInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
},
|
||||
|
||||
// Configuration setters
|
||||
setMethod: function (value) { this._internal.method = value || 'GET'; },
|
||||
setHeaders: function (value) { this._internal.headers = value || ''; },
|
||||
|
||||
// Register ALL dynamic inputs
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) return;
|
||||
|
||||
// Config inputs
|
||||
const configSetters = {
|
||||
method: this.setMethod.bind(this),
|
||||
headers: this.setHeaders.bind(this)
|
||||
};
|
||||
if (configSetters[name]) {
|
||||
return this.registerInput(name, { set: configSetters[name] });
|
||||
}
|
||||
|
||||
// Prefixed dynamic inputs
|
||||
if (name.startsWith('header-') || name.startsWith('body-')) {
|
||||
return this.registerInput(name, {
|
||||
set: this._storeInputValue.bind(this, name)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
scheduleFetch: function () {
|
||||
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
||||
},
|
||||
|
||||
doFetch: function () {
|
||||
const method = this._internal.method; // Now correctly captured!
|
||||
// ... fetch implementation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: MyNode,
|
||||
setup: function (context, graphModel) {
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [];
|
||||
|
||||
// Config ports
|
||||
ports.push({ name: 'method', type: { name: 'enum', enums: [...] }, plug: 'input' });
|
||||
ports.push({ name: 'headers', type: { name: 'stringlist' }, plug: 'input' });
|
||||
|
||||
// MUST include static ports!
|
||||
ports.push({ name: 'url', type: 'string', plug: 'input' });
|
||||
ports.push({ name: 'fetch', type: 'signal', plug: 'input' });
|
||||
|
||||
// Outputs
|
||||
ports.push({ name: 'response', type: '*', plug: 'output' });
|
||||
ports.push({ name: 'success', type: 'signal', plug: 'output' });
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
// ... rest of setup
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Node Not Appearing in Node Picker
|
||||
@@ -416,9 +758,13 @@ getInspectInfo() {
|
||||
|
||||
### Signal Not Firing
|
||||
|
||||
**Cause:** Method name doesn't match pattern `inputName + 'Trigger'`.
|
||||
**Cause #1:** Method name pattern wrong - use `valueChangedToTrue`, not `inputName + 'Trigger'`.
|
||||
|
||||
**Fix:** Ensure signal handler method is named correctly (e.g., `fetchTrigger` for input `fetch`).
|
||||
**Cause #2:** Custom `setInputValue` overriding base - see GOTCHA #1.
|
||||
|
||||
**Cause #3:** Signal not in dynamic ports - see GOTCHA #2.
|
||||
|
||||
**Fix:** Review ALL gotchas above!
|
||||
|
||||
---
|
||||
|
||||
@@ -437,14 +783,14 @@ getInspectInfo() {
|
||||
|
||||
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 |
|
||||
| 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*
|
||||
_Last Updated: December 2024_
|
||||
|
||||
@@ -852,6 +852,145 @@ const handleTreeMouseUp = useCallback(() => {
|
||||
|
||||
---
|
||||
|
||||
## 🔥 CRITICAL: Runtime Node Creation - The Unholy Trinity of Silent Failures (Dec 2025)
|
||||
|
||||
### The HTTP Node Debugging Saga: 3+ Hours of Silent Failures
|
||||
|
||||
**Context**: Creating an HTTP node (httpnode.js) as a modern, no-code-friendly alternative to the script-based REST node. Everything looked correct but nothing worked. Signals never fired, config values never set, and no errors anywhere.
|
||||
|
||||
**The Problems Found** (each took significant debugging time):
|
||||
|
||||
#### Problem 1: Signal Input Using `set` Instead of `valueChangedToTrue`
|
||||
|
||||
```javascript
|
||||
// ❌ WHAT I WROTE (never triggers)
|
||||
inputs: {
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
set: function(value) {
|
||||
this.scheduleFetch(); // ☠️ Never called
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ WHAT IT SHOULD BE
|
||||
inputs: {
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function() {
|
||||
this.scheduleFetch(); // ✓ Works!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Signals use `EdgeTriggeredInput.createSetter()` which wraps the callback and only calls `valueChangedToTrue` when value transitions from falsy to truthy. The `set` callback is never used.
|
||||
|
||||
#### Problem 2: Custom `setInputValue` Overriding Base Method
|
||||
|
||||
```javascript
|
||||
// ❌ WHAT I WROTE (breaks ALL inputs including signals)
|
||||
prototypeExtensions: {
|
||||
setInputValue: function(name, value) {
|
||||
this._internal.inputValues[name] = value; // ☠️ Overrides Node.prototype.setInputValue
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ WHAT IT SHOULD BE
|
||||
prototypeExtensions: {
|
||||
_storeInputValue: function(name, value) { // Different name!
|
||||
this._internal.inputValues[name] = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: `prototypeExtensions` methods are merged into node prototype. Defining `setInputValue` completely replaces the base implementation, which handles signal detection, input.set() calls, and event emission.
|
||||
|
||||
#### Problem 3: Dynamic Ports Replacing Static Ports
|
||||
|
||||
```javascript
|
||||
// ❌ WHAT I WROTE (static ports disappear)
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [];
|
||||
// Only add dynamic header/query param ports...
|
||||
if (parameters.headers) { /* add header ports */ }
|
||||
editorConnection.sendDynamicPorts(nodeId, ports); // Static inputs GONE!
|
||||
}
|
||||
|
||||
// ✅ WHAT IT SHOULD BE
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [
|
||||
// Re-add ALL static inputs
|
||||
{ name: 'url', displayName: 'URL', type: 'string', plug: 'input', group: 'Request' },
|
||||
{ name: 'fetch', displayName: 'Fetch', type: 'signal', plug: 'input', group: 'Actions' },
|
||||
{ name: 'cancel', displayName: 'Cancel', type: 'signal', plug: 'input', group: 'Actions' },
|
||||
// Then dynamic ports...
|
||||
];
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: `sendDynamicPorts` REPLACES all ports, not merges. The `inputs` object in node definition is only for default setup - once dynamic ports are sent, they're the only ports.
|
||||
|
||||
#### Problem 4: Config Inputs (StringList/Enum) Need Explicit Registration
|
||||
|
||||
```javascript
|
||||
// ❌ MISSING (config values never reach setters)
|
||||
// StringList inputs like "headers", "queryParams" appear in editor but their
|
||||
// values never reach the node because there's no registered input handler
|
||||
|
||||
// ✅ WHAT IT NEEDS
|
||||
registerInputIfNeeded: function(name) {
|
||||
if (this.hasInput(name)) return;
|
||||
|
||||
const configSetters = {
|
||||
'method': this.setMethod.bind(this),
|
||||
'headers': this.setHeaders.bind(this),
|
||||
'queryParams': this.setQueryParams.bind(this),
|
||||
'bodyType': this.setBodyType.bind(this),
|
||||
// ... all config inputs
|
||||
};
|
||||
|
||||
if (configSetters[name]) {
|
||||
return this.registerInput(name, { set: configSetters[name] });
|
||||
}
|
||||
|
||||
// Dynamic inputs (header-X, query-Y, etc.)
|
||||
if (name.startsWith('header-')) {
|
||||
return this.registerInput(name, {
|
||||
set: this._storeInputValue.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Inputs defined in the `inputs` object get registered automatically. Dynamic ports don't - they need `registerInputIfNeeded` to create runtime handlers.
|
||||
|
||||
**Why This Was So Hard to Debug**:
|
||||
|
||||
1. **No errors** - Everything appeared to work, logs said "success", but nothing happened
|
||||
2. **Partial success** - Some things worked (node appeared in palette) making it seem close
|
||||
3. **Multiple bugs** - Each fix revealed the next bug, each taking time to diagnose
|
||||
4. **No TypeScript** - Runtime code is JS, no compile-time checking
|
||||
5. **Unfamiliar patterns** - `valueChangedToTrue`, `registerInputIfNeeded`, etc. aren't obvious
|
||||
|
||||
**Time Lost**: 3+ hours debugging what should have been a straightforward node
|
||||
|
||||
**Prevention**:
|
||||
|
||||
- Created `dev-docs/reference/LEARNINGS-NODE-CREATION.md` with Critical Gotchas section
|
||||
- Added Node Creation Checklist to `.clinerules` Section 14
|
||||
- These gotchas are now documented with THE BUG / WHY IT BREAKS / THE FIX patterns
|
||||
|
||||
**Location**:
|
||||
|
||||
- `packages/noodl-runtime/src/nodes/std-library/data/httpnode.js` (fixed)
|
||||
- `dev-docs/reference/LEARNINGS-NODE-CREATION.md` (new documentation)
|
||||
|
||||
**Keywords**: node creation, signal, valueChangedToTrue, setInputValue, prototypeExtensions, sendDynamicPorts, registerInputIfNeeded, stringlist, enum, config inputs, HTTP node, runtime
|
||||
|
||||
---
|
||||
|
||||
## 🔥 CRITICAL: PopupLayer.dragCompleted() - Not endDrag() (Dec 2025)
|
||||
|
||||
### Wrong Method Name Causes TypeError
|
||||
|
||||
Reference in New Issue
Block a user