# FlexDash Custom Widget tutorial

**URL:** https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974
**Category:** Share Your Nodes
**Tags:** flexdash
**Created:** [10 December 2022 07:53 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974 "2022-12-10T07:53:48Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![tve](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/tve/32/59475_2.png) [@tve](https://discourse.nodered.org/u/tve)
#### Post date: [10 December 2022 07:53 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/1 "2022-12-10T07:53:49Z")

</div>

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](https://flexdash.github.io/docs/developing-widgets/quick-start/) 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:

```auto
[{"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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/2/e/2ebc305c42e034b546bf3b698ea5b6fa3a79e448.png)

The function node has:

```auto
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:

```auto
<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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/e/e/ee26c934bdf0c191141b85c918c2d84e32329b81.png)

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:

```auto
 <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](https://vuejs.org/api/built-in-directives.html) 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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/7/77615836e8c976fab55d3e8626b6df9a75f4f764.png)

## 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):

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/f/5/f54a77c5969c4d5941ae0b6f53f53e4bfcf7ae3f.png)

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:

```auto
<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:

```auto
    <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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/1/8/1834d1755bfb4cfcd445ab6da1a99c8a1d390d9c.png)

## 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:

```auto
  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:

```auto
[{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:

```auto
      <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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/6/767c1c2442b684583a7d7b91a9c3b2e775c699cc.png)

## 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?

---

<div class="post-metadata">

### Author: ![Buckskin](https://avatars.discourse-cdn.com/v4/letter/b/b9e5f3/32.png) [@Buckskin](https://discourse.nodered.org/u/Buckskin)
#### Post date: [10 December 2022 16:18 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/2 "2022-12-10T16:18:34Z")

</div>

Simple things;

1. Use filter for the temperature conversion & addition of °C
2. Align the decimal places
3. Add boarders

Like the idea of icons and pop-up additional data

---

<div class="post-metadata">

### Author: ![tve](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/tve/32/59475_2.png) [@tve](https://discourse.nodered.org/u/tve)
#### Post date: [10 December 2022 18:01 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/3 "2022-12-10T18:01:05Z")

</div>

> [@Buckskin](#):
>
> - Use filter for the temperature conversion & addition of °C
> - Align the decimal places
> - Add boarders

What do you mean by "use filter"? Vue doesn't have a notation using "|" to pipe data structures through functions. I could have written the 'computed' stuff differently, which would have been more filter-style. Something like:

```auto
computed: {
  data(): {
    return this.payload.map(p => ({ ...p, temp: <calculate C>, color: <figure color> })
  }
}

```

Then I could have replaced the original loop with `v-for="d in data"` and not have to introduce `ix`.

Dealing with the alignment and borders is primarily a CSS thing. Let's do it... Start by going back to a simpler template loop and injecting some CSS classes:

```auto
    <tr v-for="d in data" key="d.city" :class="d.row_classes">
      <td>{{d.city}}</td>
      <td>{{d.time}}</td>
      <td>{{d.weather}}</td>
      <td :class="d.temp_classes">{{d.temp}}</td>
    </tr>

```

For colors. we'll use Vuetify's [material design color classes](https://next.vuetifyjs.com/en/styles/colors/) so we get consistent color shades throughout the dashboard (you will want to click through to the [actual definition of the colors](https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/styles/settings/_colors.scss)). Instead of borders we can alternate light and dark backgrounds using more Vuetify color classes with a `bg-` prefix.

Finally, for the right-alignment I opted to use flex layout in the td:

```auto
<style scoped>
  ...
  .right { display: flex; margin-right: 1em; justify-content: end; }
</style>

```

Now replace the `temp_c` and `temp_color` with a single "filter-like" transformation:

```auto
  computed: {
    data() {
      return this.payload.map((pl, ix) => {
        // color blue if < 0C/32F, red if >= 30C/86F, black otherwise
        const color =
          pl.temp < 32 ? "text-blue-darken-2" :
          pl.temp >= 86 ? "text-red-lighten-2" : "text-green"
        return {
          ...pl,
          temp: ((pl.temp-32)*5/9).toFixed(1) + "°C",
          temp_classes: [color, "right"],
          row_classes: ix&1 ? "bg-transparent" : "bg-grey-lighten-3",
        }
      })
    },
  },

```

And this produces:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/0/e/0ec12876faa7eaa66066d7b40c7ca344b347146b.png)

---

<div class="post-metadata">

### Author: ![Buckskin](https://avatars.discourse-cdn.com/v4/letter/b/b9e5f3/32.png) [@Buckskin](https://discourse.nodered.org/u/Buckskin)
#### Post date: [10 December 2022 18:31 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/4 "2022-12-10T18:31:27Z")

</div>

Filters;

The function would have been the same but I was looking at this

```auto
Vue.js allows you to define filters that can be used to apply common text formatting. Filters are usable in two places: mustache interpolations and v-bind expressions (the latter supported in 2.1.0+). Filters should be appended to the end of the JavaScript expression, denoted by the “pipe” symbol:

<!-- in mustaches -->
{{ message | capitalize }}

```

```auto
You can define local filters in a component’s options:

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

```

Apologies if this is not part of a widget, 'cause I have to confess that I do not actually have FlexDash loaded, but looking at your widgets I am seriously thinking about it. 🤔

---

<div class="post-metadata">

### Author: ![tve](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/tve/32/59475_2.png) [@tve](https://discourse.nodered.org/u/tve)
#### Post date: [10 December 2022 18:54 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/5 "2022-12-10T18:54:33Z")

</div>

Interesting! Learned something new! I see the [feature in Vue2](https://v2.vuejs.org/v2/guide/filters.html) but I'm not seeing it in Vue3... Ah: [removed](https://v3-migration.vuejs.org/breaking-changes/filters.html).

---

<div class="post-metadata">

### Author: ![Buckskin](https://avatars.discourse-cdn.com/v4/letter/b/b9e5f3/32.png) [@Buckskin](https://discourse.nodered.org/u/Buckskin)
#### Post date: [10 December 2022 23:51 UTC](https://discourse.nodered.org/t/flexdash-custom-widget-tutorial/71974/6 "2022-12-10T23:51:19Z")

</div>

So it has, sorry about that but I have only used Vue 2. Strange reason for removing them
