Hi folks,
Developing nodes for Node-RED is not difficult, once you know how to do it. However I always forget how to create a sidebar plugin, to show a panel in the right sidebar of the flow editor. Especially if the user needs to enter data in the sidebar, and you want to remember that data. So I decided to explain here how I do it. I have only developed a couple of sidebar plugins, so I am by far a specialist. So others may have better ways to do it...
So hopefully other developers can benefit from it. Hint for a.o. @gregorius
The development steps in detail:
-
Start by telling Node-RED that - at startup time - the backend needs to load your config node js file, and the flow editor frontend needs to load your plugin html file. Do this by adding this snippet into your
package.json
file:"node-red": { "nodes": { "math": "your_config_node.js" }, "plugins": { "sidebar-plugin": "your_plugin.html" } }
-
Add a normal config node js file to your project, which most of the time does only register itself into the Node-RED runtime:
module.exports = function (RED) { function YourConfigNode (config) { RED.nodes.createNode(this, config) } RED.nodes.registerType('your_config_node_name', YourConfigNode); }
-
Add a normal config node html file (for the same config node name), where you add an html element for all the data that your want to persist:
<script type="text/javascript"> (function ($) { RED.nodes.registerType('your_config_node_name', { category: 'config', hasUsers: false, defaults: { name: { value: "Your plugin name" }, someData: { value: "" } }, paletteLabel: 'Your plugin name', label: function () { return this.name; } }); })(jQuery); </script> <!-- The html for the config node screen --> <script type="text/x-red" data-template-name="your_config_node_name"> <div class="form-tips">This config node is read-only, because it stores the information entered in the 'Your plugin name' sidebar.</div> <div class="form-row" id="auto_layout_config_name_div"> <label for="node-config-input-name"><i class="icon-tag"></i> Name</label> <input type="text" id="node-config-input-name" disabled> </div> <div class="form-row"> <label for="node-config-input-someData"><i class="fa fa-terminal"></i> Some data</label> <input type="text" id="node-config-input-someData" disabled> </div> </script> <!-- The html for the config node info panel (in right sidebar) --> <script type="text/x-red" data-help-name="your_config_node_name"> <p>Show some help about your config node</p> </script>
The
hasUsers
needs to be false, because otherwise Node-RED thinks this config node is not used anywhere (since it is not used by another node). Which means the config node would be regarded as unused node, which would not be correct (because the sidebar uses it).The html input elements for each data have an attribute
disabled
, to make sure no data is being entered by the user via the config node screen. This is not mandatory, but I do it like that because I want the config node data to be entered only via the plugin sidebar window. -
The html file for the sidebar plugin looks like this:
<script type="text/javascript"> (function() { var globalYourConfigNode = null; function ensureYourConfigNodeExists() { // This function makes sure there is 1 instance of your config node is available, and that the globalYourConfigNode variable refers to it. // Explained in the next step of this tutorial... } var initializeSidebar = function( ) { // Remove the event handler, to avoid that the sidebar is initialized at every deploy RED.events.off('runtime-state', initializeSidebar); // The html content of the sidebar has been specified below as a data-template, from where it can be loaded: var content = $($('script[type="text/x-red"][data-template-name="your_sidebar_html_name"]').i18n().html()); // Add a "Your sidebar" tabsheet to the right sidebar panel, in which this sidebar panel can be displayed RED.sidebar.addTab({ id: "your_sidebar_html_name", label: "your_sidebar", // short name for the tab name: "Your sidebar", // long name for the menu content: content, closeable: true, disableOnEdit: true, iconClass: "fa fa-outdent" // your fontawesome icon }); ensureYourConfigNodeExists(); // At startup load your config node data into the plugin sidebar html elements $("#node-input-data").val(globalYourConfigNode.someData); // When the user has entered new data in the sidebar, then store it into the config node $("#node-input-data").on("change", function() { ensureYourConfigNodeExists(); let data = $(this).val(); if (globalYourConfigNode.someData] != data) { globalYourConfigNode.someData] != data; // Since the config node has been updated, the 'Deploy' button should become active RED.nodes.dirty(true); } }) } // Add your plugin as a new tabsheet in the right sidebar AFTER the flow editor is completely started RED.events.on('runtime-state', initializeSidebar); })(); </script> <!-- The html for the right sidebar plugin screen --> <script type="text/x-red" data-template-name="your_sidebar_html_name"> <div style="position: relative; height: 100%; margin: 15px;"> <label for="node-input-data" style="margin-top: 15px"><i class="fa fa-cog"></i> Data</label> <input type="text" id="node-input-data" style="width: 100%; margin-top: 30px"> </div> </script>
Note that it is very important to call the
ensureYourConfigNodeExists()
function every time before you want to load data from your config node into your sidebar, or store updated data from the sidebar screen into the config node. Because a user might have deleted your config node meanwhile.Unfortunately I had to invent a kind of a hack. The sidebar html content needs to display all your config node property values when the flow editor is started, because you want to start again with your config node data from your previous time. However the config nodes are only loaded near the end of the core flow editor startup code. At the time being I did a feature request to get such an event, but it was never implemented. Another user implemented a workaround, but that event is triggered after RED.nodes is loaded (but the config nodes are not availble yet). So I figured out that the
runtime-state
does the job, but it is not officially published so it might be removed in the future -
And finally you need to implement the
ensureYourConfigNodeExists()
function. The following implementation does the job well for me:function ensureYourConfigNodeExists() { // If we had found it previously, check if it has been deleted by the user behind our back if (globalYourConfigNode !== null) { var configNode = RED.nodes.node(globalYourConfigNode.id); if (configNode === null) { globalYourConfigNode = null; } } // If not found previously, let's go find it if (globalYourConfigNode === null) { var configNodes = []; RED.nodes.eachConfig(function(configNode) { if (configNode.type === 'your_config_node_name') { configNodes.push(configNode); } }); // Make sure we only have 1 config node while (configNodes.length > 1) { var configNode = configNodes.pop(); RED.nodes.remove(configNode.id); RED.nodes.dirty(true); } // When we found a config node, let's use that one if (configNodes.length === 1) { globalYourConfigNode = configNodes[0]; } } // When it doesn't exist yet, create it if required if (globalYourConfigNode === null) { // TODO controleren of de defaults door de config node code zelf toegepast worden???? // Remark: since this config node is dynamically created (and only used in this sidebar which isn't another node), the config // node is in fact "unused". But since we don't want it to appear "unused" in the "config nodes" panel, we need to set hasUsers // to false (see https://github.com/node-red/node-red/blob/master/CHANGELOG.md#0161-maintenance-release). // The hasUsers needs also to be specified in the RED.nodes.registerType statement! globalYourConfigNode = { id: RED.nodes.id(), _def: RED.nodes.getType("your_config_node_name"), type: "your_config_node_name", hasUsers: false, users: [], // TODO default values for all properties name: "Your plugin name", label: function() { return this.name || "Your plugin name"}, algorithm: "dagre_lr", // Default algorithm settings: JSON.stringify(getDefaultSettings()) // Start with the default settings per algorithm } // Add the new config node to the collection of Node-RED nodes RED.nodes.add(globalYourConfigNode); // Make sure the "Deploy" button becomes active RED.nodes.dirty(true); } }
After calling this node you are sure that there is 1 single config node available: an existing one or a new one if it didn't exist yet.
Good luck with the development of your first sidebar (accompanied by a config node)!
Bart