Dynamic drop-down list issue in node-red editor for custom node

Hello All,

I have created custom node in node-red editor in which i have two drop-down lists.

  1. First drop down have 5 static items
  2. Second drop down is dynamic which is filled based on item selected in first drop down.

Issue:

  • I select an item from first drop down, it is showing correct items in second drop-down.
  • Selection from second drop down is also fine.
  • Just do save and exit the dialog box.

On again opening by double clikcing on my custom node, it is showing previously selected value for first drop-down correctly. But whatever item was selected on second drop-down is not persist. Even items are there in second drop down.

Is it expected behaviour in editor for dynamic drop down ?
Any pointers on this..

This is a common area for confusion - your not alone.

What you can do is.

  • Add the oneditprepare handler
  • Trigger the change for your parent dropdown within that handler
$('#node-input-some-select-element').trigger('change')

I am sure I don't have this fully right, but should be close

Hello @marcus-j-davies ,
Thanks for reply.

  • I added oneditprepare and added code for trigger:

$('#node-input-some-select-element').trigger('change')

It is doing something which is not intended:
First drop down has 4 items (a,b,c,d)
I selected 'a'

based on this second drop down shows (10,20,30,40,50)

then I selected 'b' from first dropdown
Now it is appeding (60,70,80,90) to existing items which were shown earlier..
so, now second drop down shows (10,20,30,40,50,60,70,80,90)

It is not intended anyhow..

original issue still there which is :
when I do select item from second drop down and save and close the dialog.

again opening it is not showing the item which was being selected in second drop down.

After you have triggered the change in the handler.

Try doing

$('#node-input-second-select-element').val(this.mySecondValueID)

If your second drop-down is growing rather than replacing, make sure that you empty the contents before changing them. Remember that a drop-down is an HTML select tag containing multiple option tags. So simply remove all of the select tag's children first.

1 Like

Hello @TotallyInformation
Issue is not related to growing item in drop down..

Drop down functionalities are working fine.. for selected item in first drop down it is showing items in second drop down.

Issue as I mentioned is when you select a item from first and second drop down and do save and close the edit dialog of node.

Then open again.. It is showing correct previously selected item in first drop down but for second dropdown it is not showing the previously selected item . Even second dropdown have all the items including previously selected one.

I think sharing your HTML file will help at this point.

```
HTML Here
```
1 Like

Hello @marcus-j-davies ,

Following is the HTML:


<script type="text/html" data-template-name="meal">
    <div class="form-row">
        <label for="node-input-meal_type">Type of Meal</label>
        <select name="meal_type" id="node-input-meal_type" onchange="onmealtypechange();">
            <option value="sel" selected>Select</option>
            <option value="breakfast" selected>Brekfast</option>
            <option value="lunch">Lunch</option>
            <option value="dinner">Dinner</option>
        </select>
    </div>

    <div class="form-row">
        <label for="node-input-meal">Select item</label>
        <select name="meal" id="node-input-meal" onchange="onmealitemchange();">
        </select>
    </div>
</script>

<script type="text/html" data-help-name="meal">
    <p>A node which represents meals</p>
</script>

listeners:

<script type="text/javascript">
    function onmealtypechange() {
        var sel = document.getElementById("node-input-meal_type");
        var drop = document.getElementById("node-input-meal");
        console.log("## meal items size (on type change): " + drop.length);
        drop.innerHTML="";

        console.log("## selected meal type: " + sel.value);

        if(sel.value == 'breakfast') {
            var opt1 = document.createElement('option');
            opt1.value = "";
            opt1.innerHTML = "Select";
            opt1.selected = true;
            drop.appendChild(opt1);

            var opt2 = document.createElement('option');
            opt2.value = 'tea';
            opt2.innerHTML = 'tea';
            drop.appendChild(opt2);

            var opt3 = document.createElement('option');
            opt3.value = 'milk';
            opt3.innerHTML = 'milk';
            drop.appendChild(opt3);
        } else if(sel.value == 'lunch') {


        } else if(sel.value == 'dinner') {

        }
    }

    function onmealitemchange() {

    }
</script>

Plugin:

<script type="text/javascript">
    RED.nodes.registerType('meal',{
        category: 'function',
        color: '#a6bbcf',
        defaults: {
            meal_type: {value: "sel"},
            meal: {value: ""}
        },
        inputs:0,
        outputs:1,
        icon: "",
        label: function() {
            return this.name||"Meal";
        },
        oneditprepare : function() {

            console.log("## on edit prepare meal_type: " + this.meal_type);
            console.log("## on edit prepare meal: " + this.meal);

            var meal_list = document.getElementById("node-input-meal_type");
            console.log("## Meal type size: " + meal_list.length);

            var meal = document.getElementById("node-input-meal");
            console.log("## meal items size: " + meal.length);
        },
        oneditsave : function(){

        }
    });
</script>

To reprodcue issue:

  1. drag meal node to editor.
  2. double click on meal node to edit -> select meal type (breakfast) -> select meal (tea or milk) -> done
  3. double click on meal node to edit

expected to show the meal_type = breakfast and meal = tea or milk (whichever is choosen in step 2)
now working as expected.

Not spotted a specific problem yet but a couple of things to tidy up maybe.

First, you have 2 items selected in the meal type?

Second, you are using raw DOM when you have access to jQuery. Not necessarily an issue but jQuery smooths out some of the DOM's oddities and can reduce the amount of code required.

Something else to remember - don't think it will impact the code you've shared but can catch you out. While the custom node's html file looks like a web page, it is NOT. It is a page fragment that is included along with other fragments. That means that your exposed JavaScript code is just that, exposed to the page global. Personally, I wrap all of my Editor code in an IIFE to prevent issues.

First, you have 2 items selected in the meal type?

=> I have corrected it ..

 <option value="sel" selected>Select</option>
 <option value="breakfast" selected>Brekfast</option>

===========================================

 <option value="sel" selected>Select</option>
<option value="breakfast" >Brekfast</option>

But it didn't have side effect.. :slight_smile:

One observation is whenever I just open edit dialogue of my meal node by double clicking.
It always call onmealtypechange()

But in that:
console.log("## meal items size (on type change): " + drop.length);

It always prints zero size, which is weird because:

  1. First time open meal node edit dialogue, it will print length as zero (It is Fine)
  2. Select menu type: breakfast, meal: tea .. save and exit dialogue
  3. Second time Open edit meal node edit dialogue. but now also it is printing length as zero. It should print 3.

Yes, because opening it is where it is CREATED in the DOM. It doesn't exist until then. So the onchange is triggered because it is changing (setting the initial value).

I think that is a timing issue.


You should note that I never use onchange in the html script. I only ever create a listener in the oneditprepare function. That function is executed once when the Editor is loaded and again whenever the panel is opened.

So use something like this in your oneditprepare function

$('#node-input-meal_type').on('change', function() {
    // NOTE that inside here, `this` refers to the selected DOM element 
    // and `$(this)` gives you a jQuerified version of the selected element.
    console.log(..)
})

You may also need to take the selected off completely. The first entry will be pre-selected anyway but I am not sure whether the Editor's panel handling will restore the saved value. Been a while since I did this I'm afraid. If it doesn't, you will also have to set the value in oneditprepare.

There is no difference on putting on change in oneditprepare:


            $('#node-input-meal_type').on('change', function() {
                // NOTE that inside here, `this` refers to the selected DOM element 
                // and `$(this)` gives you a jQuerified version of the selected element.
                var sel = document.getElementById("node-input-meal_type");
                var drop = document.getElementById("node-input-meal");
                console.log("## meal items size (on type change): " + drop.length);
                drop.innerHTML="";

                console.log("## selected meal type(edit prepare): " + sel.value);

                if(sel.value == 'breakfast') {
                    var opt1 = document.createElement('option');
                    opt1.value = "";
                    opt1.innerHTML = "Select";
                    opt1.selected = true;
                    drop.appendChild(opt1);

                    var opt2 = document.createElement('option');
                    opt2.value = 'tea';
                    opt2.innerHTML = 'tea';
                    drop.appendChild(opt2);

                    var opt3 = document.createElement('option');
                    opt3.value = 'milk';
                    opt3.innerHTML = 'milk';
                    drop.appendChild(opt3);
                }

            });

Result is same. Selected value is not persistent.

OK, was worth trying anyway. Timing edge-cases can be tricky.

var sel = document.getElementById("node-input-meal_type");

Firstly, best to use const or let since var gets hoisted and can be confusing.

Secondly, I think it would be better as:

const mealType = $(this).value
const drop = $("#node-input-meal");

Then simply replace sel.value as MealType. Feels cleaner that way.

You can also simplify your append code:

drop.append('<option value="" select>Select an option</option>')
drop.append('<option value="tea" select>Tea</option>')
...

Your way works of course but, to me at least, the other way is clearer.

Oh, and this:

 drop.innerHTML="";

could be this if you've used the jQuery above:

drop.empty();

Though not sure it really makes a difference.

But none of that makes it clear why you are getting your problem. Do you have your node in GitHub? If so, can I install it on my dev instance?

package.json

{
  "name": "node-red-meal-node",
  "version": "1.0.0",
  "description": "meal node implementation",
  "main": "meal-node.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "pioneerj",
  "license": "ISC",
  "node-red": {
    "nodes": {
      "meal-node": "meal-node.js"
    }
  }
}

meal-node.js


module.exports = function(RED) {
    function mealNode(config) {
        RED.nodes.createNode(this, config);
        
        var node = this;
        var msg = {};

        msg.payload = {
            "monitor_type" : "test"
        };

        setImmediate(() => {
            node.log(JSON.stringify(msg));
            node.send(msg);
            node.log("monitor : sent message from " + msg.node_name);
        });
    }

    RED.nodes.registerType("meal", mealNode);
}

Github repo link:

Try this:

        oneditprepare : function() {
            console.log("## on edit prepare");			
			
            $('#node-input-meal_type').on('change', function() {
                const mealType = $('#node-input-meal_type').val()
                console.log(`🍴## meal type changed: "${mealType}"`)

                const meal = $("#node-input-meal")

                meal.empty()
                meal.append('<option value="" select>Select an option</option>')

                switch (mealType) {
                    case 'breakfast': {
                        meal.append('<option value="tea">Tea</option>')
                        meal.append('<option value="milk">Milk</option>')
                        break
                    }
                
                    case 'lunch': {
                        meal.append('<option value="ham sandwich">Ham Sandwitch</option>')
                        meal.append('<option value="cheese sandwich">Cheese Sandwitch</option>')
                        break
                    }
                
                    case 'tea': {
                        meal.append('<option value="burger">Burger</option>')
                        meal.append('<option value="steak">Steak</option>')
                        break
                    }
                
                    default: {
                        break
                    }
                }
            })

            $('#node-input-meal').on('change', function() {
                const meal = $('#node-input-meal_type').val()
                console.log(`🍴## meal changed: "${meal}"`)
            })
        },

Less effort to write, a lot easier to read and not mixing jQuery/DOM which results in weird errors.

Clean code is good practice.. But it is not helping to solve issue. I tried with your changes as well..
But still original issue is there.

Steps are simple to reproduce:

  • Install GitHub - pioneerj-s/node-red-meal-node plugin

  • Drag meal node to editor.

  • Double click on meal node to edit it

  • Select meal type: breakfast

  • Select meal: milk

  • Click done

  • again Double click on meal node to edit.

  • You can see following on meal node
    - meal type: breakfast
    - meal: it should show milk which we selected previously but it is not showing.

Hope you are able to reprodcue original issue.