Change dropdown values based on selection from another tab's dropdown

i have an embedded view with four tabs representing configurations for four ports of a sensor platform. each tab has a dropdown list of the port's attached sensor type. some are mutually exclusive; ie. if port1 has a pressure sensor, none of the others can have one, and the remaining port's dropdown lists should not display that choice.

i'm not sure how best to go about this. i am learning about message handlers and was wondering if this is a good use-case: sending a configuration payload based on selection, but that feels messy since i'd basically have to create a handler matrix and that doesn't seem like the best way to do this.

any suggestions? i'm hoping there is a simpler solution that i'm just overlooking.

Where is the configuration information stored, in a tag, database, PLC, etc.?

I would build a "master" list of possibilities in view custom prop, probably as a dataset. Would include columns that express the exclusion constraints.

Then each of the four dropdowns would have their value bidirectionally bound to a view custom prop to make it available to the other props, and would have an expression on its item list that pruned the master list according to the other exclusions. Naturally, I would use my Integration Toolkit functions to make this efficient. (:

while waiting, this is what i came up with as a binding on the options prop for the tab's dropdown:

options = []
	
min1_procType = self.parent.parent.parent.getChild("mInput1-config").getChild("flx-processType").getChild("dd-SET-processType").props.value
	
if value:	#<-- if the port is active, proceed
	if value == 1:
		# to make this goof-proof, check if ANY other input has already selected that sensor type.  they are exclusive.
		if min1_procType != 0 or min2_procType != 0 or min3_procType != 0:
			options.append({ "value": 0, "label": "Pressure Sensor" })
	
		return options
			
	if value == 2:
		if min1_procType != 0 or min2_procType != 0 or min3_procType != 0:
			options.append({ "value": 0, "label": "Flow Switch" })
			
		return options
		
return options

and since this will run on each tab switch, it seems a good, simple solution. but maybe there's something i'm missing that would cause this to fail? i'm open to better methodology.

EDIT: i plan to export the settings via message handler to the consuming view as well as another message handler to update the bidirectional tag values on the custom.session props.

Can you share a list of all options and indicate which ones are exclusive?

yeah. basically the options are:

  1. pressure
  2. temp
  3. flow switch
  4. level switch

only one can exist on any given port, so, for example, i can't have port 1 set to pressure and port 2 also set to pressure.

If there're only four choices, and four dropdowns, you'll never be able to change any. There must also be an "unconfigured" or 'disabled" option to have freedom to change anything.

1 Like

ah! that explains what i'm now looking at. thank you! :slight_smile:

can you explain why that is?

hm. noticing when i choose and option and then change tabs, the option lists aren't recalculating... how do i trigger a binding refresh? i know there's a system.refreshBinding() but i'm not sure where to put it to get the job done.

Try this view (with my Integration Toolkit installed):

{
  "custom": {
    "optionsMaster": {
      "$": [
        "ds",
        192,
        1712246364304
      ],
      "$columns": [
        {
          "data": [
            "",
            "pressure",
            "temp",
            "flowsw",
            "levelsw"
          ],
          "name": "code",
          "type": "String"
        },
        {
          "data": [
            "Disabled",
            "Pressure",
            "Temperature",
            "Flow Switch",
            "Level Switch"
          ],
          "name": "display",
          "type": "String"
        },
        {
          "data": [
            true,
            false,
            false,
            false,
            false
          ],
          "name": "multiples",
          "type": "Boolean"
        }
      ]
    },
    "port1": "pressure",
    "port2": "temp",
    "port3": "flowsw",
    "port4": "levelsw"
  },
  "params": {},
  "propConfig": {
    "custom.optionsMaster": {
      "persistent": true
    },
    "custom.port1": {
      "persistent": true
    },
    "custom.port2": {
      "persistent": true
    },
    "custom.port3": {
      "persistent": true
    },
    "custom.port4": {
      "persistent": true
    }
  },
  "props": {},
  "root": {
    "children": [
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Label"
                },
                "position": {
                  "basis": "200px"
                },
                "props": {
                  "style": {
                    "paddingRight": "10px"
                  },
                  "text": "Port #1 Function:",
                  "textStyle": {
                    "textAlign": "right"
                  }
                },
                "type": "ia.display.label"
              },
              {
                "meta": {
                  "name": "Dropdown"
                },
                "position": {
                  "basis": "250px"
                },
                "propConfig": {
                  "props.options": {
                    "binding": {
                      "config": {
                        "expression": "forEach(\n\twhere(\n\t\t{view.custom.optionsMaster},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port2},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port3},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port4}\n\t),\n\tasMap(\n\t\t\u0027value\u0027, it()[\u0027code\u0027],\n\t\t\u0027label\u0027, it()[\u0027display\u0027]\n\t)\n)"
                      },
                      "type": "expr"
                    }
                  },
                  "props.value": {
                    "binding": {
                      "config": {
                        "bidirectional": true,
                        "path": "view.custom.port1"
                      },
                      "type": "property"
                    }
                  }
                },
                "type": "ia.input.dropdown"
              }
            ],
            "meta": {
              "name": "TabRow"
            },
            "position": {
              "basis": "50px"
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "Tab1Column"
        },
        "props": {
          "direction": "column"
        },
        "type": "ia.container.flex"
      },
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Label"
                },
                "position": {
                  "basis": "200px"
                },
                "props": {
                  "style": {
                    "paddingRight": "10px"
                  },
                  "text": "Port #2 Function:",
                  "textStyle": {
                    "textAlign": "right"
                  }
                },
                "type": "ia.display.label"
              },
              {
                "meta": {
                  "name": "Dropdown"
                },
                "position": {
                  "basis": "250px"
                },
                "propConfig": {
                  "props.options": {
                    "binding": {
                      "config": {
                        "expression": "forEach(\n\twhere(\n\t\t{view.custom.optionsMaster},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port1},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port3},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port4}\n\t),\n\tasMap(\n\t\t\u0027value\u0027, it()[\u0027code\u0027],\n\t\t\u0027label\u0027, it()[\u0027display\u0027]\n\t)\n)"
                      },
                      "type": "expr"
                    }
                  },
                  "props.value": {
                    "binding": {
                      "config": {
                        "bidirectional": true,
                        "path": "view.custom.port2"
                      },
                      "type": "property"
                    }
                  }
                },
                "type": "ia.input.dropdown"
              }
            ],
            "meta": {
              "name": "TabRow"
            },
            "position": {
              "basis": "50px"
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "Tab2Column"
        },
        "position": {
          "tabIndex": 1
        },
        "props": {
          "direction": "column"
        },
        "type": "ia.container.flex"
      },
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Label"
                },
                "position": {
                  "basis": "200px"
                },
                "props": {
                  "style": {
                    "paddingRight": "10px"
                  },
                  "text": "Port #3 Function:",
                  "textStyle": {
                    "textAlign": "right"
                  }
                },
                "type": "ia.display.label"
              },
              {
                "meta": {
                  "name": "Dropdown"
                },
                "position": {
                  "basis": "250px"
                },
                "propConfig": {
                  "props.options": {
                    "binding": {
                      "config": {
                        "expression": "forEach(\n\twhere(\n\t\t{view.custom.optionsMaster},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port1},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port2},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port4}\n\t),\n\tasMap(\n\t\t\u0027value\u0027, it()[\u0027code\u0027],\n\t\t\u0027label\u0027, it()[\u0027display\u0027]\n\t)\n)"
                      },
                      "type": "expr"
                    }
                  },
                  "props.value": {
                    "binding": {
                      "config": {
                        "bidirectional": true,
                        "path": "view.custom.port3"
                      },
                      "type": "property"
                    }
                  }
                },
                "type": "ia.input.dropdown"
              }
            ],
            "meta": {
              "name": "TabRow"
            },
            "position": {
              "basis": "50px"
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "Tab3Column"
        },
        "position": {
          "tabIndex": 2
        },
        "props": {
          "direction": "column"
        },
        "type": "ia.container.flex"
      },
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Label"
                },
                "position": {
                  "basis": "200px"
                },
                "props": {
                  "style": {
                    "paddingRight": "10px"
                  },
                  "text": "Port #4 Function:",
                  "textStyle": {
                    "textAlign": "right"
                  }
                },
                "type": "ia.display.label"
              },
              {
                "meta": {
                  "name": "Dropdown"
                },
                "position": {
                  "basis": "250px"
                },
                "propConfig": {
                  "props.options": {
                    "binding": {
                      "config": {
                        "expression": "forEach(\n\twhere(\n\t\t{view.custom.optionsMaster},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port1},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port2},\n\t\tit()[\u0027multiples\u0027] || it()[\u0027code\u0027] !\u003d {view.custom.port3}\n\t),\n\tasMap(\n\t\t\u0027value\u0027, it()[\u0027code\u0027],\n\t\t\u0027label\u0027, it()[\u0027display\u0027]\n\t)\n)"
                      },
                      "type": "expr"
                    }
                  },
                  "props.value": {
                    "binding": {
                      "config": {
                        "bidirectional": true,
                        "path": "view.custom.port4"
                      },
                      "type": "property"
                    }
                  }
                },
                "type": "ia.input.dropdown"
              }
            ],
            "meta": {
              "name": "TabRow"
            },
            "position": {
              "basis": "50px"
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "Tab4Column"
        },
        "position": {
          "tabIndex": 3
        },
        "props": {
          "direction": "column"
        },
        "type": "ia.container.flex"
      }
    ],
    "meta": {
      "name": "root"
    },
    "props": {
      "currentTabIndex": 3,
      "tabs": [
        "Tab1",
        "Tab2",
        "Tab3",
        "Tab4"
      ]
    },
    "type": "ia.container.tab"
  }
}

Edit: For the uninitiated, you can use the above JSON of an entire view by creating a new Perspective view, put the above on the clipboard, SHIFT-right-click on the new view, and select "Paste JSON". The clipboard content will completely replace the new view. The above view is completely stand-alone (other than using Integration Toolkit functions--see below for an alternative).

1 Like

*gulp* okay. not going to pretend i'm not intimidated. :smiley: but i'll dive in and give that a go. thanks @pturmel.

The magic relies on a bidirectional binding from the dropdown value to a corresponding view custom prop, then an expression binding that prunes the master list of options with my where() function (port #1's option binding shown):

forEach(
	where(
		{view.custom.optionsMaster},
		it()['multiples'] || it()['code'] != {view.custom.port2},
		it()['multiples'] || it()['code'] != {view.custom.port3},
		it()['multiples'] || it()['code'] != {view.custom.port4}
	),
	asMap(
		'value', it()['code'],
		'label', it()['display']
	)
)

Note that each options binding is slightly different--each only prunes the settings for the other ports.

i'm sorry to say, this project doesn't have the time budget to learn this properly. :frowning: i can see that @pturmel answer is comprehensive and very flexible, but we just don't have the budget to learn then implement on this project.

is there no other way to simply have refreshBinding fire on list change so the bindings on the other tabs update?

EDIT:
i tried putting this in the changeScript on the currentTabIndex of the root container in the hopes that when the index changes, it will run this script:

system.refreshBinding('self.parent.parent.getSibling("mInput1-config").getChild("flx-processType").getChild("dd-SET-processType").props.options')
system.refreshBinding('self.parent.parent.getSibling("mInput2-config").getChild("flx-processType").getChild("dd-SET-processType").props.options')
system.refreshBinding('self.parent.parent.getSibling("mInput3-config").getChild("flx-processType").getChild("dd-SET-processType").props.options')
system.refreshBinding('self.parent.parent.getSibling("mInput4-config").getChild("flx-processType").getChild("dd-SET-processType").props.options')

but no :heart_decoration:. i can't tell if it flat out is NOT firing, or if it's malconfigured: either on the wrong prop or what.

I find this surprising. There are eight bindings in the entire view. Four are bidirectional property bindings. The other four are variations on the one I show above. There are no scripts at all.

Did you drop the JSON into a view to even look at it?

(To make the entire thing embeddable, you'd move the port1..port4 view custom props to view in/out parameters.)

in truth, no. i just saw a wall of code and kinda soft-locked. i didn't realize it was a JSON dump. :face_with_open_eyes_and_hand_over_mouth: plus, my OCD brain won't let go of the refreshBinding problem. but that's a 'me' issue... :clown_face: :

the biggest obstacle, truly, is getting approval to install the your required toolset module. :man_shrugging:

system.refreshBinding ?

try self.parent.parent.getSibling("mInput4-config").getChild("flx-processType").getChild("dd-SET-processType").refreshBinding('props.options')

If you can't, use the same architecture, but replace the options bindings with this:

runScript(
	'someLibrary.prunePortOptions',
	0,
	{view.custom.optionsMaster},
	{view.custom.port2},
	{view.custom.port3},
	{view.custom.port4}
)

and put this into someLibrary:

def prunePortOptions(ds, *ports):
	options = []
	for row in system.dataset.toPyDataSet(ds):
		if row['multiples'] or row['code'] not in ports:
			options.append({'value': row['code'], 'label': row['display']})
	return options

One script. Everything else is expressions, and the expressions reference everything they need to auto-refresh correctly.

1 Like

not letting me paste you views JSON... :stuck_out_tongue: i'm running v8.1.27, but i don't suspect that's the issue...

Are you holding down SHIFT + right click?

image

1 Like

this kind of thing irritates me; how would i have known that? (nothing bad on you) *sigh* thanks though.