[{"data":1,"prerenderedAt":293},["ShallowReactive",2],{"blog-\u002Fblog\u002F2023\u002F10\u002Fcustom-vuetify-components-dashboard":3},{"id":4,"title":5,"body":6,"description":279,"extension":280,"meta":281,"navigation":288,"path":289,"seo":290,"stem":291,"__hash__":292},"blog\u002Fblog\u002F2023\u002F10\u002Fcustom-vuetify-components-dashboard.md","Custom Vuetify components for Dashboard 2.0",{"type":7,"value":8,"toc":269},"minimark",[9,18,29,34,49,52,110,115,129,137,141,151,162,165,202,212,228,235,239,242,257,260,265],[10,11,12,13,17],"p",{},"Vuetify is a library of UI components using Vue. This saves the developers of\nDashboard 2.0 a lot of time, but it can also help you, the end-user. As Vuetify\nis now included, it can be used to include ",[14,15,16],"em",{},"any"," of their components. So in this\npost we're going to use a few of these to teach you how to use any of them.",[10,19,20,21,28],{},"Let's install the ",[22,23,27],"a",{"href":24,"rel":25},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fgetting-started.html",[26],"nofollow","Dashboard 2.0 package"," if you want to follow along. When that's done, let's figure\nout how to build custom components on dashboards.",[30,31,33],"h2",{"id":32},"custom-components","Custom components",[10,35,36,37,42,43,48],{},"While going through the list of components on ",[22,38,41],{"href":39,"rel":40},"https:\u002F\u002Fvuetifyjs.com\u002Fen\u002Fcomponents\u002F",[26],"Vuetify","\nthere's several examples that aren't natively implemented in Dashboard 2.0.\nOne example we'll use in a dashboard in this post is the\n",[22,44,47],{"href":45,"rel":46},"https:\u002F\u002Fvuetifyjs.com\u002Fen\u002Fcomponents\u002Fprogress-circular\u002F",[26],"Progress circular"," to\nbuild a count down timer.",[10,50,51],{},"The documentation explains which elements one can change, in this case the size and\nwidth. Having set those to the values you'd want in your dashboard, the HTML is\ngenerated for you, in my case it's:",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-html shiki shiki-themes github-light github-dark","\u003Cv-progress-circular model-value=\"20\" :size=\"128\" :width=\"12\">\u003C\u002Fv-progress-circular>\n","html","",[60,61,62],"code",{"__ignoreMap":58},[63,64,67,71,75,79,82,86,89,91,94,97,99,102,105,107],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sVt8B","\u003C",[63,72,74],{"class":73},"s9eBZ","v-progress-circular",[63,76,78],{"class":77},"sScJk"," model-value",[63,80,81],{"class":69},"=",[63,83,85],{"class":84},"sZZnC","\"20\"",[63,87,88],{"class":77}," :size",[63,90,81],{"class":69},[63,92,93],{"class":84},"\"128\"",[63,95,96],{"class":77}," :width",[63,98,81],{"class":69},[63,100,101],{"class":84},"\"12\"",[63,103,104],{"class":69},">\u003C\u002F",[63,106,74],{"class":73},[63,108,109],{"class":69},">\n",[111,112,114],"h3",{"id":113},"using-the-template-node","Using the template node",[10,116,117,118,122,123,128],{},"Like the ",[22,119,121],{"href":120},"\u002Fnode-red\u002Fcore-nodes\u002Ftemplate","template core node",", the dashboard package\ncomes with ",[22,124,127],{"href":125,"rel":126},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html",[26],"a template node of its own",".\nIf we take the HTML from the Vuetify docs pages and copy it in a template node\nthe spinner will show up on the dashboard.",[10,130,131],{},[132,133],"img",{"alt":134,"src":135,"title":136},"\"Custom widget on Dashboard 2.0\"","\u002Fblog\u002F2023\u002F10\u002Fimages\u002Fcustom-element-dashboard.png","Custom widget on Dashboard 2.0",[30,138,140],{"id":139},"dynamic-templates","Dynamic templates",[10,142,143,144,150],{},"While a custom element on a page is cool, and shows you can inject arbitrary HTML\non a Dashboard, it's even better if we could make the element dynamic. So let's\nstart with a first dynamic element. The quickest way to get that done is have\nan ",[22,145,147],{"href":146},"\u002Fnode-red\u002Fcore-nodes\u002Finject",[60,148,149],{},"Inject"," node output a random number every second.",[10,152,153,154,157,158,161],{},"So let's hook up an Inject, with ",[60,155,156],{},"msg.payload","'s output being a JSONata expression\n",[60,159,160],{},"$round($random() * 100)"," to generate a random number. And let's make sure it\nsends a message every second.",[10,163,164],{},"Then we need to update the template node to the following snippet:",[53,166,168],{"className":55,"code":167,"language":57,"meta":58,"style":58},"\u003Cv-progress-circular v-model=\"msg.payload\" :size=\"128\" :width=\"12\">\u003C\u002Fv-progress-circular>\n",[60,169,170],{"__ignoreMap":58},[63,171,172,174,176,179,181,184,186,188,190,192,194,196,198,200],{"class":65,"line":66},[63,173,70],{"class":69},[63,175,74],{"class":73},[63,177,178],{"class":77}," v-model",[63,180,81],{"class":69},[63,182,183],{"class":84},"\"msg.payload\"",[63,185,88],{"class":77},[63,187,81],{"class":69},[63,189,93],{"class":84},[63,191,96],{"class":77},[63,193,81],{"class":69},[63,195,101],{"class":84},[63,197,104],{"class":69},[63,199,74],{"class":73},[63,201,109],{"class":69},[10,203,204,205,208,209,211],{},"The difference is subtle, but important. Instead of hard-coding the ",[60,206,207],{},"model-value","\nto 20, the tag has changed name and it's set to ",[60,210,156],{},". The latter makes\nthe value dynamic.",[10,213,214,215,217,218,221,222,227],{},"Changing ",[60,216,207],{}," to ",[60,219,220],{},"v-model"," is due to leaking implementation details of\nDashboard 2.0. It uses VueJS to provide, among other features, easy updating of\ncomponents. If components are dynamic, ",[14,223,224,225],{},"always use ",[60,226,220],{},". This allows VueJS\nto pick up changes made dynamically.",[10,229,230],{},[132,231],{"alt":232,"src":233,"title":234},"\"Progress spinner, random values\"","\u002Fblog\u002F2023\u002F10\u002Fimages\u002Frandom-progress-element.gif","Progress spinner, random values",[111,236,238],{"id":237},"finishing-the-count-down-timer","Finishing the count down timer",[10,240,241],{},"This is mostly a programmers job, but it's not hard, so let's get to it. A button\nwould be great to reset the timer, and for the sake of this post we can hardcode\nthe deadline to 1m from the button press.",[10,243,244,245,249,250,253,254,256],{},"When dragging in a button node, connect it to a ",[22,246,248],{"href":247},"\u002Fnode-red\u002Fcore-nodes\u002Fchange","change","\nnode. In the change node set the flow variable ",[60,251,252],{},"flow.deadline"," to the timestamp. The\nInject node from earlier needs updating to inject the ",[60,255,252],{},". All that's\nleft is calculating how many seconds passed, and normalizing 60 seconds to the\nrange between 0-100.",[10,258,259],{},"The complete flow is:",[261,262],"render-flow",{":height":263,"flow":264},"200","W3siaWQiOiJjZTliYjhmNzRlM2ZjOTM0IiwidHlwZSI6InVpLXRlbXBsYXRlIiwieiI6IjI0MDY1YTBhYWRiMzA1ZTMiLCJncm91cCI6IjhmYTc3MmE3MDlhZTMzMTYiLCJkYXNoYm9hcmQiOiJlNWEzZjRjZGIxMWU1ZTNiIiwicGFnZSI6IjViZWRmN2Y0OWQ1YTYwMzciLCJuYW1lIjoiUHJvZ3Jlc3Mgc3Bpbm5lciIsIm9yZGVyIjowLCJ3aWR0aCI6MCwiaGVpZ2h0IjowLCJmb3JtYXQiOiI8di1wcm9ncmVzcy1jaXJjdWxhciB2LW1vZGVsPVwibXNnLnBheWxvYWRcIiA6c2l6ZT1cIjEyOFwiIDp3aWR0aD1cIjEyXCI+PC92LXByb2dyZXNzLWNpcmN1bGFyPlxuIiwic3RvcmVPdXRNZXNzYWdlcyI6dHJ1ZSwiZndkSW5NZXNzYWdlcyI6dHJ1ZSwicmVzZW5kT25SZWZyZXNoIjp0cnVlLCJ0ZW1wbGF0ZVNjb3BlIjoibG9jYWwiLCJjbGFzc05hbWUiOiIiLCJ4Ijo4MTAsInkiOjgwLCJ3aXJlcyI6W1tdXX0seyJpZCI6IjhmM2U2NjMxNDE0YWEwOTYiLCJ0eXBlIjoiaW5qZWN0IiwieiI6IjI0MDY1YTBhYWRiMzA1ZTMiLCJuYW1lIjoiSW5qZWN0IGRlYWRsaW5lIiwicHJvcHMiOlt7InAiOiJwYXlsb2FkIn1dLCJyZXBlYXQiOiIxIiwiY3JvbnRhYiI6IiIsIm9uY2UiOnRydWUsIm9uY2VEZWxheSI6MC4xLCJ0b3BpYyI6IiIsInBheWxvYWQiOiJkZWFkbGluZSIsInBheWxvYWRUeXBlIjoiZmxvdyIsIngiOjE0MCwieSI6ODAsIndpcmVzIjpbWyIyOTNjZDZmOWQ3MjdmYTAyIl1dfSx7ImlkIjoiYmQ5MDMyNzE5ZDI0YTUzZCIsInR5cGUiOiJ1aS1idXR0b24iLCJ6IjoiMjQwNjVhMGFhZGIzMDVlMyIsImdyb3VwIjoiOGZhNzcyYTcwOWFlMzMxNiIsIm5hbWUiOiIiLCJsYWJlbCI6IlJlc2V0Iiwib3JkZXIiOjAsIndpZHRoIjowLCJoZWlnaHQiOjAsInBhc3N0aHJ1IjpmYWxzZSwidG9vbHRpcCI6IiIsImNvbG9yIjoiIiwiYmdjb2xvciI6IiIsImNsYXNzTmFtZSI6IiIsImljb24iOiIiLCJwYXlsb2FkIjoiIiwicGF5bG9hZFR5cGUiOiJkYXRlIiwidG9waWMiOiJkZWFkbGluZSIsInRvcGljVHlwZSI6Im1zZyIsIngiOjE3MCwieSI6MTQwLCJ3aXJlcyI6W1siNjFlZjgzZDhiMDZmZjYyNiJdXX0seyJpZCI6IjYxZWY4M2Q4YjA2ZmY2MjYiLCJ0eXBlIjoiY2hhbmdlIiwieiI6IjI0MDY1YTBhYWRiMzA1ZTMiLCJuYW1lIjoiIiwicnVsZXMiOlt7InQiOiJzZXQiLCJwIjoiZGVhZGxpbmUiLCJwdCI6ImZsb3ciLCJ0byI6IiIsInRvdCI6ImRhdGUifV0sImFjdGlvbiI6IiIsInByb3BlcnR5IjoiIiwiZnJvbSI6IiIsInRvIjoiIiwicmVnIjpmYWxzZSwieCI6MzUwLCJ5IjoxNDAsIndpcmVzIjpbW11dfSx7ImlkIjoiMjkzY2Q2ZjlkNzI3ZmEwMiIsInR5cGUiOiJjaGFuZ2UiLCJ6IjoiMjQwNjVhMGFhZGIzMDVlMyIsIm5hbWUiOiJTZWNzIHNpbmNlIHJlc2V0IiwicnVsZXMiOlt7InQiOiJzZXQiLCJwIjoicGF5bG9hZCIsInB0IjoibXNnIiwidG8iOiIoJG1pbGxpcygpIC0gbXNnLnBheWxvYWQpLzEwMDAiLCJ0b3QiOiJqc29uYXRhIn1dLCJhY3Rpb24iOiIiLCJwcm9wZXJ0eSI6IiIsImZyb20iOiIiLCJ0byI6IiIsInJlZyI6ZmFsc2UsIngiOjM0MCwieSI6ODAsIndpcmVzIjpbWyI5NzQyZGE3ZTc0ZmQzY2QyIl1dfSx7ImlkIjoiOTc0MmRhN2U3NGZkM2NkMiIsInR5cGUiOiJyYW5nZSIsInoiOiIyNDA2NWEwYWFkYjMwNWUzIiwibWluaW4iOiIwIiwibWF4aW4iOiI2MCIsIm1pbm91dCI6IjAiLCJtYXhvdXQiOiIxMDAiLCJhY3Rpb24iOiJjbGFtcCIsInJvdW5kIjpmYWxzZSwicHJvcGVydHkiOiJwYXlsb2FkIiwibmFtZSI6IlNlY29uZHMgdG8gcGVyY2VudGFnZXMiLCJ4Ijo1NzAsInkiOjgwLCJ3aXJlcyI6W1siY2U5YmI4Zjc0ZTNmYzkzNCJdXX0seyJpZCI6IjhmYTc3MmE3MDlhZTMzMTYiLCJ0eXBlIjoidWktZ3JvdXAiLCJuYW1lIjoiR3JvdXAgTmFtZSIsInBhZ2UiOiI1YmVkZjdmNDlkNWE2MDM3Iiwid2lkdGgiOiI2IiwiaGVpZ2h0IjoiMSIsIm9yZGVyIjoiIiwiZGlzcCI6dHJ1ZX0seyJpZCI6ImU1YTNmNGNkYjExZTVlM2IiLCJ0eXBlIjoidWktYmFzZSIsIm5hbWUiOiJVSSBOYW1lIiwicGF0aCI6Ii9kYXNoYm9hcmQifSx7ImlkIjoiNWJlZGY3ZjQ5ZDVhNjAzNyIsInR5cGUiOiJ1aS1wYWdlIiwibmFtZSI6IlBhZ2UgTmFtZSIsInVpIjoiZTVhM2Y0Y2RiMTFlNWUzYiIsInBhdGgiOiIvIiwibGF5b3V0IjoiZ3JpZCIsInRoZW1lIjoiODI0MGZiZTdjMDliYzgxYyJ9LHsiaWQiOiI4MjQwZmJlN2MwOWJjODFjIiwidHlwZSI6InVpLXRoZW1lIiwibmFtZSI6IlRoZW1lIE5hbWUiLCJjb2xvcnMiOnsic3VyZmFjZSI6IiNmZmZmZmYiLCJwcmltYXJ5IjoiIzAwOTRjZSIsImJnUGFnZSI6IiNlZWVlZWUiLCJncm91cEJnIjoiI2ZmZmZmZiIsImdyb3VwT3V0bGluZSI6IiNjY2NjY2MifX1d",[266,267,268],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":58,"searchDepth":270,"depth":270,"links":271},2,[272,276],{"id":32,"depth":270,"text":33,"children":273},[274],{"id":113,"depth":275,"text":114},3,{"id":139,"depth":270,"text":140,"children":277},[278],{"id":237,"depth":275,"text":238},"Vuetify is a library of UI components using Vue. This saves the developers of\nDashboard 2.0 a lot of time, but it can also help you, the end-user. As Vuetify\nis now included, it can be used to include any of their components. So in this\npost we're going to use a few of these to teach you how to use any of them.","md",{"navTitle":5,"excerpt":282},{"type":7,"value":283},[284],[10,285,12,286,17],{},[14,287,16],{},true,"\u002Fblog\u002F2023\u002F10\u002Fcustom-vuetify-components-dashboard",{"title":5,"description":279},"blog\u002F2023\u002F10\u002Fcustom-vuetify-components-dashboard","0KyGqnlGP5ZIffmrMQmpIwxSqx3FmjCOLZygQFP0KK0",1780070551026]