Skip to content

Commit

Permalink
Merge pull request #859 from FlowFuse/857-dynamic-props-dropdown
Browse files Browse the repository at this point in the history
UI Dropdown - Enable dynamic properties
  • Loading branch information
joepavitt authored May 30, 2024
2 parents 9f3b4a8 + 1b66937 commit 8d0ef4d
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 24 deletions.
42 changes: 40 additions & 2 deletions docs/contributing/widgets/core-widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,17 @@ The inputs for the `useDataTracker (widgetId, onInput, onLoad, onDynamicProperti

## Dynamic Properties

Node-RED allows for definition of the underlying configuration for a node. For example, a `ui-button` would have properties such as `label`, `color`, `icon`, etc. It is often desired to have these properties be dynamic, meaning that they can be changed at runtime. Users expect to be able to control these generally by passing in `msg.<property-name>` to the node, which in turn, should update the property.
Node-RED allows for definition of the underlying configuration for a node. For example, a `ui-button` would have properties such as `label`, `color`, `icon`, etc. It is often desired to have these properties be dynamic, meaning that they can be changed at runtime.

It is a standard practice within Dashboard 2.0 to support these property updates via a nested `msg.ui_updates` object. As such, users can expect to be able to control these generally by passing in `msg.ui_updates.<property-name>` to the node, which in turn, should update the appropriate property.

### Design Pattern

This section will outline the architectural design pattern for developing dynamic properties into a widget.

Server-side, dynamic properties are stored in our `state` store, which is a mapping of the widget ID to the dynamic properties assigned to that widget. This is done so that we can ensure separation of the dynamic properties for a widget from the initial configuration defined, and stored, in Node-RED.

Before the `ui-base` node emits the `ui-config`, we merge the dynamic properties with the initial configuration, with the dynamic properties permitted to override the underlying configuration. As such, when the client receives a `ui-config` message, it will have the most up-to-date configuration for the widget, the merging of both static and dynamic properties.
Before the `ui-base` node emits the `ui-config` event and payload, we merge the dynamic properties with the initial configuration, with the dynamic properties permitted to override the underlying configuration. As such, when the client receives a `ui-config` message, it will have the most up-to-date configuration for the widget, wth the merging of both static and dynamic properties.

### Setting Dynamic Properties

Expand Down Expand Up @@ -155,6 +157,42 @@ Now that we have the server-side state updating, anytime we refresh, the full `u

We then need to ensure that the client is aware of these dynamic properties _as they change_. To do this, we can use the `onDynamicProperties` event available in the [data tracker](#data-tracker).

A good pattern to follow is provide a `computed` variable on the component in question. This computed variable can then check a local, component-scoped, variable that is overridden when dynamic properties are set, if that hasn't been set, fall back to the `props.<property>` value.

```js
{
// ...,
data () {
return {
// ...,
dynamic: {
label: null
}
}
},
computed: {
label () {
return this.dynamic.label !== null ? this.dynamic.label : this.props.label
}
},
created () {
// we can define a custom onDynamicProperty handler for this widget
useDataTracker(this.id, null, null, this.onDynamicProperty)
// ...,
methods () {
// ...,
onDynamicProperty (msg) {
// standard practice to accept updates via msg.ui_updates
const updates = msg.ui_updates
if (typeof updates?.label !== 'undefined') {
this.dynamic.label = updates.label
}
}
}
}

```
### Updating Documentation
There are two important places to ensure documentation is updated when adding dynamic properties:
Expand Down
18 changes: 14 additions & 4 deletions docs/nodes/widgets/ui-dropdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@ description: Incorporate ui-dropdown in Node-RED Dashboard 2.0 for user selectio
props:
Group: Defines which group of the UI Dashboard this widget will render in.
Size: Controls the width of the dropdown with respect to the parent group. Maximum value is the width of the group.
Label: The text shown to the left of the dropdown.
Label:
description: The text shown to the left of the dropdown.
dynamic: true
Options:
description: A list of the options available in the dropdown. Each row defines a `label` (shown in the dropdown) and `value` (emitted on selection) property.
dynamic: true
Allow Multiple: Whether or not a user can select multiple options, if so, checkboxes are shown, and value is emitted in an array.
Allow Multiple:
description: Whether or not a user can select multiple options, if so, checkboxes are shown, and value is emitted in an array.
dynamic: true
dynamic:
Label:
payload: msg.ui_update.label
structure: ["String"]
Options:
payload: msg.options
payload: msg.ui_update.options
structure: ["Array<String>", "Array<{value: String}>", "Array<{value: String, label: String}>"]
Allow Multiple:
payload: msg.ui_update.multiple
structure: ["Boolean"]
Class:
payload: msg.class
payload: msg.ui_update.class
structure: ["String"]
---

Expand Down
2 changes: 2 additions & 0 deletions nodes/widgets/locales/en-US/ui_dropdown.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ <h3>Selecting Options via <code>msg.</code></h3>
<h3>Dynamic Properties (Inputs)</h3>
<p>Any of the following can be appended to a <code>msg.</code> in order to override or set properties on this node at runtime.</p>
<dl class="message-properties">
<dt class="optional">label <span class="property-type">string</span></dt>
<dd>The label displayed to explain the purpose of the dropdown.</dd>
<dt class="optional">options <span class="property-type">array</span></dt>
<dd>
Change the options available in the dropdown at runtime
Expand Down
21 changes: 14 additions & 7 deletions nodes/widgets/ui_dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ module.exports = function (RED) {
const evts = {
onChange: true,
beforeSend: function (msg) {
if (msg.options) {
// dynamically set "options" property
statestore.set(group.getBase(), node, msg, 'options', msg.options)
}
if (msg.label) {
// dynamically set "label" property
statestore.set(group.getBase(), node, msg, 'label', msg.label)
if (msg.ui_update) {
const update = msg.ui_update
if (update.options) {
// dynamically set "options" property
statestore.set(group.getBase(), node, msg, 'options', update.options)
}
if (typeof update.label !== 'undefined') {
// dynamically set "label" property
statestore.set(group.getBase(), node, msg, 'label', update.label)
}
if (typeof update.multiple !== 'undefined') {
// dynamically set "label" property
statestore.set(group.getBase(), node, msg, 'multiple', update.multiple)
}
}
return msg
}
Expand Down
45 changes: 34 additions & 11 deletions ui/src/widgets/ui-dropdown/UIDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
v-model="value"
:disabled="!state.enabled"
:class="className"
:label="props.label"
:multiple="props.multiple"
:label="label"
:multiple="multiple"
:items="options"
item-title="label"
item-value="value"
Expand All @@ -31,7 +31,11 @@ export default {
data () {
return {
value: null,
items: null
items: null,
dynamic: {
label: null,
multiple: null
}
}
},
computed: {
Expand All @@ -58,6 +62,12 @@ export default {
set (value) {
this.items = value
}
},
multiple: function () {
return this.dynamic.multiple === null ? this.props.multiple : this.dynamic.multiple
},
label: function () {
return this.dynamic.label !== null ? this.dynamic.label : this.props.label
}
},
created () {
Expand All @@ -82,26 +92,39 @@ export default {
// 1. add/replace the dropdown options (to support dynamic options e.g: nested dropdowns populated from a database)
// 2. update the selected value(s)
const payload = msg.payload
if (payload !== undefined) {
// 2. update the selected value(s)
this.select(payload)
}
// keep options out for backward compatibility
const options = msg.options
if (options) {
// 1. add/replace the dropdown options
// TODO: Error handling if options is not an array
this.items = options
}
const payload = msg.payload
if (payload !== undefined) {
// 2. update the selected value(s)
this.select(payload)
// update the UI with any other changes
const updates = msg.ui_updates
if (updates) {
if (Array.isArray(updates.options)) {
this.items = updates.options
}
if (typeof updates.label !== 'undefined') {
this.dynamic.label = updates.label
}
if (typeof updates.multiple !== 'undefined') {
this.dynamic.multiple = updates.multiple
}
}
// additionally, we need to support both single and multi selection
// For now, we only support selecting which item(s) are selected, not updating the available options
// if the payload is an array, we assume it is a list of values to select
},
onChange () {
// ensure our data binding with vuex store is updated
const msg = this.messages[this.id] || {}
if (this.props.multiple) {
if (this.multiple) {
// return an array
msg.payload = this.value.map((option) => {
return option.value
Expand Down

0 comments on commit 8d0ef4d

Please sign in to comment.