Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Dropdown - Enable dynamic properties #859

Merged
merged 11 commits into from
May 30, 2024
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') {
joepavitt marked this conversation as resolved.
Show resolved Hide resolved
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
Loading