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

Dynamic Options - Implement new helper functions & update widget-load behaviour #1123

Merged
merged 11 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions docs/contributing/widgets/core-widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,22 +187,20 @@ 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.
A good pattern to follow is provide a `computed` variable on the component in question. We then provide three helpful, global, functions:

- `setDynamicProperties(config)`: Will assign the provided properties (in `config`) to the widget, in the client-side store. This will automatically update the widget's state, and any references using this property.
- `updateDynamicProperty(property, value)`: Will update the relevant `property` with the provided `value` in the client-side store. Will also ensure the property is not of type `undefined`. This will automatically update the widget's state, and any references using this property.
- `getProperty(property)`: Automatically gets the correct value for the requested property. Will first look in the dynamic properties, and if not found, will default to the static configuration defined in the [`ui-config` event](../guides/events.md#ui-config).

The computed variables can wrap the `this.getProperty` function, which will always be up-to-date with the centralized vuex store.

```js
{
// ...,
data () {
return {
// ...,
dynamic: {
label: null
}
}
},
// ...
computed: {
label () {
return this.dynamic.label !== null ? this.dynamic.label : this.props.label
return this.getProperty('label')
}
},
created () {
Expand All @@ -214,9 +212,8 @@ A good pattern to follow is provide a `computed` variable on the component in qu
onDynamicProperty (msg) {
// standard practice to accept updates via msg.ui_update
const updates = msg.ui_update
if (typeof updates?.label !== 'undefined') {
this.dynamic.label = updates.label
}
// use globally available API to update the dynamic property
this.updateDynamicProperty('label', updates.label)
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,18 +670,19 @@ module.exports = function (RED) {
}
async function handler () {
let msg = datastore.get(id)
const state = statestore.getAll(id)
RED.plugins.getByType('node-red-dashboard-2').forEach(plugin => {
if (plugin.hooks?.onLoad) {
msg = plugin.hooks.onLoad(conn, id, msg)
msg = plugin.hooks.onLoad(conn, id, msg, state)
}
})

if (!msg) {
if (!msg && !state) {
// a plugin has made msg blank - meaning that we do anything else
return
}

conn.emit('widget-load:' + id, msg)
conn.emit('widget-load:' + id, msg, state)
}
// wrap execution in a try/catch to ensure we don't crash Node-RED
try {
Expand Down
4 changes: 4 additions & 0 deletions nodes/widgets/ui_dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ module.exports = function (RED) {
statestore.set(group.getBase(), node, msg, 'multiple', update.multiple)
}
}
if (msg.options) {
// backward compatibility support
statestore.set(group.getBase(), node, msg, 'options', msg.options)
}
return msg
}
}
Expand Down
30 changes: 30 additions & 0 deletions ui/src/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,36 @@ fetch('_setup')
const head = createHead()
app.use(head)
app.mixin(VueHeadMixin)
app.mixin({
methods: {
setDynamicProperties (config) {
this.$store.commit('ui/widgetState', {
widgetId: this.id,
config
})
},
updateDynamicProperty (property, value) {
if (!property && typeof property !== 'string') {
throw new Error('updateDynamicProperty requires a valid, string "property" argument')
}
if (typeof value !== 'undefined') {
const config = {}
config[property] = value
this.$store.commit('ui/widgetState', {
widgetId: this.id,
config
})
}
},
// retrieves a property from the store for a given widget
getProperty (property) {
const config = this.props ? this.props[property] : null // last known value for the config of this widget property
const state = this.state[property] // chec if there have been any dynamic updates to this property
// return the dynamic property if it exists, otherwise return the last known configuration
return this.state && property in this.state && state !== null ? state : config
}
}
})

// make the socket service available app-wide via this.$socket
app.provide('$socket', socket)
Expand Down
16 changes: 8 additions & 8 deletions ui/src/store/ui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,10 @@ const mutations = {
},
widgetState (state, data) {
const wId = data.widgetId
const config = data.config

if ('enabled' in data) {
state.widgets[wId].state.enabled = data.enabled
}
if ('visible' in data) {
state.widgets[wId].state.visible = data.visible
}
if ('class' in data) {
state.widgets[wId].state.class = data.class
for (const prop in config) {
state.widgets[wId].state[prop] = config[prop]
}
},
/**
Expand All @@ -176,6 +171,11 @@ const mutations = {
*/
setProperty (state, { item, itemId, property, value }) {
state[item + 's'][itemId][property] = value
},
setProperties (state, { item, itemId, config }) {
for (const prop in config) {
state[item + 's'][itemId][prop] = config[prop]
}
}
}

Expand Down
24 changes: 19 additions & 5 deletions ui/src/widgets/data-tracker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
if ('enabled' in msg) {
store.commit('ui/widgetState', {
widgetId,
enabled: msg.enabled
config: {
enabled: msg.enabled
}
})
}

if ('visible' in msg) {
store.commit('ui/widgetState', {
widgetId,
visible: msg.visible
config: {
visible: msg.visible
}
})
}

if ('class' in msg || ('ui_update' in msg && 'class' in msg.ui_update)) {
const cls = msg.class || msg.ui_update?.class
store.commit('ui/widgetState', {
widgetId,
class: cls
config: {
class: cls
}
})
}

Expand All @@ -43,7 +49,15 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
// lifecycle to setup and teardown side effects.
onMounted(() => {
if (socket && widgetId) {
socket.on('widget-load:' + widgetId, (msg) => {
socket.on('widget-load:' + widgetId, (msg, state) => {
// automatic handle state/dynamic updates for ALL widgets
if (state) {
store.commit('ui/widgetState', {
widgetId,
config: state
})
}
// then see if there is custom onLoad functionality to deal with the latest data payloads
if (onLoad) {
onLoad(msg)
} else {
Expand All @@ -57,7 +71,7 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
})
// This will on in msg input for ALL components
socket.on('msg-input:' + widgetId, (msg) => {
// check for dynamic properties
// check for common dynamic properties cross all widget types
checkDynamicProperties(msg)

if (onInput) {
Expand Down
44 changes: 20 additions & 24 deletions ui/src/widgets/ui-button-group/UIButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ export default {
},
data () {
return {
selection: null,
dynamic: {
label: null,
options: null
}
selection: null
}
},
computed: {
Expand All @@ -48,10 +44,10 @@ export default {
return this.look === 'default' ? null : this.look
},
label: function () {
return this.dynamic.label || this.props.label
return this.getProperty('label')
},
options: function () {
const options = this.dynamic.options || this.props.options
const options = this.getProperty('options')
if (options) {
return options.map(option => {
if (typeof option === 'string') {
Expand Down Expand Up @@ -91,29 +87,29 @@ export default {
}
},
onLoad (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg?.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
if (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
}
}
}
}
},
onDynamicProperty (msg) {
const updates = msg.ui_update
if (typeof updates?.label !== 'undefined') {
this.dynamic.label = updates.label
}
if (typeof updates?.options !== 'undefined') {
this.dynamic.options = updates.options
if (updates) {
this.updateDynamicProperty('label', updates.label)
this.updateDynamicProperty('options', updates.options)
}
},
onChange (value) {
Expand Down
55 changes: 14 additions & 41 deletions ui/src/widgets/ui-button/UIButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,35 @@ export default {
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
data () {
return {
dynamic: {
label: null,
icon: null,
buttonColor: null,
textColor: null,
iconColor: null,
iconPosition: null
}
}
},
computed: {
...mapState('data', ['messages']),
prependIcon () {
const icon = this.getPropertyValue('icon')
const icon = this.getProperty('icon')
const mdiIcon = this.makeMdiIcon(icon)
return icon && this.iconPosition === 'left' ? mdiIcon : undefined
},
appendIcon () {
const icon = this.getPropertyValue('icon')
const icon = this.getProperty('icon')
const mdiIcon = this.makeMdiIcon(icon)
return icon && this.iconPosition === 'right' ? mdiIcon : undefined
},
label () {
return this.getPropertyValue('label')
return this.getProperty('label')
},
iconPosition () {
return this.getPropertyValue('iconPosition')
return this.getProperty('iconPosition')
},
iconOnly () {
return this.getPropertyValue('icon') && !this.getPropertyValue('label')
return this.getProperty('icon') && !this.getProperty('label')
},
buttonColor () {
return this.getPropertyValue('buttonColor')
return this.getProperty('buttonColor')
},
iconColor () {
return this.getPropertyValue('iconColor')
return this.getProperty('iconColor')
},
textColor () {
return this.getPropertyValue('textColor')
return this.getProperty('textColor')
}
},
created () {
Expand All @@ -92,27 +80,12 @@ export default {
if (!updates) {
return
}
if (typeof updates.label !== 'undefined') {
this.dynamic.label = updates.label
}
if (typeof updates.icon !== 'undefined') {
this.dynamic.icon = updates.icon
}
if (typeof updates.iconPosition !== 'undefined') {
this.dynamic.iconPosition = updates.iconPosition
}
if (typeof updates.buttonColor !== 'undefined') {
this.dynamic.buttonColor = updates.buttonColor
}
if (typeof updates.textColor !== 'undefined') {
this.dynamic.textColor = updates.textColor
}
if (typeof updates.iconColor !== 'undefined') {
this.dynamic.iconColor = updates.iconColor
}
},
getPropertyValue (property) {
return this.dynamic[property] !== null ? this.dynamic[property] : this.props[property]
this.updateDynamicProperty('label', updates.label)
this.updateDynamicProperty('icon', updates.icon)
this.updateDynamicProperty('iconPosition', updates.iconPosition)
this.updateDynamicProperty('buttonColor', updates.buttonColor)
this.updateDynamicProperty('textColor', updates.textColor)
this.updateDynamicProperty('iconColor', updates.iconColor)
}
}
}
Expand Down
Loading
Loading