Tutorial - create a sidebar plugin and persist the data in a config node

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 :innocent:

The development steps in detail:

  1. 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"
         }
     }
    
  2. 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);
    }
    
  3. 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.

  4. 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 :frowning_face:

  5. 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

11 Likes

Nice tutorial, thank you Bart! :nerd_face:

I wonder if stuff like that could make it into the tutorials section of the official documentation? :thinking:

1 Like

Hi @kuema,
That is indeed an idea. But in fact writing this one here (without restrictions about styling, and so on...) was already a bit too much for my free time restrictions. I hadn't even the time to test this (simplified) code examples. So unfortunately this is by far the most I can offer ar the moment. It is this or nothing :roll_eyes:

1 Like

Thank you for the explanation, I will brazenly copy it :slight_smile:

is the second line actually globalYourConfigNode.someData = data;

Does that ensure the node isn't shown in the palette sidebar? If I understand the tutorial correctly the node is shown but can't be edited - so question becomes, why would it be shown in the first place?

To avoid confusion, this is what I mean as palette sidebar:

I've hit this problem myself a number of times.

It would be really nice to have some process that let people submit documentation improvements/corrections in a way that could be properly incorporated when someone actually gets time to download the doc repo, works out what/where to update, sorts out the style and submit a PR.

I suspect that would let the Node-RED documentation improve at a higher pace.

1 Like

As it's a node registered with category defined config, it won't show up in the (left-hand) palette sidebar.

Once created, it will yet show up in the (right-hand) Configuration nodes panel - similar to the global-config config node that is used to store the global env variable data (NR v3.1).

That said, I think (yet haven't tested) it's not necessary to create html elements for the stored data (and then disable those); it should be enough to declare the defaults.

@BartButenaers: Great idea to put this knowledge into a tutorial!

2 Likes

@gregorius as @ralphwetzel explains, the config node will show up in the config nodes list. So not in the palette, because you don't add a config node manually in your flow. Instead config nodes are being created automatically by other nodes, when the user creates another configuration for that node.

That is why I explained that ´hasUsers´ needs to be set to false. Because our config node is not linked to another node, so it would end up incorrectly between the unused config nodes:

In that case our sidebar data could get lost accidentally, if a user removes all unused config nodes (unaware that our sidebar data is stored in one of these config nodes...).

Hopefully that is a bit clear?

Isn't this repo the documentation of nodered.org? If so, a PR there would be probably the right place.

:+1: Thanks @ralphwetzel I didn't notice the category being config

So the html would only be included in the sidebar .html? That would make sense and would keep the code DRY.

Ok, I think I'll have a play around and see whether I can get something up and running

Thanks for all the clarification - much :sunny: to all!

2 Likes

There's still the need to define a .html file for the config node - or to include it htmlish definition in the .html file of the sidebar (not sure what happens when you mix definitions for plugin & node in one .html file; better safe to go with separated files). It shouldn't be necessary though to create ui elements and disable them:

1 Like

I know where and I believe I've submitted a PR before. But what I was trying to get across was that it actually isn't easy to update the node-red docs. Certainly not something that you can do in a few minutes.

Having an easier staging place would really help more people submit things that could then be triaged for the main docs, an FAQ in the forum or the cookbook, the wording style could be adjusted, locations chosen - multiple people could help. Then doing the final submission and PR would also be a lot easier.

In all seriousness what you speak of is a wiki. PRs are great for code and wikis are great for documentation.

2 Likes

From the wiki's back to the sidebar :yum:

@gregorius: it is also sometimes useful to make some features of your sidebar available as a flow editor action.

  1. Suppose you have e.g. a button on your sidebar, which calls a function when being clicked:
    $("#do-something-button").click(function() {
        doSomething();
    })
    
  2. In your sidebar html file you can also register an action for that function:
    RED.actions.add("core:my-sidebar-action",function() {
       doSomething();
    });
    
    This way your sidebar functionality becomes available in the flow editor actions list.

Although I assume the "core" (in the action name) should be replaced by something custom...

2 Likes

I changed this to be:

var initialiseConfigNodeOnce = () => {
      RED.events.off('runtime-state', initialiseConfigNodeOnce);
....
};
RED.events.on('runtime-state', initialiseConfigNodeOnce);

I assume you want that code to be executed once and once only but it was being executed each deployment.

Btw in the configuration node panel, the node isn't setup, i.e. doesn't contain the current value:

Is there something else that needs to happen for that panel to be correctly setup? The html shown is from the sidebar html file, not the node definition...

Since RED.events is an EventEmitter, you could use RED.events.once() instead, so you could save the RED.events.off() in your callback.

I was thinking the same:

but my browser told me something else :frowning:

I had something similar in the beginning, due to 2 reasons:

  • Every time the user changes something in your sidebar (i.e. dropdown value, input field value, ...), you need to store the updated data immediately in your config node. Perhaps you have added an onchange event handler to your sidebar input element, but you did not tab away from that field (so the event is not triggered and the data is not synced toward your config node)? Best to put a debugger statement in your code and check whether the updated data is stored into your config node.

  • After I refreshed my flow editor window, my config node was empty again because my sidebar was filled to early. That is why I used the "runtime-state" event: because when you run your code earlier, the config node was not loaded yet (by the flow editor) so your sidebar thinks incorrectly no config node yet, and it creates a new empty one.

Aaah, sorry! My bad... I was a bit quick there.

I thought it refers to the runtime code, not the editor. Then ignore what I've said... :sweat_smile:

1 Like

Solved the problem, the node that is defined is the config and should have a different type - I was using the same type for tab and config node. The html (data-template-name) defined in the config node is then shown in the property panel when opened via the configuration nodes menu point.

I.e. the html that we deleted above is the html that is shown in the configuration panel!

2 Likes

So I've created my first sidebar plugin by converting the flow2uml node from palette to config node with sidebar menu:

Screen Shot 2023-10-16 at 17.37.35

I also pulled all this code (sidebar templates) into the nodedev set of nodes so that it's now possible to generate sidebar nodes using the NodeFactory:

3 Likes

Cool. A fast student in our classroom...
Due to your new NodeFactory feature this tutorial has become already obsolete for you :wink: