Adding New Core Widgets
A single widget consist of two key parts:
- A Node-RED node that will appear in the palette of the Node-RED Editor
.vue
and client-side code that renders the widget into a dashboard
You can explore our collection of core widgets here.
We are always open to Pull Requests and new ideas on widgets that can be added to the core Dashboard repository.
When adding a new widget to the core collection, you will need to follow the steps below to ensure that the widget is available in the Node-RED editor and renders correctly in the UI.
Checklist
When adding a new widget to Dashboard 2.0, you'll need to ensure that the following steps have been followed for that new widget to be recognised and included in a Dashboard 2.0 build:
- In
/nodes/
:- Add
<widget>.html
- Add
<widget>.js
- Add the in the
node-red/nodes
section inpackage.json
- Add
- In
/ui/
:- Add
widgets/<widget>/<widget>.vue
- Add widget to the
index.js
file in/ui/widgets
- Add
Example <widget.vue>
<template>
<div @click="onAction">
{{ id }}
</div>
</template>
<script>
import { useDataTracker } from '../data-tracker.js'
import { mapState } from 'vuex'
export default {
name: 'DBUIWidget',
// we need to inject $socket so that we can send events to Node-RED
inject: ['$socket'],
props: {
id: String, // the id of the widget, as defined by Node-RED
props: Object, // the properties for this widget defined in the Node-RED editor
state: Object // the state of this widget, e.g. enabled, visible
},
computed: {
// map our data store such that we can get any data bound to this widget
// received on input from Node-RED
...mapState('data', ['messages']), // provides access to `this.messages` where `this.messages[this.id]` is the stored msg for this widget
},
setup (props) {
// Use our data-tracker, which setups up the basic event handling for us
// including `on('msg-input')` and `on('widget-load')`
useDataTracker(props.id)
},
methods: {
onAction () {
// we can send any data we need Node-RED through this (optional) message parameter
const msg = {
payload: 'hello world'
}
// send an event to Node-RED to inform it that we've clicked this widget
this.$socket.emit('widget-action', this.id, msg)
}
}
}
</script>
<style scoped>
</style>
Data Tracker
The data tracker is a set of utility functions that help setup the standard event handlers for a core widget. It will setup the following events:
on('widget-load')
- to handle any initial data that is sent to the widget when it is loadedon('msg-input')
- to handle any incoming data from Node-RED
It also provides flexibility to define custom event handlers for the widget if there is bespoke functionality required for a given node, for example in a ui-chart
node, we have a collection of custom logic that handles the merging of data points and the rendering of the chart when a message is received.
The inputs for the useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
function are used as follows:
widgetId
- the unique ID of the widgetonInput
- a function that will be called when a message is received from Node-RED through theon(msg-input)
socket handleronLoad
- a function that will be called when the widget is loaded, and triggered by thewidget-load
eventonDynamicProperties
- a function called as part of theon(msg-input)
event, and is triggered before the defaultonInput
function. This is a good entry point to check against any properties that have been included in themsg
in order to set a dynamic property.
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.
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.
Setting Dynamic Properties
Server-Side
In order to set a dynamic property in the server-side state
store we can utilise the beforeSend
event on the node. This event is triggered on any occasion that the server-side node is about to send a message to the client, including when a new input is received into a given node.
For this, we make the most of the state store's set
function:
/**
*
* @param {*} base - associated ui-base node
* @param {*} node - the Node-RED node object we're storing state for
* @param {*} msg - the full received msg (allows us to check for credentials/socketid constraints)
* @param {*} prop - the property we are setting on the node
* @param {*} value - the value we are setting
*/
set (base, node, msg, prop, value) {
if (canSaveInStore(base, node, msg)) {
if (!state[node.id]) {
state[node.id] = {}
}
state[node.id][prop] = value
}
},
For example, in ui-dropdown
:
const evts = {
onChange: true,
beforeSend: function (msg) {
if (msg.options) {
// dynamically set "options" property
statestore.set(group.getBase(), node, msg, 'options', msg.options)
}
return msg
}
}
// inform the dashboard UI that we are adding this node
group.register(node, config, evts)
Client Side
Now that we have the server-side state updating, anytime we refresh, the full ui-config
will already contain the dynamic properties.
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.
Updating Documentation
There are two important places to ensure documentation is updated when adding dynamic properties:
Online Documentation:
Each node will have a corresponding /docs/nodes/widgets/<node>.md
file which allows for the definition of dynamic` table in the frontmatter, e.g:
dynamic:
Options:
payload: msg.options
structure: ["Array<String>", "Array<{value: String}>", "Array<{value: String, label: String}>"]
Class:
payload: msg.class
structure: ["String"]
You can then render this table into the documentation with:
## Dynamic Properties
<DynamicPropsTable/>
Editor Documentation:
Each node will have a corresponding /locales/<locale>/<node>.html
file which should include a table of dynamic properties, e.g:
<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">options <span class="property-type">array</span></dt>
<dd>
Change the options available in the dropdown at runtime
<ul>
<li><code>Array<string></code></li>
<li><code>Array<{value: String}></code></li>
<li><code>Array<{value: String, label: String}></code></li>
</ul>
</dd>
<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>
Debugging Dynamic Properties
Dashboard 2.0 comes with as Debug View that includes a specialist panel to monitor any dynamic properties assigned to a widget. This can be a very useful tool when checking whether the client is aware of any dynamic properties that have been sent.