My first module, config does not update until NodeRED is restarted

Hi,

I have used NodeRED for a number of years, but never needed to create my own custom node... until now.

I'm on NodeRED 3.0.2

I have a 'name' property in the data-template and also a multi-select typedInput. I have created the JS and HTML files, all looks nice and works well if I restart NodeRED.

However, I'd like my node to read the correct, current value of the multiselect, this is changed when the flow is deployed, but the JS sees the old value.

I attach a simplified version of my code. Any help is gratefully received.

my .html file

<script type="text/javascript">
    RED.nodes.registerType('m2et-events-xml',{
        category: 'Lownto',
        color: 'rgb(231, 231, 174)',
        defaults: {
            name: {value:"events handler"},
            events: {value:""}
        },
        inputs:0,
        outputs:0,
        icon: "white-globe.svg",
        label: function() {
            return this.name||"m2et-events-xml";
        },
        oneditprepare: function () {
            $("#node-input-events").typedInput({
                type:"events",
                types: [{
                        value: "events",
                        multiple: "true",
                        options: [
                            { value: "adminhtml_customer_save_after", label: "adminhtml_customer_save_after"},
                            { value: "catalog_category_save_after", label: "catalog_category_save_after"},
                        ]
                    }]
            });
        }
    });
</script>

<script type="text/html" data-template-name="m2et-events-xml">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-events"><i class="fa fa-tag"></i>Enabled Events</label>
        <input type="text" id="node-input-events">
    </div>
</script>

<script type="text/html" data-help-name="m2et-events-xml">
    <p>My first node</p>
</script>

The relevant part of my .js

module.exports = function (RED) {
    let xmlbuilder = require('xmlbuilder');
    function HttpXmlRequestResponse(config) {
        RED.nodes.createNode(this, config);
        let node = this;
        let events = config.events;
// NOTE : This one outputs the updated value per the multi-select if you change it in admin and deploy.
        node.warn("set events var to " + events);

        RED.httpNode.get('/my_endpoint, function (req, res) {
// NOTE : 'events' here is always the value as per when nodeRED is started, so it's wrong until I restart NodeRED
            let xmlResponse = generateEventsXmlResponse(events);

            if (xmlResponse) {
                res.set('Content-Type', 'text/xml');
                res.send(xmlResponse);
            } else {
                res.sendStatus(404);
            }
        });

        function generateEventsXmlResponse(area, events) {
              // 'events' is always stale until a restart of NodeRED
        }
    }

    RED.nodes.registerType('m2et-events-xml', HttpXmlRequestResponse);
};

I think that is because it is a config property and those are not updated unless you do a deploy. And even then, because you used a typed input, you cannot simply use the config property in your runtime, you have to process it.

In the uibuilder nodes that use typed inputs, I've created a helper function.

/** Get an individual value for a typed input field
 * @param {string} propName Name of the node property to check
 * @param {runtimeNode & uibElNode} node reference to node instance
 * @param {*} msg incoming msg
 */
async function getSource(propName, node, msg) {
    const src = `${propName}Source`
    const srcType = `${propName}SourceType`
    if (node[src] !== '') {
        try {
            node[propName] = await mod.evaluateNodeProperty(node[src], node[srcType], node, msg)
        } catch (e) {
            node.warn(`Cannot evaluate source for ${propName}. ${e.message} (${srcType})`)
        }
    }
}

As you will note, getting the actual values is an async function.

Note that mod.evaluateNodeProperty is a reference to promisify(RED.util.evaluateNodeProperty) which ensures that I can do things like this for efficiency:

    // Get all of the typed input values (in parallel)
    await Promise.all([
        getSource('parent', node, msg),
        getSource('elementId', node, msg),
        getSource('heading', node, msg),
        getSource('position', node, msg),
    ])

Hi,
Thanks for replying.

Based on this, it sounds like I've taken a wrong approach?

because it is a config property and those are not updated unless you do a deploy.

Is there a way that I can have the flexibility of being able to edit config without needing a restart?

Actually, sorry @TotallyInformation , I don't mind doing a deploy (in fact I would expect to).

But the value doesn't update, even IF I do a deploy. Only a full restart of NodeRED

As I say, you have to use RED.util.evaluateNodeProperty to get the value from a typed input.

Thanks, I think maybe I've over-complicated it for my first custom node. I'm going to switch to using a basic HTML multi-select input and see if that makes it any easier.

So, after having this sully working with a list which was declared in pure HTML, I'm back to the point of it not working again now I dynamically add options to the select list with javascript.

I know what I'm doing "works" in terms of the options are visible, get selected and save to 'config'. But the value in the .js file only gets updated when I fully reboot NodeRED.

I have tried, as you suggested:

    function HttpXmlRequestResponse(config) {
        RED.nodes.createNode(this, config);
        let node = this;
        let area = "frontend"; // As this is dynamic from somewhere else
        RED.util.evaluateNodeProperty(config[area + '_eventlist'], 'str', node);

But this still returns the value as it was when NodeRED was last rebooted.

Again, any advice/assistance will be gratefully received.

That is only for typed inputs. Remember that anything entered into the Editor's config panel for a node has to be recorded in the editor and requires a Deploy. Once deployed, your runtime code for the node has the contents of the Editor config inputs available on the config parameter that is passed into the callback function in RED.nodes.registerType. However, for typed inputs ONLY, the data has to be processed correctly to get the actual data out of it. Everything else is directly accessible.

So to get the value of a drop-down in the Editor config panel, you have to make sure that you update the actual input field that Node-RED uses. That value is then available in the runtime's config object. Except when the input is a typed input and then you have to process the config entry using mod.evaluateNodeProperty.

Sorry if this is what you already understand but from what you've written, I can't be sure.

I think by trying to make this simple to help and avoid pasting walls of code I'm making it harder for help.

In the hrml file I have an empty HTML select structure. The options are added to the select using the jQuery append(HTML) function in oneditprepare(). After the options are present (in the same function) I use $('#mySelect').val(node.myField) to set the values which have been selected and 'saved'. Then in oneditsave I read the clicked values from the select and save them in node.myField and click deploy.
I think this all works as I can then restart NodeRED and th new values are selected in the edit window and my code reflects those new values.

If I read config.myField, it doesn't update until I restart NodeRED. My trying with my previous code was my attempt to get around this.

I'm obviously missing something...

I don't think that is actually the best approach? Would it not be better to have a hidden input field that you update directly?

So if the property you wanted to configure was called myprop in the html defaults section, you would have a field <input type="hidden" id="node-input-myprop"> in the panel. Then create an on-change jQuery callback on the select element that updates the hidden input. I don't believe you will then need to mess with the on save function nor would you need to manually set the this.myprop variable.


Here is an example from uibuilder's uib-sender node:

From the panel html:

<div aria-label="uibuilder URL to associate with" class="form-row" title="uibuilder URL to associate with">
    <label for="node-input-url"><i class="fa fa-globe"></i> URL</label>
    <select id="node-input-url">
        <option value="" name="">--choose a uibuilder instance--</option>
    </select>
</div>

and from the script's oneditprepare:

        $('#node-input-url')
            .on('change', function() {
                node.uibId = Object.keys(uibInstances)[Object.values(uibInstances).indexOf(this.value)]
                // console.log('>> URL Change >>', this.value, node.uibId, Object.keys(uibInstances)[Object.values(uibInstances).indexOf(this.value)])
            })

url is defined in defaults as url: { value: '', required: true },

There is no oneditsave.

So in fact, my memory wasn't quite correct because I believe that just by putting id="node-input-url" on the select element, Node-RED already saves the updated value! I only have the on-change function because I need to set some other property as well.

Thanks again @TotallyInformation for your assistance.

In the end, with a bit of luck and some more persistence I managed to find out the issue. Posting here in case anyone has the same issue and possibly so I can be warned that this is a bad idea and there's a better way.

In my code, I am listening on various URLs, binding as below:

RED.httpNode.get('/etc/:area?/:filetype.xml', function(req, res) {

This code only gets called once (at the point when NodeRED starts up) therefore the config values are from when the restart of NodeRED happens.

I fixed this (with the help of ChatGPT) by closing the HTTP connection as below whenever the flow is deployed.

node.on('close', function(removed, done) {
    RED.httpNode._router.stack.forEach(function(route, i, routes) {
        if (route.route && route.route.path === '/etc/:area?/:filetype.xml') {
            routes.splice(i, 1);
        }
    });
    done();
}

My config values are now updated when the flow restarts, as was my original goal.

1 Like

Yes, tidying routes in Express can be tricky. In uibuilder, I have a LOT of routes and have to track them all. Express is very poor and being able to show you the active routes that you've added as well. I reorganised the uibuilder routes onto sub-router objects and also keep my own "log" of active routes.

Thankfully, taking out a single route isn't too bad.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.