Skip to content

Commit 731294f

Browse files
authored
Merge pull request #83 from flowforge/ui-toggle
Widget: UI Toggle Switch
2 parents 392098a + 7600c3e commit 731294f

File tree

12 files changed

+470
-41
lines changed

12 files changed

+470
-41
lines changed

docs/.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default {
4646
{ text: 'ui-button', link: '/nodes/widgets/ui-button' },
4747
{ text: 'ui-dropdown', link: '/nodes/widgets/ui-dropdown' },
4848
{ text: 'ui-slider', link: '/nodes/widgets/ui-slider' },
49+
{ text: 'ui-switch', link: '/nodes/widgets/ui-switch' },
4950
{ text: 'ui-text', link: '/nodes/widgets/ui-text' },
5051
{ text: 'ui-text-input', link: '/nodes/widgets/ui-text-input' },
5152
{ text: 'ui-markdown', link: '/nodes/widgets/ui-markdown' },
51.3 KB
Loading

docs/nodes/widgets/ui-switch.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
props:
3+
Group: Defines which group of the UI Dashboard this widget will render in.
4+
Size: Controls the width of the button with respect to the parent group. Maximum value is the width of the group.
5+
Label: The text shown within the button.
6+
On Payload: The type & value to output in <code>msg.payload</code> when the switch is turned on.
7+
Off Payload: The type & value to output in <code>msg.payload</code> when the switch is turned off.
8+
On Icon: If provided, this <a href="https://pictogrammers.com/library/mdi/" target="_blank">Material Design icon</a> will replace the default switch when in "on" state
9+
Off Icon: If provided, this <a href="https://pictogrammers.com/library/mdi/" target="_blank">Material Design icon</a> will replace the default switch when in "off" state
10+
On Color: If provided with a icons, this colour is used for the icon when in "on" state
11+
Off Color: If provided with a icons, this colour is used for the icon when in "off" state
12+
---
13+
14+
<script setup>
15+
</script>
16+
17+
# Toggle Switch `ui-switch`
18+
19+
Adds a toggle switch to the user interface.
20+
21+
## Properties
22+
23+
<PropsTable/>
24+
25+
## Example
26+
27+
![Example of a Toggle Switch](../../assets/images/node-examples/ui-switch.png "Example of a Toggle Switch"){data-zoomable}
28+
*Example of rendered switches in a Dashboard.*

nodes/config/ui_base.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,10 @@ module.exports = function(RED) {
295295

296296
// Handle Socket IO Event Handlers
297297
if (widgetEvents?.onChange) {
298+
// have we configured a listener for this widget's change event?
298299
if (!ui.events.change[widget.id]) {
299300
ui.ioServer.on('connection', function(conn) {
300-
function handler (value) {
301+
function defaultHandler (value) {
301302
// ensure we have latest instance of the widget's node
302303
const wNode = RED.nodes.getNode(widgetNode.id);
303304

@@ -312,6 +313,10 @@ module.exports = function(RED) {
312313
wNode.send(msg)
313314
}
314315

316+
// Most of the time, we can just use this default handler,
317+
// but sometimes a node needs to do something specific (e.g. ui-switch)
318+
const handler = typeof(widgetEvents.onChange) === "function" ? widgetEvents.onChange : defaultHandler
319+
315320
// listen to in-UI events that Node-RED may need to action
316321
conn.on('widget-change:' + widget.id, handler)
317322

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script type="text/html" data-help-name="ui-switch">
2+
<p>
3+
Adds a toggle switch to the user interface.
4+
</p>
5+
<p>
6+
Each change in the state of the switch will generate
7+
a <code>msg.payload</code> with the specified <b>On</b> and <b>Off</b> values.
8+
</p>
9+
<p>
10+
The <b>On/Off Color</b> and <b>On/Off Icon</b> are optional fields. If the icons are present, the default
11+
toggle switch will be replaced with the relevant icons and their respective colors (if defined).
12+
</p>
13+
<p>
14+
The <b>On/Off Icon</b> field can be either a <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
15+
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
16+
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
17+
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.
18+
</p>
19+
<p>
20+
The switch state can be updated by an incoming <code>msg.payload</code>. The values must either be <code>true</code>/<code>false</code>
21+
or match the specified type (number, string, etc) in the node's configuration.</p>
22+
<!-- <p>
23+
The label can also be set by a message property by setting
24+
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
25+
<p>If a <b>Topic</b> is specified, it will be added to the output as <code>msg.topic</code>.</p> -->
26+
<!-- <p>Setting <code>msg.enabled</code> to <code>false</code> will disable the switch widget.</p> -->
27+
<!-- <p>
28+
If a <b>Class</b> is specified, it will be added to the parent card. This way you can style the card
29+
and the elements inside it with custom CSS. The Class can be set at runtime by setting a
30+
<code>msg.className</code> string property.
31+
</p> -->
32+
</script>

nodes/widgets/ui_switch.html

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<script type="text/javascript">
2+
RED.nodes.registerType('ui-switch', {
3+
category: RED._("@flowforge/node-red-dashboard/ui-base:ui-base.label.category"),
4+
color: 'rgb(176, 223, 227)',
5+
defaults: {
6+
name: {value: ''},
7+
label: {value: 'switch'},
8+
// tooltip: {value: ''},
9+
group: {type: 'ui-group', required: true},
10+
order: {value: 0},
11+
width: {value: 0, validate: function(v) {
12+
var width = v||0;
13+
var currentGroup = $('#node-input-group').val()||this.group;
14+
var groupNode = RED.nodes.node(currentGroup);
15+
var valid = !groupNode || +width <= +groupNode.width;
16+
$("#node-input-size").toggleClass("input-error",!valid);
17+
return valid;
18+
}
19+
},
20+
height: {value: 0},
21+
passthru: {value: true},
22+
topic: {value: 'topic', validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('topicType'):function(v) { return true})},
23+
topicType: {value: 'msg'},
24+
style: {value: ''},
25+
className: {value: ''},
26+
// on state
27+
onvalue: {value: true, validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('onvalueType'):function(v) { return true})},
28+
onvalueType: {value: 'bool'},
29+
onicon: {value: '' },
30+
oncolor: {value: ''},
31+
// off state
32+
offvalue: {value: false, validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('offvalueType'):function(v) { return true})},
33+
offvalueType: {value: 'bool'},
34+
officon: {value: ''},
35+
offcolor: {value: ''}
36+
},
37+
inputs:1,
38+
outputs:1,
39+
icon: "ui_switch.png",
40+
paletteLabel: 'switch',
41+
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'switch'; },
42+
labelStyle: function() { return this.name?"node_label_italic":""; },
43+
oneditprepare: function() {
44+
$("#node-input-size").elementSizer({
45+
width: "#node-input-width",
46+
height: "#node-input-height",
47+
group: "#node-input-group"
48+
});
49+
$('#node-input-custom-icons').on("change", function() {
50+
if ($('#node-input-custom-icons').val() === "default") {
51+
$(".form-row-custom-icons").hide();
52+
}
53+
else {
54+
$(".form-row-custom-icons").show();
55+
}
56+
});
57+
58+
if (this.onicon !== "" || this.oncolor !== "" || this.officon !=="" || this.offcolor !== "") {
59+
$('#node-input-custom-icons').val('custom');
60+
}
61+
else {
62+
$(".form-row-custom-icons").hide();
63+
$('#node-input-custom-icons').change();
64+
}
65+
66+
$('#node-input-onvalue').typedInput({
67+
default: 'str',
68+
typeField: $("#node-input-onvalueType"),
69+
types: ['str','num','bool','json','bin','date','flow','global']
70+
});
71+
72+
$('#node-input-offvalue').typedInput({
73+
default: 'str',
74+
typeField: $("#node-input-offvalueType"),
75+
types: ['str','num','bool','json','bin','date','flow','global']
76+
});
77+
78+
$('#node-input-topic').typedInput({
79+
default: 'str',
80+
typeField: $("#node-input-topicType"),
81+
types: ['str','msg','flow','global']
82+
});
83+
84+
$('#node-input-passthru').on("change", function() {
85+
if (this.checked) {
86+
$('.form-row-decouple').hide();
87+
$('#node-input-decouple').val("false");
88+
}
89+
else {
90+
$('.form-row-decouple').show();
91+
}
92+
});
93+
},
94+
oneditsave: function() {
95+
if ($('#node-input-custom-icons').val() === 'default') {
96+
$('#node-input-onicon').val('');
97+
$('#node-input-officon').val('');
98+
$('#node-input-oncolor').val('');
99+
$('#node-input-offcolor').val('');
100+
}
101+
}
102+
});
103+
</script>
104+
105+
<script type="text/html" data-template-name="ui-switch">
106+
<div class="form-row">
107+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
108+
<input type="text" id="node-input-name">
109+
</div>
110+
<div class="form-row">
111+
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
112+
<input type="text" id="node-input-group">
113+
</div>
114+
<div class="form-row">
115+
<label><i class="fa fa-object-group"></i> Size</label>
116+
<input type="hidden" id="node-input-width">
117+
<input type="hidden" id="node-input-height">
118+
<button class="editor-button" id="node-input-size"></button>
119+
</div>
120+
<div class="form-row">
121+
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
122+
<input type="text" id="node-input-label">
123+
</div>
124+
<!--<div class="form-row">
125+
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
126+
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
127+
</div>-->
128+
<div class="form-row">
129+
<label for="node-input-custom-icons"><i class="fa fa-picture-o"></i> Icon</label>
130+
<select id="node-input-custom-icons" style="width:35%">
131+
<option value="default">Default</option>
132+
<option value="custom">Custom</option>
133+
</select>
134+
</div>
135+
<div class="form-row form-row-custom-icons">
136+
<label for="node-input-onicon" style="text-align:right;"><i class="fa fa-toggle-on"></i> On Icon</label>
137+
<input type="text" id="node-input-onicon" style="width:120px">
138+
<label for="node-input-oncolor" style="width:50px; text-align:right;">Colour</label>
139+
<input type="text" id="node-input-oncolor" style="width:120px">
140+
</div>
141+
<div class="form-row form-row-custom-icons">
142+
<label for="node-input-officon" style="text-align:right;"><i class="fa fa-toggle-off"></i> Off Icon</label>
143+
<input type="text" id="node-input-officon" style="width:120px">
144+
<label for="node-input-offcolor" style="width:50px; text-align:right;">Colour</label>
145+
<input type="text" id="node-input-offcolor" style="width:120px">
146+
</div>
147+
<div class="form-row">
148+
<label style="width:auto" for="node-input-onvalue"><i class="fa fa-envelope-o"></i> When clicked, send:</label>
149+
</div>
150+
<div class="form-row">
151+
<label for="node-input-onvalue" style="padding-left:25px; margin-right:-25px">On Payload</label>
152+
<input type="text" id="node-input-onvalue" style="width:70%">
153+
<input type="hidden" id="node-input-onvalueType">
154+
</div>
155+
<div class="form-row">
156+
<label for="node-input-offvalue" style="padding-left:25px; margin-right:-25px">Off Payload</label>
157+
<input type="text" id="node-input-offvalue" style="width:70%">
158+
<input type="hidden" id="node-input-offvalueType">
159+
</div>
160+
<!--<div class="form-row">
161+
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
162+
<input type="text" id="node-input-topic">
163+
<input type="hidden" id="node-input-topicType">
164+
</div>
165+
<div class="form-row">
166+
<label for="node-input-className"><i class="fa fa-code"></i> Class</label>
167+
<input type="text" id="node-input-className" placeholder="Optional CSS class name(s) for widget"/>
168+
</div>-->
169+
</script>

nodes/widgets/ui_switch.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
module.exports = function(RED) {
2+
function SwitchNode(config) {
3+
// create node in Node-RED
4+
RED.nodes.createNode(this, config);
5+
6+
var node = this;
7+
node.status({});
8+
9+
const states = ['off', 'on']
10+
11+
// which group are we rendering this widget
12+
var group = RED.nodes.getNode(config.group);
13+
14+
const evts = {
15+
// runs on UI interaction
16+
onChange: function (value) {
17+
// ensure we have latest instance of the widget's node
18+
const wNode = RED.nodes.getNode(node.id);
19+
const msg = wNode._msg || {}
20+
21+
node.status({
22+
fill: value ? "green" : "red",
23+
shape: 'ring',
24+
text: value ? states[1] : states[0]
25+
})
26+
27+
// retrieve the assigned on/off value
28+
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, wNode)
29+
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, wNode)
30+
msg.payload = value ? on : off
31+
32+
wNode._msg = msg
33+
34+
// simulate Node-RED node receiving an input
35+
wNode.send(msg)
36+
},
37+
onInput: function (msg, send) {
38+
var error = null
39+
// ensure we have latest instance of the widget's node
40+
const wNode = RED.nodes.getNode(node.id);
41+
42+
// retrieve the assigned on/off value
43+
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, wNode)
44+
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, wNode)
45+
if (msg.payload === true || msg.payload === on) {
46+
msg.payload = on
47+
} else if (msg.payload === false || msg.payload === off) {
48+
msg.payload = off
49+
} else {
50+
// throw Node-RED error
51+
error = 'Invalid payload value'
52+
}
53+
if (!error) {
54+
node.status({
55+
fill: msg.payload ? "green" : "red",
56+
shape: 'ring',
57+
text: msg.payload ? states[1] : states[0]
58+
})
59+
60+
send(msg)
61+
} else {
62+
node.error(error)
63+
}
64+
}
65+
}
66+
67+
console.log(config.onvalue)
68+
console.log(config.onvalueType)
69+
console.log(config.offvalue)
70+
console.log(config.offvalueType)
71+
72+
const on = RED.util.evaluateNodeProperty(config.onvalue, config.onvalueType, node)
73+
const off = RED.util.evaluateNodeProperty(config.offvalue, config.offvalueType, node)
74+
75+
config.evaluated = {
76+
on: on,
77+
off: off
78+
}
79+
80+
// inform the dashboard UI that we are adding this node
81+
group.register(node, config, evts)
82+
}
83+
84+
RED.nodes.registerType("ui-switch", SwitchNode);
85+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"ui-button": "nodes/widgets/ui_button.js",
3636
"ui-dropdown": "nodes/widgets/ui_dropdown.js",
3737
"ui-slider": "nodes/widgets/ui_slider.js",
38+
"ui-switch": "nodes/widgets/ui_switch.js",
3839
"ui-text": "nodes/widgets/ui_text.js",
3940
"ui-chart": "nodes/widgets/ui_chart.js",
4041
"ui-markdown": "nodes/widgets/ui_markdown.js"

0 commit comments

Comments
 (0)