Skip to content

Commit 27fae60

Browse files
authored
Merge pull request #420 from FlowFuse/fix-script-injection
UI Template - Fix external script injection & update ui-template docs with more examples
2 parents 2a7d710 + 5605eab commit 27fae60

File tree

3 files changed

+203
-116
lines changed

3 files changed

+203
-116
lines changed

docs/nodes/widgets/ui-template.md

Lines changed: 137 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -23,81 +23,128 @@ Provide custom JS and HTML (including any [Vuetify components](https://vuetifyjs
2323

2424
<PropsTable/>
2525

26-
## Built In Functionality
26+
## Writing Custom Widgets
2727

28-
Any Vue Components that you define in `ui-template` extend the underlying `ui-template` Vue component. This includes a collection of builtin methods, data and supported Widgets. You can also render dynamic content using any VueJS data-binding expressions, (e.g. `v-if`, `v-for`).
28+
UI Template will parse different tags and render them into Dashboard. The available tags are:
2929

30-
### Vuetify Widgets
30+
- `<template>` - Any HTML code in here will be rendered into the Dashboard.
31+
- `<script>` - Any JavaScript code in here will be executed when the widget is loaded. You can also [define a full VueJS component](#building-full-vue-components) here.
32+
- `<style>` - Any CSS code in here will be injected into the Dashboard.
3133

32-
The `ui-template` node also has access to the [Vuetify component library](https://vuetifyjs.com/en/components/all/) by default. The library provides a large number of pre-built widgets that you can use in your Dashboard.
34+
### Built-in Variables
3335

34-
These are particularly useful as they provide easy access to a large number of pre-built widgets that aren't necessarily included in the core nodes of Node-RED Dashboard 2.0.
36+
You have access to a number of built-in variables in your `ui-template` node:
3537

36-
Some example widgets that you may find useful are:
38+
- `this.id` - The ID of the `ui-template` node, assigned by Node-RED.
39+
- `this.$socket` - The socket.io connection that is used to communicate with the Node-RED backend.
40+
- `this.msg` - The last message received by the `ui-template` node.
3741

38-
- [File Input](https://vuetifyjs.com/en/components/file-inputs/) - Allows for the selection of a file from the user's local filesystem.
39-
- [Star Rating Widget](https://vuetifyjs.com/en/components/ratings/) - A star rating widget, where users can select a rating from 1-n stars.
40-
- [Progress Linear](https://vuetifyjs.com/en/components/progress-linear/) - A horizontal bar to show progress of a task or single bar-graph type visualisations.
42+
### Built-in Functions
4143

42-
### Reading Node Input
44+
We also offer some helper functions for the Node-RED integration too:
4345

44-
Whenever a `ui-template` receives a `msg` in Node-RED, that is automatically assigned to the `msg` variable in the template.
46+
- `this.send` - Send a message to the Node-RED flow.
47+
- `this.$socket.on('msg-input' + this.id, (msg) = { ... })` - will listen to any messages received by your `ui-template `node and react accordingly.
4548

46-
Additionally, we also bind a computed variable `value` to `msg.payload`. This means that you can access the `msg` object in your template, as well as binding the `value` variable to any widgets you use.
49+
### Example (Raw JavaScript)
4750

48-
Such an example would be:
51+
Putting this together, here is a simple starter widget that will alert the user when they click the button, and send a message into Node-RED.
4952

50-
```html
51-
<div>
52-
<h2>Latest <code>msg</code> received:</h2>
53-
<pre>{{ msg }}</pre>
54-
</div>
55-
```
53+
```vue
54+
<template>
55+
<!-- Any HTML can go here -->
56+
<button class="my-class" onclick="onClick()">My Button</button>
57+
</template>
5658
57-
![Example of UI Template displaying the last received message](/images/node-examples/ui-template-lastmsg.png "Example of UI Template displaying the last received message"){data-zoomable}
59+
<script>
60+
/* Write any JavaScript here */
61+
// add our onClick function to the window object to make it accessible by the HTML <button>
62+
window.onClick = function () {
63+
alert('Button has been clicked')
64+
}
5865
59-
### Sending Messages in Node-RED
66+
// Use send() function to pass on data back into Node-RED:
67+
this.send('Component has loaded')
6068
61-
Two exposed methods, `send` and `submit`, allow you to send messages from the Dashboard to the Node-RED flow.
69+
// Subscribe to the incoming msg's
70+
this.$socket.on('msg-input:' + this.id, function(msg) {
71+
// do stuff with the message
72+
alert('message received: ' + msg.payload)
73+
})
74+
</script>
6275
63-
- `send` - Outputs a message (defined by the input to this function call) from this node in the Node-RED flow.
64-
- `submit` - Send a `FormData` object when attached to a `<form>` element. The created object will consnist of the `name` attributes for each form element, corresponding to their respective `value` attributes.
76+
<style>
77+
/* define any styles here - supports raw CSS */
78+
.my-class {
79+
color: red;
80+
}
81+
</style>
82+
```
6583

66-
#### Sending on Click
67-
Here, we call it when someone clicks our "Send Hello World" button:
84+
### Loading External Dependencies
6885

69-
```vue
70-
<v-btn @click="send({payload: 'Hello World'})">Send Hello World</v-btn>
86+
It is possible to load external dependencies into your `ui-template` node. This is useful if you want to use a library that isn't included in the core Node-RED Dashboard 2.0 nodes.
87+
88+
To do this, you'll need to load the dependency in the `<script>` section of your template. For example, to load the [Babylon.js](https://www.babylonjs.com/) library, you would do the following:
89+
90+
```html
91+
<script src="https://cdn.babylonjs.com/babylon.js"></script>
7192
```
7293

73-
#### Sending on Change
74-
Or another example, where the payload is automatically sent any time the `v-model` is changed:
94+
You can then have _another_ `<script />` tag in the same `ui-template` that utilises this library.
7595

76-
![Example of UI Template using Vuetify's Rating Widget](/images/node-examples/ui-template-rating1.png "Example of UI Template using Vuetify's Rating Widget"){data-zoomable}
96+
An important caveat here is that, whilst this does get injected into the `<head />` of the Dashboard, because our widgets are loaded after the initial page load, the library isn't always available straight away as your Widget and HTML loads.
97+
98+
If you need access to the library as soon as it's available, the trick to this is to run a `setInterval()` and watch the `window` object for the library to be loaded.
99+
100+
For example:
77101

78102
```vue
79-
<v-rating hover :length="5" :size="32" v-model="value"
80-
active-color="primary" @update:modelValue="send({payload: value})"/>
81-
```
103+
<template>
104+
<!-- Template Content Here -->
105+
</template>
82106
83-
`v-model` in Vue is a way of two-way binding a variable to a widget. Here, we bind the `value` variable to the `v-rating` widget. Then we watch for changes on that value with `@update:modelValue` and send the `value` variable to the Node-RED flow via `msg.payload`.
107+
<script src="https://cdn.babylonjs.com/babylon.js"></script>
84108
85-
When changed, if wired to a "Debug" node, then we can see the resulting outcome is as follows:
109+
<script>
110+
function init () {
111+
alert('Babylon.js is loaded')
112+
}
86113
87-
![Example output from using Vuetify's Rating Widget](/images/node-examples/ui-template-rating2.png "Example output from using Vuetify's Rating Widget"){data-zoomable}
114+
// run this code when the widget is built
115+
let interval = setInterval(() => {
116+
if (window.BABYLON) {
117+
// call an init() to use BABYLON
118+
init();
119+
// Babylon.js is loaded, so we can now use it
120+
clearInterval(interval);
121+
}
122+
}, 100);
123+
</script>
124+
```
88125

89126
## Building Full Vue Components
90127

91-
You can also build full Vue components in the `ui-template` node. This allows you to build your own bespoke behaviour, and gives you more control over the UI.
128+
You can build full Vue components in the `ui-template` node, using VueJS's [Options API](https://vuejs.org/api/#options-api). This allows you to build your own bespoke behaviour, and gives you more control over the UI.
129+
130+
A full list of the VueJS Options API properties that we currently support are:
131+
132+
- `name` - The name of your component
133+
- `data` - A function that returns data you want available across your component (in both `<template>` and `<script>` sections)
134+
- `watch` - Run a function anytime a particular component variable changes
135+
- `computed` - Calculate a variable based on other variables in your component
136+
- `methods` - Define functions that can be called from your `<template>` or `<script>` sections
137+
- `mounted` - Run code when the component is first loaded
138+
- `unmounted` - Run code when the component is removed from the Dashboard
92139

93-
To do this in `ui-template` you need to define a `<template>` and `<script>` section in your template.
140+
### Example (Full Vue Component)
94141

95-
For example, here we define a counter widget:
142+
Here we define a counter widget, and utilise Vue's `data`, `watch`, `computed` and `methods` properties. This widget will automatically update the `formattedCount` variable whenever the `count` variable changes and will send a message to Node-RED whenever the `count` variable reaches a multiple of 5.
96143

97144
```vue
98145
<template>
99146
<div>
100-
<h2>Counter</h2>
147+
<h2>Counter - loaded: {{ loaded }}</h2>
101148
<p>Current Count: {{ count }}</p>
102149
<p class="my-class">Formatted Count: {{ formattedCount }}</p>
103150
<v-btn @click="increase()">Increment</v-btn>
@@ -110,12 +157,13 @@ For example, here we define a counter widget:
110157
// define variables available component-wide
111158
// (in <template> and component functions)
112159
return {
160+
loaded: false,
113161
count: 0
114162
}
115163
},
116164
watch: {
117165
// watch for any changes of "count"
118-
count() {
166+
count: function () {
119167
if (this.count % 5 === 0) {
120168
this.send({payload: 'Multiple of 5'})
121169
}
@@ -124,15 +172,23 @@ For example, here we define a counter widget:
124172
computed: {
125173
// automatically compute this variable
126174
// whenever VueJS deems appropriate
127-
formattedCount() {
175+
formattedCount: function () {
128176
return `${this.count} Apples`
129177
}
130178
},
131179
methods: {
132180
// expose a method to our <template> and Vue Application
133-
increase() {
181+
increase: function () {
134182
this.count++
135183
}
184+
},
185+
mounted() {
186+
// code here when the component is first loaded
187+
this.loaded = true
188+
},
189+
unmounted() {
190+
// code here when the component is removed from the Dashboard
191+
// i.e. when the user navigates away from the page
136192
}
137193
}
138194
</script>
@@ -150,99 +206,67 @@ We can also `watch` any of these `data` variables, and react accordingly. For ex
150206

151207
We use a `computed` variable which will automatically update whenever the `count` variable changes. This allows us to format the `count` variable in a way that is more useful to us to display, without affecting the underlying `count` variable.
152208

153-
### Running code on load
209+
## Additional Examples
154210

155-
In VueJS, you can run code when a component is loaded by using the `mounted()` function. This is called when the component is first loaded into the Dashboard:
211+
Any Vue Components that you define in `ui-template` extend the underlying `ui-template` Vue component. This includes a collection of builtin methods, data and supported Widgets. You can also render dynamic content using any VueJS data-binding expressions, (e.g. `v-if`, `v-for`).
156212

157-
```vue
213+
### Reading Node-RED Input
214+
215+
Whenever a `ui-template` receives a `msg` in Node-RED, that is automatically assigned to the `msg` variable in the template. Such an example would be:
216+
217+
```html
158218
<template>
159-
<div style="display: flex; gap: 8px;">
160-
<label>Has Loaded:</label>
161-
<code>{{ loaded }}</code>
219+
<div>
220+
<h2>Latest <code>msg</code> received:</h2>
221+
<pre>{{ msg }}</pre>
162222
</div>
163223
</template>
164-
165-
<script>
166-
export default {
167-
data() {
168-
return {
169-
loaded: false
170-
}
171-
},
172-
mounted() {
173-
this.loaded = true
174-
}
175-
}
176-
</script>
177224
```
178225

179-
If you want to do the inverse, run code when your widget is removed from the screen, you can use the `unmounted()` function.
226+
![Example of UI Template displaying the last received message](/images/node-examples/ui-template-lastmsg.png "Example of UI Template displaying the last received message"){data-zoomable}
180227

181-
### VueJS Options API
228+
### Sending Messages to Node-RED
182229

183-
We are utilising VueJS's [Options API](https://vuejs.org/api/#options-api) here as it's far easier to merge with the `ui-template` component. Unfortunately, at the moment, we do not support the [Composition API](https://vuejs.org/api/#composition-api).
230+
Two exposed methods, `send` and `submit`, allow you to send messages from the Dashboard to the Node-RED flow.
184231

185-
A full list of the VueJS Options API properties that we currently support are:
232+
- `send` - Outputs a message (defined by the input to this function call) from this node in the Node-RED flow.
233+
- `submit` - Send a `FormData` object when attached to a `<form>` element. The created object will consnist of the `name` attributes for each form element, corresponding to their respective `value` attributes.
186234

187-
- `name`
188-
- `data`
189-
- `watch`
190-
- `computed`
191-
- `methods`
192-
- `mounted`
193-
- `unmounted`
235+
#### Sending on Click
236+
Here, we call it when someone clicks our "Send Hello World" button:
194237

195-
## Loading External Dependencies
238+
```vue
239+
<v-btn @click="send({payload: 'Hello World'})">Send Hello World</v-btn>
240+
```
196241

197-
It is possible to load external dependencies into your `ui-template` node. This is useful if you want to use a library that isn't included in the core Node-RED Dashboard 2.0 nodes.
242+
#### Sending on Change
243+
Or another example, where the payload is automatically sent any time the `v-model` is changed:
198244

199-
To do this, you'll need to load the dependency in the `<script>` section of your template. For example, to load the [Babylon.js](https://www.babylonjs.com/) library, you would do the following:
245+
![Example of UI Template using Vuetify's Rating Widget](/images/node-examples/ui-template-rating1.png "Example of UI Template using Vuetify's Rating Widget"){data-zoomable}
200246

201-
```html
202-
<script src="https://cdn.babylonjs.com/babylon.js"></script>
247+
```vue
248+
<v-rating hover :length="5" :size="32" v-model="value"
249+
active-color="primary" @update:modelValue="send({payload: value})"/>
203250
```
204251

205-
You can then have _another_ `<script />` tag in the same `ui-template` that utilises this library.
252+
`v-model` in Vue is a way of two-way binding a variable to a widget. Here, we bind the `value` variable to the `v-rating` widget. Then we watch for changes on that value with `@update:modelValue` and send the `value` variable to the Node-RED flow via `msg.payload`.
206253

207-
An important caveat here is that, whilst this does get injected into the `<head />` of the Dashboard, because our widgets are loaded after the initial page load, the library isn't always available straight away as your Vue component and HTML loads.
254+
When changed, if wired to a "Debug" node, then we can see the resulting outcome is as follows:
208255

209-
If you need access to the library as soon as it's available, the trick to this is to run a `setInterval()` and watch the `window` object for the library to be loaded.
256+
![Example output from using Vuetify's Rating Widget](/images/node-examples/ui-template-rating2.png "Example output from using Vuetify's Rating Widget"){data-zoomable}
210257

211-
For example:
258+
### Vuetify Widgets
212259

213-
```vue
214-
<template>
215-
<!-- Template Content Here -->
216-
</template>
217-
<script src="https://cdn.babylonjs.com/babylon.js"></script>
218-
<script>
219-
export default {
220-
name: "MyComponent",
221-
mounted () {
222-
// run this code when the widget is built
223-
let interval = setInterval(() => {
224-
if (window.BABYLON) {
225-
// Babylon.js is loaded, so we can now use it
226-
clearInterval(interval);
227-
// call an init() function defined in this component
228-
this.init();
229-
}
230-
}, 100);
231-
},
232-
methods: {
233-
init () {
234-
// do stuff with BABYLON here
235-
}
236-
}
237-
}
238-
</script>
239-
```
260+
The `ui-template` node also has access to the [Vuetify component library](https://vuetifyjs.com/en/components/all/) by default. The library provides a large number of pre-built widgets that you can use in your Dashboard.
240261

241-
## Writing Raw JavaScript
262+
These are particularly useful as they provide easy access to a large number of pre-built widgets that aren't necessarily included in the core nodes of Node-RED Dashboard 2.0.
242263

243-
You are not limited to having to use a Vue components, as you can also write raw JavaScript in the `ui-template` node. This code will then run as the widget is loaded into Dashboard.
264+
Some example widgets that you may find useful are:
244265

266+
- [File Input](https://vuetifyjs.com/en/components/file-inputs/) - Allows for the selection of a file from the user's local filesystem.
267+
- [Star Rating Widget](https://vuetifyjs.com/en/components/ratings/) - A star rating widget, where users can select a rating from 1-n stars.
268+
- [Progress Linear](https://vuetifyjs.com/en/components/progress-linear/) - A horizontal bar to show progress of a task or single bar-graph type visualisations.
245269

246-
## Examples
270+
### Articles & Tutorials
247271

248272
- [Building a Custom Video Player with UI Template](https://flowfuse.com/blog/2023/12/dashboard-0-10-0/)

0 commit comments

Comments
 (0)