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 Switch - Dynamic properties #1227

Merged
merged 1 commit into from
Aug 27, 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
23 changes: 22 additions & 1 deletion docs/nodes/widgets/ui-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,28 @@ controls:
description: Allow control over whether or not the switch can be toggled via the UI.
dynamic:
Class:
payload: msg.class
payload: msg.ui_update.class
structure: ["String"]
Label:
payload: msg.ui_update.label
structure: ["Boolean"]
Passthrough:
payload: msg.ui_update.passthru
structure: ["Boolean"]
Indicator:
payload: msg.ui_update.decouple
structure: ["Boolean"]
On Color:
payload: msg.ui_update.oncolor
structure: ["String"]
Off Color:
payload: msg.ui_update.offcolor
structure: ["String"]
On Icon:
payload: msg.ui_update.onicon
structure: ["String"]
Off Icon:
payload: msg.ui_update.officon
structure: ["String"]
---

Expand Down
32 changes: 30 additions & 2 deletions nodes/widgets/locales/en-US/ui_switch.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,37 @@ <h3>Properties</h3>
<dd>The type & value to output in msg.payload when the switch is turned off.</dd>
</dl>
<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>
<p>Any of the following can be appended to a <code>msg.ui_update</code> in order to override or set properties on this node at runtime.</p>
<dl class="message-properties">
<dt class="optional">class <span class="property-type">string</span></dt>
<dd>Add a CSS class, or more, to the Button at runtime.</dd>
</dl>
</script>
<dl class="message-properties">
<dt class="optional">label <span class="property-type">string</span></dt>
<dd>Change the switch label at runtime.</dd>
</dl>
<dl class="message-properties">
<dt class="optional">passthru <span class="property-type">boolean</span></dt>
<dd>Change the passthrough behaviour of input messages at runtime.</dd>
</dl>
<dl class="message-properties">
<dt class="optional">decouple <span class="property-type">boolean</span></dt>
<dd>Change the indicator at runtime. When <code>true</code> the icon shows state of the input, and when <code>false</code> the state of the output</dd>
</dl>
<dl class="message-properties">
<dt class="optional">oncolor <span class="property-type">string</span></dt>
<dd>Change the ON color of the switch at runtime.</dd>
</dl>
<dl class="message-properties">
<dt class="optional">offcolor <span class="property-type">string</span></dt>
<dd>Change the OFF color of the switch at runtime.</dd>
</dl>
<dl class="message-properties">
<dt class="optional">onicon <span class="property-type">string</span></dt>
<dd>Change the ON icon of the switch at runtime.</dd>
</dl>
<dl class="message-properties">
<dt class="optional">officon <span class="property-type">string</span></dt>
<dd>Change the OFF icon of the switch at runtime.</dd>
</dl>
</script>
69 changes: 44 additions & 25 deletions nodes/widgets/ui_switch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const datastore = require('../store/data.js')
const statestore = require('../store/state.js')
const { appendTopic } = require('../utils/index.js')

module.exports = function (RED) {
Expand All @@ -14,24 +15,27 @@ module.exports = function (RED) {
// which group are we rendering this widget
const group = RED.nodes.getNode(config.group)

// retrieve the assigned on/off values
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, node)
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, node)

config.evaluated = {
on,
off
}

const evts = {
// runs on UI interaction
// value = true | false from the ui-switch
onChange: async function (msg, value) {
// ensure we have latest instance of the widget's node
const wNode = RED.nodes.getNode(node.id)

// retrieve the assigned on/off value
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, wNode)
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, wNode)
msg.payload = value ? on : off

if (config.topic || config.topicType) {
msg = await appendTopic(RED, config, node, msg)
}

if (!config.passthru && config.decouple) {
wNode.send(msg)
node.send(msg)
} else {
node.status({
fill: value ? 'green' : 'red',
Expand All @@ -41,17 +45,11 @@ module.exports = function (RED) {
datastore.save(group.getBase(), node, msg)

// simulate Node-RED node receiving an input
wNode.send(msg)
node.send(msg)
}
},
onInput: async function (msg, send) {
let error = null
// ensure we have latest instance of the widget's node
const wNode = RED.nodes.getNode(node.id)

// retrieve the assigned on/off value
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, wNode)
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, wNode)

if (msg.payload === undefined) {
// may be setting class dynamically or something else that doesn't require a payload
Expand Down Expand Up @@ -100,22 +98,43 @@ module.exports = function (RED) {
}
},
beforeSend: async function (msg) {
// ensure we have latest instance of the widget's node
const wNode = RED.nodes.getNode(node.id)
const updates = msg.ui_update
if (updates) {
if (typeof updates.label !== 'undefined') {
// dynamically set "label" property
statestore.set(group.getBase(), node, msg, 'label', updates.label)
}
if (typeof updates.passthru !== 'undefined') {
// dynamically set "passthru" property
statestore.set(group.getBase(), node, msg, 'passthru', updates.passthru)
}
if (typeof updates.decouple !== 'undefined') {
// dynamically set "decouple" property
statestore.set(group.getBase(), node, msg, 'decouple', updates.decouple)
}
if (typeof updates.oncolor !== 'undefined') {
// dynamically set "oncolor" property
statestore.set(group.getBase(), node, msg, 'oncolor', updates.oncolor)
}
if (typeof updates.offcolor !== 'undefined') {
// dynamically set "offcolor" property
statestore.set(group.getBase(), node, msg, 'offcolor', updates.offcolor)
}
if (typeof updates.onicon !== 'undefined') {
// dynamically set "onicon" property
statestore.set(group.getBase(), node, msg, 'onicon', updates.onicon)
}
if (typeof updates.officon !== 'undefined') {
// dynamically set "officon" property
statestore.set(group.getBase(), node, msg, 'officon', updates.officon)
}
}

msg = await appendTopic(RED, config, wNode, msg)
msg = await appendTopic(RED, config, node, msg)
return msg
}
}

const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, node)
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, node)

config.evaluated = {
on,
off
}

// inform the dashboard UI that we are adding this node
group.register(node, config, evts)
}
Expand Down
61 changes: 42 additions & 19 deletions ui/src/widgets/ui-switch/UISwitch.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<template>
<div class="nrdb-switch" :class="{'nrdb-nolabel': !props.label, [className]: !!className}">
<label v-if="props.label" class="v-label">{{ props.label }}</label>
<div class="nrdb-switch" :class="{'nrdb-nolabel': !label, [className]: !!className}">
<label v-if="label" class="v-label">{{ label }}</label>
<v-switch
v-if="!icon" v-model="status"
:disabled="!state.enabled" :class="{'active': status}"
:disabled="!state.enabled"
:class="{'active': status}"
hide-details="auto" color="primary"
:loading="loading ? (status === true ? 'secondary' : 'primary') : null"
readonly
Expand Down Expand Up @@ -33,21 +34,30 @@ export default {
},
computed: {
...mapState('data', ['messages']),
icon: function () {
if (this.props.onicon && this.props.officon) {
const icon = this.status ? this.props.onicon : this.props.officon
label () {
return this.getProperty('label')
},
icon () {
const onicon = this.getProperty('onicon')
const officon = this.getProperty('officon')

if (onicon && officon) {
const icon = this.status ? onicon : officon
return 'mdi-' + icon.replace(/^mdi-/, '')
} else {
return null
}
},
color: function () {
if (this.props.oncolor || this.props.offcolor) {
return this.status ? this.props.oncolor : this.props.offcolor
color () {
const oncolor = this.getProperty('oncolor')
const offcolor = this.getProperty('offcolor')

if (oncolor || offcolor) {
return this.status ? oncolor : offcolor
}
return null
},
value: function () {
value () {
return this.selection
},
status: {
Expand All @@ -72,28 +82,41 @@ export default {
msg.payload = val
this.selection = val
this.messages[this.id] = msg
if (this.decouple) {
if (this.getProperty('decouple')) {
this.loading = true
}
}
}
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, null)
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties)

// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
},
methods: {
onDynamicProperties (msg) {
const updates = msg.ui_update
if (!updates) {
return
}
this.updateDynamicProperty('label', updates.label)
this.updateDynamicProperty('decouple', updates.decouple)
this.updateDynamicProperty('oncolor', updates.oncolor)
this.updateDynamicProperty('offcolor', updates.offcolor)
this.updateDynamicProperty('onicon', updates.onicon)
this.updateDynamicProperty('officon', updates.officon)
},
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
// Update our vuex store with the value (in the payload) retrieved from Node-RED.
if (msg.payload !== undefined) {
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})

// make sure our v-model is updated to reflect the value from Node-RED
this.selection = msg.payload
this.loading = false
}
Expand All @@ -113,7 +136,7 @@ export default {
},
toggle () {
if (this.state.enabled) {
if (this.props.decouple) {
if (this.getProperty('decouple')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to move this to a computed property and refer it here as a readability improvement. But we can do that later

computed: {
    decouple () {
        return this.getProperty('decouple')
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we setting Passthru as a dynamic property #1181. It's not implemented other widgets at the moment

It wasn't a priority in others, switch is used in different ways where having this would be useful. Not goign to push it away if it's already been implemented!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback Joe. I will check this separately

this.loading = true
// send the inverse, but don't update the status until we get a response
this.$socket.emit('widget-change', this.id, !this.status)
Expand Down
Loading