Skip to content

Commit

Permalink
Merge pull request #765 from bartbutenaers/dynamic-radio-options
Browse files Browse the repository at this point in the history
Dynamic radio options
  • Loading branch information
joepavitt authored Apr 17, 2024
2 parents 6e66abf + bb43566 commit 4e73e3f
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 14 deletions.
11 changes: 11 additions & 0 deletions docs/nodes/widgets/ui-radio-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ props:
Columns: The number of grid columns within which to render the radio group. This is useful for when you want to render the options horizontally, or if you have many ptions and want to save vertical space.
Topic: The `msg.topic` that will be included in any emitted values
dynamic:
Options:
payload: msg.options
structure: ["Array<String>", "Array<{value: String}>", "Array<{value: String, label: String}>"]
Class:
payload: msg.class
structure: ["String"]
Expand All @@ -19,6 +22,14 @@ dynamic:

Adds a Radio Group to your dashboard that will emit values in Node-RED under `msg.payload` anytime a value is selected.

## Programmatic Selections

You can dynamically make selections for this dropdown by passing in the respective `value` to `msg.payload`, e.g. `msg.payload = "option1"`.

### Clear Selection

To clear any selection for a dropdown, pass a `null` as `msg.payload`.

## Properties

<PropsTable/>
Expand Down
2 changes: 1 addition & 1 deletion nodes/widgets/ui_notification.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@
</div>
<div class="form-tips"><b>Note</b>: checking <i>Accept raw HTML/JavaScript</i> can allow injection of code.
Ensure the input comes from trusted sources.</span></div>
</script>
</script>
11 changes: 10 additions & 1 deletion nodes/widgets/ui_radio_group.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const statestore = require('../store/state.js')

module.exports = function (RED) {
function RadioGroupNode (config) {
// create node in Node-RED
Expand All @@ -9,7 +11,14 @@ module.exports = function (RED) {
const group = RED.nodes.getNode(config.group)

const evts = {
onChange: true
onChange: true,
beforeSend: function (msg) {
if (msg.options) {
// dynamically set "options" property
statestore.set(group.getBase(), node, msg, 'options', msg.options)
}
return msg
}
}

// inform the dashboard UI that we are adding this node
Expand Down
110 changes: 98 additions & 12 deletions ui/src/widgets/ui-radio-group/UIRadioGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
v-model="value" class="nrdb-ui-radio-group" :disabled="!state.enabled"
:class="'nrdb-ui-radio-group--cols-' + props.columns + ' ' + className"
:label="label" variant="outlined" hide-details="auto"
@update:model-value="onChange"
>
<v-radio
v-for="option in props.options" :key="option.value"
v-for="option in options" :key="option.value"
:label="option.label" :value="option.value"
/>
</v-radio-group>
Expand All @@ -24,27 +25,112 @@ export default {
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
setup (props) {
useDataTracker(props.id)
data () {
return {
value: null,
items: null
}
},
computed: {
...mapState('data', ['messages']),
label: function () {
return this.props.label
},
value: {
options: {
get () {
return this.messages[this.id]?.payload
const items = this.items || this.props.options
return items.map((item) => {
if (typeof item !== 'object') {
return {
label: item,
value: item
}
} else if (!('label' in item) || item.label === '') {
return {
label: item.value,
value: item.value
}
} else {
return item
}
})
},
set (val) {
if (this.value === val) {
return // no change
set (value) {
this.items = value
}
}
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
useDataTracker(this.id, this.onInput, this.onLoad)
// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
},
methods: {
// given the last received msg into this node, load the state
onLoad (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
this.select(this.messages[this.id]?.payload)
},
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// When a msg comes in from Node-RED, we need support 2 operations:
// 1. add/replace the radio options (to support dynamic options e.g: radiobuttons populated from a database)
// 2. update the selected value(s)
const options = msg.options
if (options) {
// 1. add/replace the radio options
// TODO: Error handling if options is not an array
this.items = options
}
// 2. update the selected value(s)
const payload = msg.payload
if (payload || payload === '') {
this.select(payload)
}
},
onChange () {
// ensure our data binding with vuex store is updated
const msg = this.messages[this.id] || {}
if (this.value) {
// return a single value
msg.payload = this.value
} else {
// return null
msg.payload = null
}
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
this.$socket.emit('widget-change', this.id, msg.payload)
},
select (value) {
// An empty string value can be used to clear the current selection
if (value !== '') {
const option = this.options.find((o) => {
return o.value === value[0]
})
// if we didn't find any matching options, we stop here
if (!option) {
return
}
const msg = this.messages[this.id] || {}
msg.payload = val
this.messages[this.id] = msg
this.$socket.emit('widget-change', this.id, val)
}
// ensure we set our local "value" to match
this.value = value
}
}
}
Expand Down

0 comments on commit 4e73e3f

Please sign in to comment.