One of the most requested features since the earliest versions of FlexDash has been a ui_template
equivalent. I just released a first version of this and thought I'd cook up an example that represents some of the most common uses of ui_template, which I believe is to iterate through some data structure and display it.
I did write up a tutorial in the docs on how to clone one of the built-in FlexDash widgets into a custom widget so one can easily make tweaks to it. But that's a different use case from the typical ui_template.
Now, I'm not a creative user of ui_template, so I decided to write this up here in a more interactive manner: I'll write some of the tutorial and hope to get feedback on what to add next and then I'll go back and write the next part accordingly.
Sooo, here we go!
Unfortunately with a caveat: as I've been preparing this post I've been having occasional trouble with the hot-reload, i.e., when updating the widget instead of seeing the new result I get a pile of obscure errors in the browser console (for FlexDash). Looks like I need to figure out the dirty tricks on how one does do these on-the-fly code updates... The fix is to reload the browser window.
Lift-off
This tutorial assumes that you have FlexDash installed and that you have something like the hello world example going, i.e. you have a tab and a grid into which you can add a new custom widget.
What I did is to grab some boring weather data for a couple of cities, put that into a function node, and route it into a FD custom node. I then wrote a very simple template to display an HTML table with the data. Here are the 3 nodes that you can insert into a flow of yours:
[{"id":"a7c070cd4d145da9","type":"function","z":"451d9acba8e52c2f","name":"data","func":"let payload = [\n [\"Amsterdam\",\"Fri 7: 39 pm\",\"Ice fog.Chilly.\",32],\n [\"Oslo\",\"Fri 7: 39 pm\",\"Clear.Cold.\",12],\n [\"Athens\",\"Fri 8: 39 pm\",\"Mild.\",64],\n [\"Paris\",\"Fri 7: 39 pm\",\"Low clouds.Chilly.\",32],\n [\"Belgrade\",\"Fri 7: 39 pm\",\"Overcast.Cool.\",56],\n [\"Podgorica\",\"Fri 7: 39 pm\",\"Light rain.Mostly cloudy.Cool.\",57],\n [\"Berlin\",\"Fri 7: 39 pm\",\"Low clouds.Chilly.\",28],\n [\"Prague\",\"Fri 7: 39 pm\",\"Light snow.Ice fog.Chilly.\",30],\n]\npayload = payload.map(d => ({\n city: d[0], time: d[1], weather: d[2], temp: d[3]\n}))\n\nreturn { payload }","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":150,"y":640,"wires":[["5021eeaf697e64c2"]]},{"id":"22531105076fbf39","type":"inject","z":"451d9acba8e52c2f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":600,"wires":[["a7c070cd4d145da9"]]},{"id":"5021eeaf697e64c2","type":"flexdash custom","z":"451d9acba8e52c2f","fd_container":"28d1859702cdf729","fd_cols":"4","fd_rows":"3","fd_array":false,"fd_array_max":10,"fd_output_topic":"","fd_loopback":false,"name":"Weather table","sfc_source":"<template>\n <table>\n <tr>\n <th>City</th>\n <th>Time</th>\n <th>Weather</th>\n <th>Temperature</th>\n </tr>\n <tr v-for=\"d in payload\" key=\"d.city\">\n <td>{{d.city}}</td>\n <td>{{d.time}}</td>\n <td>{{d.weather}}</td>\n <td>{{d.temp}}°F</td>\n </tr>\n </table>\n</template>\n\n<style scoped>\n th { font-weight: 700; }\n td { padding: 0px 4px; }\n</style>\n\n<script scoped>\nexport default {\n props: {\n payload: { type: Array, default: [] },\n },\n}\n</script>\n","x":220,"y":680,"wires":[[]]},{"id":"28d1859702cdf729","type":"flexdash container","name":"misc","kind":"StdGrid","fd_children":",d2dbe97c44f6fb5a,d2b3883a74adc563,ab904c40d22f83e1,527401ee602f4386,4f31b9c704e0b947,a8dfa00ae270ac88,b27685878ffe3711,8713c375979525c8,5021eeaf697e64c2","title":"","tab":"9f23b0158f8280e3","min_cols":"1","max_cols":"20","parent":"","solid":false,"cols":"1","rows":"1"},{"id":"9f23b0158f8280e3","type":"flexdash tab","name":"chart","icon":"mdi-chart-line","title":"","fd_children":",28d1859702cdf729","fd":"e8f5aea52ab49500"}]
This should give you the following three nodes:
The function node has:
let payload = [
["Amsterdam","Fri 7: 39 pm","Ice fog.Chilly.",32],
["Oslo","Fri 7: 39 pm","Clear.Cold.",12],
["Athens","Fri 8: 39 pm","Mild.",64],
["Paris","Fri 7: 39 pm","Low clouds.Chilly.",32],
["Belgrade","Fri 7: 39 pm","Overcast.Cool.",56],
["Podgorica","Fri 7: 39 pm","Light rain.Mostly cloudy.Cool.",57],
["Berlin","Fri 7: 39 pm","Low clouds.Chilly.",28],
["Prague","Fri 7: 39 pm","Light snow.Ice fog.Chilly.",30],
]
payload = payload.map(d => ({
city: d[0], time: d[1], weather: d[2], temp: d[3]
}))
return { payload }
And the custom widget code to turn that payload into data is:
<template>
<table>
<tr>
<th>City</th>
<th>Time</th>
<th>Weather</th>
<th>Temperature</th>
</tr>
<tr v-for="d in payload" key="d.city">
<td>{{d.city}}</td>
<td>{{d.time}}</td>
<td>{{d.weather}}</td>
<td>{{d.temp}}°F</td>
</tr>
</table>
</template>
<style scoped>
th { font-weight: 700; }
td { padding: 0px 4px; }
</style>
<script scoped>
export default {
props: {
payload: { type: Array, default: [] },
},
}
</script>
And for completeness, the general tab of the custom widget has:
If you look at the <template>
section of the custom widget code you'll see pretty basic HTML to form a table. The only thing that is odd is the <tr>
that represents the data rows:
<tr v-for="d in payload" key="d.city">
Attributes of the from "v-xxx" are special Vue attributes. Vue calls them built-in directives and v-for
causes the element to which it is applied (and all its children) to be repeated. Similar to ng-repeat
in angular.
The d in payload
form iterates through the payload array, makes a copy of the <tr>
element and its children for each item, and assigns the item to d
, which can be used in the child elements.
From the code in the function node we can gather that each item in the payload array consists of city, time, weather, and temp, so we simply destructure each one into a <td>
by writing, e.g., <td>{{d.city}}</td>
. The familiar {{ ... }}
mustache notation is replaced by the value of the javascript expression contained within.
The custom widget code further contains a <style>
element, which has CSS to be applied to the widget. The scoped
attribute causes the CSS to be scoped just to the elements in this widget. I.e., the CSS that makes <th>
content bold-face is only applied to this table here, not to <th>
elements elsewhere on the page.
Finally, the <script>
element contains the javascript code for this widget, or more properly stated, it contains an "options object" in the style of Vue's "options API". Here, this is really minimal and only contains the definition of the "props", which are the properties that can be input to the widget. We need to get the payload with the data into the widget somehow!
Hitting "deploy" should display the Weather Table in FlexDash:
Simple calculations
Let's make one change: it's kind'a perverse to display the temperature for European cities in Farenheit, we can fix that! The quick & dirty fix is to change the expression in the <td>
to something like <td>{{(d.temp-32)*5/9}}°C</td>
and that works, but the result isn't great (you can simply edit the code, hit done&deploy, and the dashboard updates):
In general it is frowned upon to put much more than a variable reference into the template because pretty quick it becomes an unreadable jumble of HTML and javascript. Better to separate HTML and code.
What we need is an array of temperatures that parallels the array of city data. So for cities 1..N we have temperatures 1..N in Centigrade/Celsius. Let's expand the script:
<script scoped>
export default {
props: {
payload: { type: Array, default: [] },
},
computed: {
temp_c() {
return this.payload.map(pl => ((pl.temp-32)*5/9).toFixed(1))
},
},
}
</script>
The new computed
section contains functions to define reactive variables. In this case, the temp_c
function accesses this.payload
and returns an array (of temperatures in centigrade printed with one decimal). The effect of this function in the "computed" section causes a variable this.temp_c
to be set to the returned array.
In addition, Vue takes note of what is accessed by the function (this.payload
in this case) and every time this.payload changes it will re-run the function to recalculate this.temp_c. This is the most fundamental feature that makes Vue a pleasure to use: one doesn't have to sprinkle trigger calls everywhere to update other stuff, it happens automatically.
Now all that's left is to use the temp_c
array in the template:
<tr v-for="(d,ix) in payload" key="d.city">
<td>{{d.city}}</td>
<td>{{d.time}}</td>
<td>{{d.weather}}</td>
<td>{{temp_c[ix]}}°C</td>
</tr>
The result:
Computed styles
Something else we can do is to color the temperatures in blue below 0 degrees. For that we want to compute a CSS style and apply it to the appropriate <td>
. Let's compute that style:
computed: {
temp_c() {
return this.payload.map(pl => ((pl.temp-32)*5/9).toFixed(1))
},
temp_color() {
return this.payload.map(pl => {
// color blue if < 0C/32F, red if >= 30C/86F, black otherwise
const color = pl.temp < 32 ? "blue" : pl.temp >= 86 ? "red" : "black"
return { color }
})
},
},
This produces this.temp_color
with something like:
[ {color: "blue"}, {color: "black"}, ...]
with the intent that each one of those objects is a set of CSS styles, one for each city.
Now we need to apply it. For this we can use a style
attribute on the td
element:
<td :style="temp_color[ix]">{{temp_c[ix]}}°C</td>
The :
in front of the style attribute is important: it tells Vue that temp_color[ix]
is not a literal value but a javascript expression. In our case that expression yields objects like {color: "blue"}
. The result looks as follows:
Next?
Maybe next we could map the weather description text (e.g. "Ice fog. Chilly.") to some icons? And then make the icons clickable and pop-up some additional info on click? Other suggestions?