Skip to content

Commit

Permalink
Merge pull request #1123 from FlowFuse/1122-dynamic-props-rewrite
Browse files Browse the repository at this point in the history
Dynamic Options - Implement new helper functions & update widget-load behaviour
  • Loading branch information
joepavitt authored Aug 7, 2024
2 parents 27866e4 + 75124f2 commit 1d19cf6
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 212 deletions.
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

0 comments on commit 1d19cf6

Please sign in to comment.