Best practice: custom v. function node

Hi There!

Are there best practices for creating a custom node versus writing a function node?

I recently created the filetype node and realised that its a very thin wrapper around the file-type package - in fact there would be more flexibility if it was a function node.

That made me think about the difference between the two:

  • custom node can be colourised and iconified, function node not
  • ESM packages can be used with custom nodes not with function nodes (at least not via the 'Setup' tab)
  • custom nodes can be distributed directly, function node can only be distributed via flows (subflows)
  • depended package versions can be specified with a custom node, version 'latest' is assumed when using a function node (again via the setup tab)
  • JS code can be split amongst many files using custom nodes, function nodes have only external packages as dependencies.

Of course this list ignores config nodes and sidebar nodes, this is a comparison between "palette" custom node and a function node.

Are there any other differences? Is there a guide available anywhere?

(My personal preference is for creating a custom node because of reusability and distributability but I have come to realise that a function node is just as easily reused amongst flows - export & import make that very simple.)

Addon:

  • custom node usually has an Edit property dialog window for configuration; function node may (only) be configured by magic (= commanding) msg properties.
1 Like

Good point :+1:

or hardcoding code changes in the function node ... I find myself continually copy and pasting function node and extending/modifying them as needed. I've began to think that as opposed to textual programming, copy&paste in a visual programming context is actually ok. I.e., a certain amount of code duplication is ok.

Also

  • Custom node can be used by those who do not have the skills to use a Function node or do not have the expertise in the appropriate area to know how to achieve the desired result.
  • Functionality is (hopefully) already well tested in a custom node so even experienced developers do not have to think about coping with edge conditions.
2 Likes

I would group that under usability: custom nodes are simpler to use for non-developers or folks just starting out with Node-RED.

Unfortunately I've been guilty of shipping broken custom nodes :-/ - that's a downside that the turnaround time in fixing a broken custom node can be rather longer than desired plus the end-users need to update the node. Function nodes can be fixed by their users - so fixability (or right to repair) is definitely simpler with function nodes.

Thank you for those points :+1:

This is not really sustainable. I would always recommend accessing Function Nodes with common functionality via a link-call (or subflow but you already mentioned that)

Why not? If the ESM module has Default exports, then they can be used. (Under the hood, dynamic import is used to import function node modules)

IMHO, a few points have been slightly mis-stated in this discussion.

The function node can be iconified in the Appearance tab. I remember discussion to the effect that changing color could cause the node not to be recognized by the user as a function.

Additionally, the code can be saved in the Library and distributed as text.

Configuration can also be done in the On Start tab, subject to the need to use context to copy settings to the On Message code.

Without trying to get into the heads of the core developers, I suspect that some of these features were added to the function node in order to make it more like (as capable as) a custom node. To a degree, both are departures from the low-code/no-code philosophy of Node-RED.

1 Like

Mea culpa, indeed I forgot that - cheers for pointing that out :+1:

I find the function node to be exactly right - for me Node-RED is hidden low-code and that's good. I would hate to code a function node purely using existing nodes. I think Node-RED has a good mix between connecting nodes and extending the basis functionality using hidden extension/code in nodes.

For me, a no-code solution is ok if you have a very limited functionality, something like Blender nodes for shading or Retrobatch for batch photo conversion/manipulation. Node-RED is far too generalisable and extendable to be a no-code solution.

2 Likes

And that is one of the reasons it exists of course. Node-RED is a general purpose low code development tool. It can be used for all manner of things and by all manner of people with different experience levels.

As a reasonably experienced JavaScript programmer, I often find things much easier to accomplish in a function node than having to use a bunch of other nodes. Though I might experiment with a flow using nodes and after a year or so get fed up with the number of nodes cluttering the screen so wrap them all up into a function node or two. I also regularly find myself using function nodes as simple wrappers around 3rd-party libraries - especially for things like CSV/Excel file handling.

So I'll use Node-RED both as a prototyping platform and as a live platform because I'm too lazy to faff with all the boilerplate code likely to be needed to stand up even a simple microservice using Node.js or Python.

But of course, for many, people, it is the simplicity of the node-and-wire based approach - avoiding code completely - that attracts them.

And for all of us, having access to thousands of specialist nodes that wrap up a level of complexity, makes life very easy. This is one of the reasons that I wrote UIBUILDER. To hide most of the complexity of dealing with the DOM and with 2-way, realtime communications between the client and the server. But without having to commit to an equally complex front-end framework with all its own quirks and issues and that takes you away from learning about vanilla HTML/CSS/JavaScript so leaving you at the mercy of the development of the framework. UIBUILDER is therefore, I suppose, like the function node of web development in Node-RED. :grinning:

Anyway, coming back on-topic. BOTH custom nodes and function nodes have their - well defined - place in the Node-RED universe.

However, what I would say, now that I've a number of complex custom nodes under my belt, is that developing nodes, while fairly straight-forwards, is time consuming, very time consuming in fact. And on this note, I've been playing with some ideas on how the time and complexity to create custom nodes could be reduced. See some of those ideas taking form in An idea for third-party UI in ui-builder - Developing Nodes - Node-RED Forum (nodered.org). Where I start to try and go beyond uibuilder to think about how the experience could be improved for all.

Anyway, that is a different topic so I'll stop here.

2 Likes

I think it would be nice to define exactly which should be used when, it is confusing and to have a clear divide would be helpful - IMHO. I mean once we start, then there also flows and subflows - both do the same as custom nodes and function nodes: extend Node-RED functionality.

And therefore I've started a decision tree flow:

[{"id":"104818a352e4dbd7","type":"subflow","name":"dectree node - Done/Result.","info":"","category":"decision tree","in":[{"x":198,"y":175,"wires":[{"id":"4adc25c3f4071a67"},{"id":"1f6121378b57e435"}]}],"out":[],"env":[{"name":"result","type":"str","value":""}],"meta":{},"color":"#87A980","icon":"font-awesome/fa-check"},{"id":"4adc25c3f4071a67","type":"change","z":"104818a352e4dbd7","name":"","rules":[{"t":"set","p":"question","pt":"msg","to":"result","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":421,"y":272,"wires":[["137eec287dc9b928"]]},{"id":"137eec287dc9b928","type":"function","z":"104818a352e4dbd7","name":"define and stringify choices","func":"msg.choices = [\n]\nmsg._choices = JSON.stringify(msg.choices)\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":721,"y":382,"wires":[["d270593519bddf9c"]]},{"id":"d270593519bddf9c","type":"link call","z":"104818a352e4dbd7","name":"","links":["42fa137c805ffa57"],"linkType":"static","timeout":"30","x":1027,"y":534,"wires":[[]]},{"id":"1f6121378b57e435","type":"change","z":"104818a352e4dbd7","name":"","rules":[{"t":"set","p":"enabled","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":461,"y":146,"wires":[["89e90816434341cc"]]},{"id":"89e90816434341cc","type":"link call","z":"104818a352e4dbd7","name":"","links":["cfcd2e897d5c0486"],"linkType":"static","timeout":"30","x":754,"y":98,"wires":[[]]},{"id":"fbfaa81b6a6dfb52","type":"catch","z":"104818a352e4dbd7","name":"","scope":null,"uncaught":false,"x":997,"y":47,"wires":[[]]},{"id":"08e99cbd5f4d9de5","type":"subflow","name":"dectree node - yes/no","info":"","category":"decision tree","in":[{"x":153,"y":137,"wires":[{"id":"546930450ce8938c"}]}],"out":[{"x":1109,"y":559,"wires":[{"id":"9e35b3119e821a39","port":0}]},{"x":1134,"y":655,"wires":[{"id":"9e35b3119e821a39","port":1}]}],"env":[{"name":"question","type":"str","value":""}],"meta":{},"color":"#E2D96E","outputLabels":["Yes","No"],"icon":"font-awesome/fa-question"},{"id":"7b93f6ae1b50b506","type":"link call","z":"08e99cbd5f4d9de5","name":"","links":["42fa137c805ffa57"],"linkType":"static","timeout":"30","x":603,"y":439,"wires":[["28402ef1afc40367"]]},{"id":"23bad6a3d8f3f08b","type":"debug","z":"08e99cbd5f4d9de5","name":"NO CHOICE MATCHED","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1119,"y":905,"wires":[]},{"id":"546930450ce8938c","type":"function","z":"08e99cbd5f4d9de5","name":"define and stringify choices","func":"msg.question = env.get(\"question\")\n\nmsg.choices = [\n    {\n        value: \"1\",\n        text: \"Yes\"\n    },\n    {\n        value: \"2\",\n        text: \"No\"\n    }\n]\nmsg._choices = JSON.stringify(msg.choices)\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":643,"y":329,"wires":[["7b93f6ae1b50b506"]]},{"id":"9e35b3119e821a39","type":"function","z":"08e99cbd5f4d9de5","name":"function 27","func":"let choices = JSON.parse(msg._choices);\n\nlet rAry = Array.from({ length: choices.length }, () => { \n    return undefined \n})\n\nfor ( var idx = 0 ; idx < choices.length; idx++ ) {\n    if ( msg.payload == choices[idx].value ) {\n        delete msg.payload;\n        delete msg.__linkSource;\n        delete msg._choices;\n        \n        rAry[idx] = msg;\n        return rAry;\n    }\n}\n\nreturn rAry.concat([msg]);","outputs":3,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":776,"y":631,"wires":[[],[],["23bad6a3d8f3f08b"]]},{"id":"28402ef1afc40367","type":"switch","z":"08e99cbd5f4d9de5","name":"","property":"__linkSource","propertyType":"msg","rules":[{"t":"nnull"}],"checkall":"true","repair":false,"outputs":1,"x":676,"y":540,"wires":[["9e35b3119e821a39"]]},{"id":"1211ca9a99229556","type":"catch","z":"08e99cbd5f4d9de5","name":"","scope":null,"uncaught":false,"x":926,"y":105,"wires":[[]]},{"id":"e51c499288aa059c","type":"tab","label":"[Decision tree] Which Node-RED extension should I use?","disabled":false,"info":"::: aim\n\nCreate a decision tree to help decision making.\n\n:::\n","env":[]},{"id":"6c956af2b71922a1","type":"group","z":"e51c499288aa059c","name":"Dashboard element for displaying the questions and buttons","style":{"label":true},"nodes":["e26a30ff2809e98b","0d73bc93ea491552","42fa137c805ffa57","021a75e9de1c8f9c","03a875dc9fc37e55","99db60304c670cc9"],"x":83.50003051757812,"y":1783.5001831054688,"w":982.3334655761719,"h":240.83319091796875},{"id":"56847658385cd98e","type":"group","z":"e51c499288aa059c","name":"visualise markdown content in info box","style":{"label":true},"nodes":["d07aa0c3b830ccf1","06a42fdc032c539f","605bc2ca67a50994","9df58b6177897325","3242ad96df5ac7da"],"x":85,"y":1569,"w":424,"h":192},{"id":"2822351c6b8f8bdd","type":"junction","z":"e51c499288aa059c","x":2617.571408033371,"y":1136.85711145401,"wires":[["afb3d0f568acb3ae","4ee2308ab360bec7"]]},{"id":"e26a30ff2809e98b","type":"ui_template","z":"e51c499288aa059c","g":"6c956af2b71922a1","group":"97885fc627878fef","name":"show question","order":8,"width":12,"height":7,"format":"","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":658.1666870117188,"y":1824.5001831054688,"wires":[["99db60304c670cc9"]]},{"id":"0d73bc93ea491552","type":"template","z":"e51c499288aa059c","g":"6c956af2b71922a1","name":"","field":"template","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<div style=\"font-size: 130%; font-weight: bold; margin-bottom: 30px; display: flex; justify-content: center;\">\n<span>{{{ question}}}</span>\n</div>\n\n{{#choices}}\n{{#text}}\n<md-button ng-click=\"send({ payload:'{{value}}', __linkSource: '{{__linkSource}}', _choices: '{{ _choices }}' })\">\n    {{text}}\n</md-button>\n{{/text}}\n{{/choices}}","output":"str","x":481.50018310546875,"y":1824.5001831054688,"wires":[["e26a30ff2809e98b"]]},{"id":"42fa137c805ffa57","type":"link in","z":"e51c499288aa059c","g":"6c956af2b71922a1","name":"decision tree ui","links":[],"x":124.50003051757812,"y":1884.5000762939453,"wires":[["03a875dc9fc37e55"]]},{"id":"021a75e9de1c8f9c","type":"link out","z":"e51c499288aa059c","g":"6c956af2b71922a1","name":"link out 124","mode":"return","links":[],"x":1024.83349609375,"y":1884.5000762939453,"wires":[]},{"id":"03a875dc9fc37e55","type":"function","z":"e51c499288aa059c","g":"6c956af2b71922a1","name":"prepare copy of link source \\n and htmlify the question","func":"msg.__linkSource = JSON.stringify(msg._linkSource)\n\nmsg.question = msg.question.replaceAll(\"\\\\n\",\"<br>\")\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":297.16668701171875,"y":1971.3333740234375,"wires":[["0d73bc93ea491552"]]},{"id":"99db60304c670cc9","type":"function","z":"e51c499288aa059c","g":"6c956af2b71922a1","name":"prepare the link source to \\n return to the original link call node","func":"if (msg.__linkSource && msg.payload ) {\n    msg._linkSource = JSON.parse(msg.__linkSource)\n}\n\nif (msg._linkSource) { return msg }\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":851.1666870117188,"y":1971.3333740234375,"wires":[["021a75e9de1c8f9c"]]},{"id":"51621964393c5d99","type":"ui_button","z":"e51c499288aa059c","name":"","group":"97885fc627878fef","order":4,"width":8,"height":2,"passthru":false,"label":"start / restart","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"date","topic":"topic","topicType":"msg","x":256,"y":886,"wires":[["83537c84badd2f0e","d38b15bd49a920cd","0536aa265d220f9e"]]},{"id":"59e70b48e3ac5385","type":"change","z":"e51c499288aa059c","name":"","rules":[{"t":"set","p":"enabled","pt":"msg","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":365,"y":757,"wires":[["51621964393c5d99"]]},{"id":"83537c84badd2f0e","type":"delay","z":"e51c499288aa059c","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":481,"y":886,"wires":[["59e70b48e3ac5385"]]},{"id":"cfcd2e897d5c0486","type":"link in","z":"e51c499288aa059c","name":"input to start button","links":[],"x":99,"y":954,"wires":[["51621964393c5d99","5bc239bb80c26247"]]},{"id":"5bc239bb80c26247","type":"link out","z":"e51c499288aa059c","name":"link out 125","mode":"return","links":[],"x":187,"y":1016,"wires":[]},{"id":"45671ba13d950f7c","type":"inject","z":"e51c499288aa059c","name":"enable button","props":[{"p":"enabled","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":142,"y":1072,"wires":[["51621964393c5d99"]]},{"id":"d38b15bd49a920cd","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Is Node-RED missing functionality?","env":[{"name":"question","value":"Is Node-RED missing functionality?","type":"str"}],"x":553,"y":1278,"wires":[["7edee354c4d71a9f"],["c29f1df3a6b3dce5"]]},{"id":"c29f1df3a6b3dce5","type":"subflow:104818a352e4dbd7","z":"e51c499288aa059c","name":"OK, then you are done!","env":[{"name":"result","value":"OK, then you are done!","type":"str"}],"x":838,"y":1284,"wires":[]},{"id":"7edee354c4d71a9f","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Have you checked flows.nodered.org?","env":[{"name":"question","value":"Have you checked flows.nodered.org?","type":"str"}],"x":883,"y":1225,"wires":[["25a4bbef9d4e1182"],["3fea93b5670dd77c","05769ebd154bdb43"]]},{"id":"d07aa0c3b830ccf1","type":"ui_template","z":"e51c499288aa059c","g":"56847658385cd98e","group":"97885fc627878fef","name":"show info","order":1,"width":12,"height":8,"format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":356,"y":1673,"wires":[[]]},{"id":"3fea93b5670dd77c","type":"template","z":"e51c499288aa059c","name":"flows.nodered.org ","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Existing extensions\n\n[Flows.nodered.org](https://flows.nodered.org) has a collection of nodes, flows and collections.\n\nMuch has already been done, perhaps there is a solution that will solve your requirements.\n\n### Nodes\n\nFor specific interfaces to devices, checkout the [nodes collection](https://flows.nodered.org/search?type=node). \n\n### Flows\n\nFor more doing specific tasks using Node-RED, perhaps there already exists a [flow](https://flows.nodered.org/search?type=flow).\n\n### collections\n\nFinally there are collections of Node-RED resources around a specific topic. For example, for [interacting with Home Assistant](https://flows.nodered.org/collection/0R7Vjr77K6fS).\n\n","output":"str","x":1165,"y":1256,"wires":[["e8111ed9553ecdf9"]]},{"id":"05769ebd154bdb43","type":"subflow:104818a352e4dbd7","z":"e51c499288aa059c","name":"Ok, check flows.nodered.org first","env":[{"name":"result","value":"Ok, check flows.nodered.org first","type":"str"}],"x":1234,"y":1212,"wires":[]},{"id":"06a42fdc032c539f","type":"change","z":"e51c499288aa059c","g":"56847658385cd98e","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":344,"y":1715,"wires":[["d07aa0c3b830ccf1"]]},{"id":"605bc2ca67a50994","type":"link in","z":"e51c499288aa059c","g":"56847658385cd98e","name":"reset info block","links":["0536aa265d220f9e","a1765e0cd70424e5"],"x":128,"y":1720,"wires":[["06a42fdc032c539f"]]},{"id":"0536aa265d220f9e","type":"link out","z":"e51c499288aa059c","name":"link out 128","mode":"link","links":["605bc2ca67a50994","f5234b7d5afb80b8"],"x":432,"y":924,"wires":[]},{"id":"9df58b6177897325","type":"function","z":"e51c499288aa059c","g":"56847658385cd98e","name":"MD -> HTML w/ note container","func":"if (msg.payload === undefined || typeof msg.payload !== \"string\") {\n    throw \"Markdown Content Not Found\";\n} \n\nvar md = markdownIt({ \n    html: true, \n    linkify: true, \n    typographer: true,\n    breaks: true,\n    xhtmlOut: false,\n    highlight: function(str, lang) {\n        if ( lang == \"mermaid\") {\n            return '<pre class=\"mermaid-pre-container\"><code class=\"mermaid\">\\n' + md.utils.escapeHtml(str) + '\\n</code></pre>';\n        }\n\n        // this is for javascript and co.\n        if (lang && highlightJs.getLanguage(lang)) {\n            try {\n                return '<pre class=\"hljs\"><code class=\"language-'+ lang + ' hljs\">' +\n                    highlightJs.highlight(str, { language: lang, ignoreIllegals: true }).value +\n                    '</code></pre>';\n            } catch (__) { }\n        }\n\n        // this is for noderedjson-XXXXX\n        return '<pre class=\"language-'+lang+' hljs\"><code class=\"language-'+lang+' hljs\">' + md.utils.escapeHtml(str) + '</code></pre>';\n    }\n});\n\nmd.use(markdownItFootnote);\nmd.use(markdownItMark);\nmd.use(markdownItSup);\nmd.use(markdownItLinkAttributes, {\n    attrs: {\n        target: \"_blank\",\n        rel: \"noopener\",\n    },\n});\n\nmsg.payload = md.render(msg.payload);\n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"markdownIt","module":"markdown-it"},{"var":"markdownItContainer","module":"markdown-it-container"},{"var":"markdownItFootnote","module":"markdown-it-footnote"},{"var":"markdownItMark","module":"markdown-it-mark"},{"var":"highlightJs","module":"highlight.js"},{"var":"markdownItLinkAttributes","module":"markdown-it-link-attributes"},{"var":"markdownItSup","module":"markdown-it-sup"}],"x":353,"y":1610,"wires":[["d07aa0c3b830ccf1"]],"icon":"node-red-node-markdown/parser-markdown.png"},{"id":"25a4bbef9d4e1182","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Have you asked on the Node-RED forum?","env":[{"name":"question","value":"Have you asked on the Node-RED forum?","type":"str"}],"x":1278,"y":1149,"wires":[["10fddea0fd8b7aa1","a1a40cd559b74f41"],["78ffc5b43d407b3e","501f96be679bea15"]]},{"id":"78ffc5b43d407b3e","type":"subflow:104818a352e4dbd7","z":"e51c499288aa059c","name":"Ok, ask the forum for help.","env":[{"name":"result","value":"Ok, ask the forum for help.","type":"str"}],"x":1649,"y":1279,"wires":[]},{"id":"501f96be679bea15","type":"template","z":"e51c499288aa059c","name":"node-red forum","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Node-RED Forum\n\nThe Node-RED [forum](https://discourse.nodered.org/) is open to all and every level of experience with Node-RED.\n\nIf you are unsure where to ask, start at the [general](https://discourse.nodered.org/c/general/7) category.\n\nPoliteness is always good!\n\n","output":"str","x":1601,"y":1327,"wires":[["e84cbae4e95dcdf3"]]},{"id":"e84cbae4e95dcdf3","type":"link out","z":"e51c499288aa059c","name":"link out 129","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":1715,"y":1328,"wires":[]},{"id":"e8111ed9553ecdf9","type":"link out","z":"e51c499288aa059c","name":"link out 130","mode":"link","links":["3242ad96df5ac7da"],"x":1290,"y":1256,"wires":[]},{"id":"3242ad96df5ac7da","type":"link in","z":"e51c499288aa059c","g":"56847658385cd98e","name":"show md content","links":["e8111ed9553ecdf9","e84cbae4e95dcdf3","0bc14bd8a852dfa4","4149e843d7e7c18f","0272ec429d75b936","f865415e5ecd2fd0","9f4321011394b44d","3435a30902c36512"],"x":126,"y":1622,"wires":[["9df58b6177897325"]]},{"id":"10fddea0fd8b7aa1","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Is there an existing NPMjs.com package?","env":[{"name":"question","value":"Is there an existing NPMjs.com package?","type":"str"}],"x":1650,"y":1086,"wires":[["fa1412682d1ac9be","63fa450506e4576b"],["8fbe733aacf19d32","062b33d26362ea6d"]]},{"id":"a1a40cd559b74f41","type":"template","z":"e51c499288aa059c","name":"npmjs.com","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## npmjs.com\n\n[NPMjs](https://npmjs.com) is the package manager for Javascript and there are many thousands of packages that can easily be integrated into Node-RED.\n\n","output":"str","x":1562,"y":1142,"wires":[["0bc14bd8a852dfa4"]]},{"id":"0bc14bd8a852dfa4","type":"link out","z":"e51c499288aa059c","name":"link out 131","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":1677,"y":1142,"wires":[]},{"id":"8fbe733aacf19d32","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Can you code Javascript?","env":[{"name":"question","value":"Can you code Javascript?","type":"str"}],"x":1996,"y":1130,"wires":[["2822351c6b8f8bdd"],["2822351c6b8f8bdd"]]},{"id":"062b33d26362ea6d","type":"template","z":"e51c499288aa059c","name":"js & nodejs","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Javascript & NodeJS\n\nNode-RED has been created in NodeJS which is a server side version of Javascript. Existing Javascript code can be easily integrated into Node-RED using the *function node*.\n\n","output":"str","x":1952,"y":1177,"wires":[["4149e843d7e7c18f"]]},{"id":"4149e843d7e7c18f","type":"link out","z":"e51c499288aa059c","name":"link out 133","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":2078,"y":1178,"wires":[]},{"id":"fa1412682d1ac9be","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Can you code Javascript?","env":[{"name":"question","value":"Can you code Javascript?","type":"str"}],"x":1992,"y":1032,"wires":[["854c8c5f070a70f1","d4c1e29ae166fa33"],["2822351c6b8f8bdd"]]},{"id":"63fa450506e4576b","type":"template","z":"e51c499288aa059c","name":"js & nodejs","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Javascript & NodeJS\n\nNode-RED has been created in NodeJS which is a server side version of Javascript. Existing Javascript code can be easily integrated into Node-RED using the *function node*.\n\nEven though Node-RED is largely low-code, an awareness of Javascript is of benefit. Much can be done with Node-RED with a basic understanding of Javascript","output":"str","x":1951,"y":1069,"wires":[["0272ec429d75b936"]]},{"id":"0272ec429d75b936","type":"link out","z":"e51c499288aa059c","name":"link out 134","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":2069,"y":1069,"wires":[]},{"id":"854c8c5f070a70f1","type":"subflow:08e99cbd5f4d9de5","z":"e51c499288aa059c","name":"Is the existing package an ESM package?","env":[{"name":"question","value":"Is the existing package an ESM package?","type":"str"}],"x":2330,"y":970,"wires":[["5507b6d18c9ce484","fbb697d9a1693bb9"],["2822351c6b8f8bdd"]]},{"id":"d4c1e29ae166fa33","type":"template","z":"e51c499288aa059c","name":"ESM Package","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## ESM v. CJS Packages\n\n[ECMAScript modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) (ESM) are the official standard format to package JavaScript code for reuse. Modules are defined using a variety of import and export statements.\n\n[CommonJS modules](https://nodejs.org/api/modules.html#modules-commonjs-modules) (CJS) are the original way to package JavaScript code for Node.js. Node.js also supports the ECMAScript modules standard used by browsers and other JavaScript runtimes.\n\nESM Packages are only partially supported in Node-RED.\n","output":"str","x":2250,"y":923,"wires":[["f865415e5ecd2fd0"]]},{"id":"f865415e5ecd2fd0","type":"link out","z":"e51c499288aa059c","name":"link out 135","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":2362,"y":923,"wires":[]},{"id":"5507b6d18c9ce484","type":"subflow:104818a352e4dbd7","z":"e51c499288aa059c","name":"Then you will need to create a custom node.","env":[{"name":"result","value":"Then you will need to create a custom node.","type":"str"}],"x":2739,"y":920,"wires":[]},{"id":"fbb697d9a1693bb9","type":"template","z":"e51c499288aa059c","name":"creating custom nodes","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Custom Nodes\n\nCreating custom nodes in Node-RED is [well documented](https://nodered.org/docs/creating-nodes/).","output":"str","x":2670,"y":869,"wires":[["9f4321011394b44d"]]},{"id":"9f4321011394b44d","type":"link out","z":"e51c499288aa059c","name":"link out 136","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":2809,"y":864,"wires":[]},{"id":"afb3d0f568acb3ae","type":"subflow:104818a352e4dbd7","z":"e51c499288aa059c","name":"Questionaire is not yet Completed.","env":[{"name":"result","value":"Questionaire is not yet Completed.","type":"str"}],"x":2909,"y":1355,"wires":[]},{"id":"4ee2308ab360bec7","type":"template","z":"e51c499288aa059c","name":"QED","field":"payload","fieldType":"msg","format":"markdown","syntax":"plain","template":"## Sorry this questionaire is not complete\n\nCompletion of this questionaire is pending!\n\n","output":"str","x":2823,"y":1307,"wires":[["3435a30902c36512"]]},{"id":"3435a30902c36512","type":"link out","z":"e51c499288aa059c","name":"link out 137","mode":"link","links":["a7d81acd47232659","3242ad96df5ac7da"],"x":2996,"y":1309,"wires":[]},{"id":"e7be40705d5862a1","type":"ui_spacer","z":"e51c499288aa059c","name":"spacer","group":"97885fc627878fef","order":3,"width":2,"height":1},{"id":"caa80dbba2c50eae","type":"ui_spacer","z":"e51c499288aa059c","name":"spacer","group":"97885fc627878fef","order":5,"width":2,"height":1},{"id":"23da4b112927e09f","type":"ui_spacer","z":"e51c499288aa059c","name":"spacer","group":"97885fc627878fef","order":6,"width":2,"height":1},{"id":"9494fc34a568e360","type":"ui_spacer","z":"e51c499288aa059c","name":"spacer","group":"97885fc627878fef","order":7,"width":2,"height":1},{"id":"97885fc627878fef","type":"ui_group","name":"Extension Selection","tab":"7260db544c0af12f","order":1,"disp":true,"width":"12","collapse":false,"className":""},{"id":"7260db544c0af12f","type":"ui_tab","name":"Extension Selection","icon":"dashboard","disabled":false,"hidden":false}]

it creates a dashboard with simple yes/no questions and helpful tips (well sort of!). Admittedly I didn't get to far but it's just an idea that someone will perhaps find useful.

My approach is using my own nodedev nodes which is very meta in the sense that I'm creating a flow that creates nodes that can be used in flows ... it's turtles all way down. But I was pleasantly surprised that I am not the only one, node-maker takes the exact same approach, just for the UI components of nodes.

My workflow is in combination with flowhub.org - all my nodes are hosted there in form of flows. I can make changes to my nodes in minutes, not hours. My workflow is roughly:

  1. pull in the flow (using FlowHubPull node) that represents the node I want to change, for example nodedev nodes,
  2. make my change to the code in Node-RED
  3. install a local copy into my node-red (having deleted the original version of the package)
  4. fix my changes and repeat step 3
  5. Using the NodeDevOps node, I check the diff against Github, if ok, I push the change to Github
  6. I do a version bump and publish to npm and push to Github in parallel
  7. I go to flows.nodered.org and push the check for update button,
  8. check the changes against what is at flowhub.org
  9. push changes to flowhub.org
  10. rinse and repeat

The only time I leave Node-RED is to press the button @ flows.nodered.org. The end result is a diff where the purple nodes are the changes I made - that was a Buffer.concat error I noticed, took about ten minutes from noticing it to having a new version published and live.

This is what I've come to after experimenting around a bit, I'm sure I'll find some more improvements but for the time being, it makes node development less painful.

Also the interesting thing is that GitHub is no longer the source of truth, my flows are a combination of CI build steps and development. For example, I use the uglify node to combine my JS code before committing to Github - so that this flow contains the original, human readable JS and this Github repo has the uglified version.

Sorry for blowing my own trumpet, my intention is not say this is the workflow, my intention is to draw attention to a different approach since this is visual programming. For me that also means that my textual tools take a step back and I search for visual alternatives - including the workflow.

I just tried file-type and it didn't work with a function node, I gave up and came here instead! Or better said, after I included it in the function node setup, the variable (fileType) was undefined on message ...

link-calls have the drawback that there is no visual connection to the link-in node ... I get easily confused with link-calls since I have to remember names of link-in nodes (and their tabs) but otherwise they do a great job.

Copy and paste? I don't know. I'm one of the DRYist people, I hate repetition. But I have seriously found myself just copy & pasting nodes and changing variable names instead of generalising the code.

I wonder whether this is something that comes with visual low-code programming - I dunno. To be sure, I'm talking about 10 to 20 lines of code, not 100s. 100s is a link-call, 10-20 is c&p.

But from who's perspective? In my view, one of the major benefits of Node-RED is that it is not too prescriptive about how you do things. This is, I believe, one of the major reasons it continues to gain ground. I've already indicated that how I use the different approaches will depend heavily on how I feel, what I'm trying to do, how many other things I've got on at the same time and just how my brain happens to be processing the logic of my requirements at that time. There is no one-size fits all and that is a great strength of Node-RED.

No, don't be, it is great to see different people using Node-RED in different ways.

For myself, I'm only partially a visual thinker. I believe this is down to when I started in IT - at a time when visual stuff mostly still had to be done by hand on paper. So to me, using something like your approach or node-maker mostly wouldn't work because the visual aspects would get in the way of the design process. But that is just me, I think it is great that others have put together the almost purely visual approach. I work with lists, pseudo-code and text when I'm programming. I only occasionally delve into diagrams and charts. So for me, I want to be able to work in VScode to create custom nodes. Different person, different approach.

Hi.

Sorry, but I would say it is BETTER that you have the option of doing it different ways.
As if it was fixed it would (in some ways) limit Node-Red's versatility.

The old saying: There's more than one way to skin a cat comes to mind.
And I think that is a good thing for Node-Red.
It is flexible.

file-type unfortunately doesn't export a default symbol.

Thus it's - with the current implementation of dynamic import in Node-RED - not usable.
We really need to make up our minds how to "fix" this...

I did a test. I changed

sandbox[vname] = lib.default || lib

Here: https://github.com/node-red/node-red/blob/master/packages/node_modules/%40node-red/nodes/core/function/10-function.js#L318

And the library we discussed the other day was available.

I still think supporting named (destructured) imports would be a better (if somewhat trickier)

1 Like

@gregorius

Sorry for going a bit off-topic:

Yep - yet this is going to break the import of cjs modules - as I tried to explain in the other post.
The issue here is (from my perspective) that both types (cjs & esm) currently are processed by the same logic. For cjs type lib.default is mandatory, for esm type lib.default should always just be lib (independently if there's a lib.default).

My phrasing was too dogmatic, of course freedom should remain but for people new to Node-RED, it would be nice to have guidelines when to use what when extending Node-RED. So yes, you are right there should not be a must but more along the lines use a custom node if you want XYZ if you don't want XYZ then use whatever - my example for XYZ are certain ESM packages but even that is a grey zone! Ah, I have one: if you wanted a different coloured node, then you'll need a custom node (damn, you could also use a subflow) :wink:

You also have to see it from the Node-RED developers perspective: where does the development of function node stop and where do custom nodes take over? Will the next release bring a colour changer to function node? Or will it remain at only the icon? From my perspective, Node-RED is very stable piece of software where changes are well thought out. So I would nearly expect there is an answer to that colour question (of course colour might be laughable matter - it's just a metaphor for other functionality).

For developers of Node-RED it is just as important to have clear limits between responsibilities else feature creep begins. Another example, if I (or anyone else) created a PR for adding a colour changer to the function node tomorrow - because there is no one saying "no colour changer for the function node", then it would be wasted effort if then someone said: "oh, no we don't want a colour changer for the function node".

So having clear responsibilities also allows for a clear path forwards.

I find that interesting since you still use Node-RED. For me, I think am a little like you, more and more its not the visual part that fascinates me, its the highlighting of data flows and not code that interests me more. I.e., the meaning of the visuals rather than the visuals themselves. Ever since I discovered flow based programming was invented in the 1960s, the more I see Node-RED as a visual flow based programming environment (a textual flow based programming environment would be the Unix pipe on the command line).

If I would be to complain, it would be a case of the pot calling the kettle black. :slight_smile:

1 Like

Believe me, I've been with Node-RED for 10 years and I absolutely get it. And truthfully I'm not arguing at all about someone coming up with some guides, I think that would be very helpful as would your "flowchart" flow. I'd be as happy as anyone to see such guidance created as long as it was made clear that this is just guidance, there are times to ignore it and not necessarily some fundamental right or wrong way.

Yes, that is a hard ask because it requires yet more work on documentation and, typically, developers want to develop not document. :rofl: To be fair though, there is quite a lot now captured in the design documents.

I'm lazy! As I say, I love the fact that Node-RED carries a lot of weight for me, I don't have to think about it. :slight_smile:

Also, after 10 years, and the development of several nodes that people actually seem interested in, I'm rather "invested". :grinning:

And, though I regularly look, I've never found anything that comes anywhere near Node-RED in any language. Its stability, flexibility, support and, of course, the wonderful community and its overall perspective and approach are all very attractive.

It is only a wild guess, but I suspect that less than 10% of newcomers to node-red are able to competently code a function node, and less then 1% would be able to develop a new custom node.

2 Likes

Just to complete this line of thought. The reason we don’t want to allow colour change is because of readability of the flow. When a user look at another users flow we want them to be able to skim read it and get a gist of what it is doing, and to be quickly able to identify node types. If function nodes get skinned to look like other nodes by changing icon, colour, and name that makes things harder for us all.

4 Likes