[{"data":1,"prerenderedAt":1632},["ShallowReactive",2],{"blog-\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0":3,"blog-all-for-related":1631},{"id":4,"title":5,"authors":6,"body":8,"cta":1593,"date":1594,"description":1595,"extension":1596,"image":1597,"lastUpdated":1593,"meta":1598,"navigation":146,"path":1619,"seo":1620,"sitemap":1621,"stem":1622,"subtitle":1623,"tags":1624,"tldr":1593,"video":1593,"__hash__":1630},"blog\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0.md","Building a Custom Video Player in Dashboard 2.0",[7],"joe-pavitt",{"type":9,"value":10,"toc":1579},"minimark",[11,25,44,47,50,62,80,85,88,99,304,307,363,368,374,415,422,426,430,433,567,570,614,621,628,632,635,640,661,927,937,943,947,971,1293,1297,1304,1510,1513,1516,1532,1536,1543,1552,1560,1576],[12,13,14,15,19,20,24],"p",{},"Dashboard 2.0 just got ",[16,17,18],"em",{},"a lot"," more powerful with our new updates to the ",[21,22,23],"code",{},"ui-template"," node. New features added to the node include:",[26,27,28,32,39],"ul",{},[29,30,31],"li",{},"Support for a full Vue component  to be defined using the VueJS Options API.",[29,33,34,35,38],{},"Running of raw JavaScript within ",[21,36,37],{},"\u003Cscript \u002F>"," tags",[29,40,41,42,38],{},"Loading of external dependencies through ",[21,43,37],{},[12,45,46],{},"In this article we're going to deepdive into an example of how you can use this new functionality to build a custom video player.",[12,48,49],{},"We're going to aim for 3 key features:",[51,52,53,56,59],"ol",{},[29,54,55],{},"Emit events into Node-RED when a user plays\u002Fpauses the video",[29,57,58],{},"Allow for the video to be played\u002Fpaused from within Node-RED",[29,60,61],{},"Allow the user to seek to a specific point in the video from within Node-RED",[63,64,66,67,71,72,75,76,79],"div",{"style":65},"background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;","Reminder: all new releases of Dashboard are now under the ",[21,68,70],{"style":69},"background-color: transparent;","@flowfuse"," namespace, so you'll need to update to use ",[21,73,74],{"style":69},"@flowfuse\u002Fnode-red-dashboard",", and not ",[21,77,78],{"style":69},"@flowforge",".",[81,82,84],"h2",{"id":83},"building-a-vue-component","Building a Vue Component",[12,86,87],{},"With Dashboard 2.0, we switched over our underlying front-end framework to VueJS. We're aware that not everyone coming into Dashboard 2.0 will be familiar with VueJS.",[12,89,90,91,98],{},"We have a more detailed guide ",[92,93,97],"a",{"href":94,"rel":95},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#building-full-vue-components",[96],"nofollow","here",", but we'll also give a quick overview of the elements from Vue \"component\" that we'll use here:",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Ctemplate>\n    \u003C!-- Our HTML content will go here -->\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  name: 'MyComponent',\n  methods: {\n    \u002F\u002F JS methods we want to use across our component will go here\n  },\n  mounted () {\n    \u002F\u002F Code we want to run when our component is loaded will go here\n  },\n  unmounted () {\n    \u002F\u002F Code we want to run when our component is unloaded will go here\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle>\n    \u002F* We can define custom CSS here too *\u002F\n\u003C\u002Fstyle>\n","html","",[21,107,108,124,131,141,148,158,171,193,203,209,215,226,232,237,247,253,259,265,274,279,289,295],{"__ignoreMap":105},[109,110,113,117,121],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"sMK4o","\u003C",[109,118,120],{"class":119},"swJcz","template",[109,122,123],{"class":115},">\n",[109,125,127],{"class":111,"line":126},2,[109,128,130],{"class":129},"sHwdD","    \u003C!-- Our HTML content will go here -->\n",[109,132,134,137,139],{"class":111,"line":133},3,[109,135,136],{"class":115},"\u003C\u002F",[109,138,120],{"class":119},[109,140,123],{"class":115},[109,142,144],{"class":111,"line":143},4,[109,145,147],{"emptyLinePlaceholder":146},true,"\n",[109,149,151,153,156],{"class":111,"line":150},5,[109,152,116],{"class":115},[109,154,155],{"class":119},"script",[109,157,123],{"class":115},[109,159,161,165,168],{"class":111,"line":160},6,[109,162,164],{"class":163},"s7zQu","export",[109,166,167],{"class":163}," default",[109,169,170],{"class":115}," {\n",[109,172,174,177,180,183,187,190],{"class":111,"line":173},7,[109,175,176],{"class":119},"  name",[109,178,179],{"class":115},":",[109,181,182],{"class":115}," '",[109,184,186],{"class":185},"sfazB","MyComponent",[109,188,189],{"class":115},"'",[109,191,192],{"class":115},",\n",[109,194,196,199,201],{"class":111,"line":195},8,[109,197,198],{"class":119},"  methods",[109,200,179],{"class":115},[109,202,170],{"class":115},[109,204,206],{"class":111,"line":205},9,[109,207,208],{"class":129},"    \u002F\u002F JS methods we want to use across our component will go here\n",[109,210,212],{"class":111,"line":211},10,[109,213,214],{"class":115},"  },\n",[109,216,218,221,224],{"class":111,"line":217},11,[109,219,220],{"class":119},"  mounted",[109,222,223],{"class":115}," ()",[109,225,170],{"class":115},[109,227,229],{"class":111,"line":228},12,[109,230,231],{"class":129},"    \u002F\u002F Code we want to run when our component is loaded will go here\n",[109,233,235],{"class":111,"line":234},13,[109,236,214],{"class":115},[109,238,240,243,245],{"class":111,"line":239},14,[109,241,242],{"class":119},"  unmounted",[109,244,223],{"class":115},[109,246,170],{"class":115},[109,248,250],{"class":111,"line":249},15,[109,251,252],{"class":129},"    \u002F\u002F Code we want to run when our component is unloaded will go here\n",[109,254,256],{"class":111,"line":255},16,[109,257,258],{"class":115},"  }\n",[109,260,262],{"class":111,"line":261},17,[109,263,264],{"class":115},"}\n",[109,266,268,270,272],{"class":111,"line":267},18,[109,269,136],{"class":115},[109,271,155],{"class":119},[109,273,123],{"class":115},[109,275,277],{"class":111,"line":276},19,[109,278,147],{"emptyLinePlaceholder":146},[109,280,282,284,287],{"class":111,"line":281},20,[109,283,116],{"class":115},[109,285,286],{"class":119},"style",[109,288,123],{"class":115},[109,290,292],{"class":111,"line":291},21,[109,293,294],{"class":129},"    \u002F* We can define custom CSS here too *\u002F\n",[109,296,298,300,302],{"class":111,"line":297},22,[109,299,136],{"class":115},[109,301,286],{"class":119},[109,303,123],{"class":115},[12,305,306],{},"Some quick gotchas to note:",[26,308,309,316,322,328,334,351],{},[29,310,311,312,315],{},"{% raw %}",[21,313,314],{},"\u003Cdiv>{{ msg }}\u003C\u002Fdiv>","{% endraw %} - is an example of how you render variables into the HTML.",[29,317,318,321],{},[21,319,320],{},"\u003Cdiv v-if=\"myVar\">\u003C\u002Fdiv>"," - lets you conditionally show\u002Fhide content based on a variable.",[29,323,324,327],{},[21,325,326],{},"\u003Cdiv v-for=\"item in items\">\u003C\u002Fdiv>"," - lets you loop over an array of items and render them into the HTML.",[29,329,330,333],{},[21,331,332],{},"\u003Cdiv @click=\"myMethod\">\u003C\u002Fdiv>"," - lets you bind a method to an event, in this case, when the user clicks on the div.",[29,335,336,339,340,342,343,346,347,350],{},[21,337,338],{},"\u003Cdiv :class=\"{ 'my-class': isActive }\">\u003C\u002Fdiv>"," - ",[21,341,179],{}," is a way to define a \"bound\" property. In this case, the class ",[21,344,345],{},"my-class"," will be applied when ",[21,348,349],{},"isActive"," is true.",[29,352,353,356,357,359,360,79],{},[21,354,355],{},"console.log(this.myVar)"," - when you're writing code inside the ",[21,358,37],{}," tags, you can access Component variables and methods using ",[21,361,362],{},"this",[364,365,367],"h3",{"id":366},"built-in-extras","Built-in Extras",[12,369,370,371,373],{},"In addition to building a component from scratch, we'll also utilize some built-in features of ",[21,372,23],{}," too. These will be:",[26,375,376,402],{},[29,377,378,382],{},[379,380,381],"strong",{},"Variables:",[26,383,384,390,396],{},[29,385,386,389],{},[21,387,388],{},"id"," - The unique ID for this node in Node-RED",[29,391,392,395],{},[21,393,394],{},"msg"," - The message that was most recently received into the node",[29,397,398,401],{},[21,399,400],{},"$socket"," - The underlying SocketIO connection to Node-RED. Use this to listen to any incoming events, and send new ones back.",[29,403,404,407],{},[379,405,406],{},"Functions:",[26,408,409],{},[29,410,411,414],{},[21,412,413],{},"send(payload)"," - Send a message back to Node-RED",[12,416,417,418,79],{},"As above, we have more detailed documentation on these features ",[92,419,97],{"href":420,"rel":421},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#built-in-functionality",[96],[81,423,425],{"id":424},"building-the-video-player","Building the Video Player",[364,427,429],{"id":428},"defining-the-content-html","Defining the Content (HTML)",[12,431,432],{},"We're going to start by adding a basic HTML video player:",[100,434,436],{"className":102,"code":435,"language":104,"meta":105,"style":105},"\u003Ctemplate>\n    \u003Cvideo ref=\"my-video\" style=\"width: 100%\" controls @play=\"onPlay\" @pause=\"onPause\">\n        \u003Csource src=\"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4\" type=\"video\u002Fmp4\">\n        Your browser does not support the video tag.\n    \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n",[21,437,438,446,510,544,550,559],{"__ignoreMap":105},[109,439,440,442,444],{"class":111,"line":112},[109,441,116],{"class":115},[109,443,120],{"class":119},[109,445,123],{"class":115},[109,447,448,451,454,458,461,464,467,469,472,474,476,479,481,484,487,489,491,494,496,499,501,503,506,508],{"class":111,"line":126},[109,449,450],{"class":115},"    \u003C",[109,452,453],{"class":119},"video",[109,455,457],{"class":456},"spNyl"," ref",[109,459,460],{"class":115},"=",[109,462,463],{"class":115},"\"",[109,465,466],{"class":185},"my-video",[109,468,463],{"class":115},[109,470,471],{"class":456}," style",[109,473,460],{"class":115},[109,475,463],{"class":115},[109,477,478],{"class":185},"width: 100%",[109,480,463],{"class":115},[109,482,483],{"class":456}," controls",[109,485,486],{"class":456}," @play",[109,488,460],{"class":115},[109,490,463],{"class":115},[109,492,493],{"class":185},"onPlay",[109,495,463],{"class":115},[109,497,498],{"class":456}," @pause",[109,500,460],{"class":115},[109,502,463],{"class":115},[109,504,505],{"class":185},"onPause",[109,507,463],{"class":115},[109,509,123],{"class":115},[109,511,512,515,518,521,523,525,528,530,533,535,537,540,542],{"class":111,"line":133},[109,513,514],{"class":115},"        \u003C",[109,516,517],{"class":119},"source",[109,519,520],{"class":456}," src",[109,522,460],{"class":115},[109,524,463],{"class":115},[109,526,527],{"class":185},"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4",[109,529,463],{"class":115},[109,531,532],{"class":456}," type",[109,534,460],{"class":115},[109,536,463],{"class":115},[109,538,539],{"class":185},"video\u002Fmp4",[109,541,463],{"class":115},[109,543,123],{"class":115},[109,545,546],{"class":111,"line":143},[109,547,549],{"class":548},"sTEyZ","        Your browser does not support the video tag.\n",[109,551,552,555,557],{"class":111,"line":150},[109,553,554],{"class":115},"    \u003C\u002F",[109,556,453],{"class":119},[109,558,123],{"class":115},[109,560,561,563,565],{"class":111,"line":160},[109,562,136],{"class":115},[109,564,120],{"class":119},[109,566,123],{"class":115},[12,568,569],{},"A few things of importance to note here:",[26,571,572,586,592,605],{},[29,573,574,577,578,581,582,585],{},[21,575,576],{},"ref"," is Vue's replacement for ",[21,579,580],{},"document.getElementById()",". This is copied to each instance of the component, meaning we can call ",[21,583,584],{},"this.$refs['my-video']"," to access the video element, and this doesn't break when duplicating the widget multiple times in Dashboard.",[29,587,588,591],{},[21,589,590],{},"style=\"\""," is required here to ensure the video fills the group\u002Fwrapper that it is contained within.",[29,593,594,597,598,601,602,604],{},[21,595,596],{},"@play="," is Vue's way of binding onto the standard ",[21,599,600],{},"onplay"," event listener available on HTML video players. We'll define the ",[21,603,493],{}," method in the next section.",[29,606,607,610,611,613],{},[21,608,609],{},"@pause="," is our event listener for when the video is paused by the user. As with ",[21,612,493],{},", we'll define this shortly.",[12,615,616,617,620],{},"With ",[16,618,619],{},"just"," the above defined, we end up with a standard video player rendered:",[12,622,623],{},[624,625],"img",{"alt":626,"src":627},"HTML5 Video Player rendered in Dashboard","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-1.png",[364,629,631],{"id":630},"defining-the-behaviors-vuejs","Defining the Behaviors (VueJS)",[12,633,634],{},"Now we begin to build our Vue component. Referring back to our earlier set of features, we'll tackle these one at a time.",[636,637,639],"h4",{"id":638},"_1-emitting-events-to-node-red-on-playpause","1. Emitting Events to Node-RED on Play\u002FPause",[12,641,642,643,646,647,649,650,652,653,656,657,660],{},"We can use ",[21,644,645],{},"methods"," to define our ",[21,648,493],{}," and ",[21,651,505],{}," functions that are called ",[21,654,655],{},"@play","\u002F",[21,658,659],{},"@pause"," respectively.",[100,662,664],{"className":102,"code":663,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    capture (eventType) {\n        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n        \u002F\u002F this prevents duplicated code across the two methods\n\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n        this.send({\n            \u002F\u002F specify which action is taking place\n            event: eventType,\n            \u002F\u002F use Vue's $refs to get the video's currentTime\n            time: video.currentTime\n        })\n    },\n    onPlay () {\n        this.capture('play')\n    },\n    onPause () {\n        this.capture('pause')\n    }\n  }\n}\n\u003C\u002Fscript>\n",[21,665,666,674,682,697,705,722,727,732,736,741,770,774,779,794,799,811,816,830,838,843,852,870,874,884,902,908,913,918],{"__ignoreMap":105},[109,667,668,670,672],{"class":111,"line":112},[109,669,116],{"class":115},[109,671,155],{"class":119},[109,673,123],{"class":115},[109,675,676,678,680],{"class":111,"line":126},[109,677,164],{"class":163},[109,679,167],{"class":163},[109,681,170],{"class":115},[109,683,684,686,688,690,693,695],{"class":111,"line":133},[109,685,176],{"class":119},[109,687,179],{"class":115},[109,689,182],{"class":115},[109,691,692],{"class":185},"MyVideoPlayer",[109,694,189],{"class":115},[109,696,192],{"class":115},[109,698,699,701,703],{"class":111,"line":143},[109,700,198],{"class":119},[109,702,179],{"class":115},[109,704,170],{"class":115},[109,706,707,710,713,717,720],{"class":111,"line":150},[109,708,709],{"class":119},"    capture",[109,711,712],{"class":115}," (",[109,714,716],{"class":715},"sHdIc","eventType",[109,718,719],{"class":115},")",[109,721,170],{"class":115},[109,723,724],{"class":111,"line":160},[109,725,726],{"class":129},"        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n",[109,728,729],{"class":111,"line":173},[109,730,731],{"class":129},"        \u002F\u002F this prevents duplicated code across the two methods\n",[109,733,734],{"class":111,"line":195},[109,735,147],{"emptyLinePlaceholder":146},[109,737,738],{"class":111,"line":205},[109,739,740],{"class":129},"        \u002F\u002F get the Video's DOM element\n",[109,742,743,746,749,752,755,758,761,763,765,767],{"class":111,"line":211},[109,744,745],{"class":456},"        const",[109,747,748],{"class":548}," video",[109,750,751],{"class":115}," =",[109,753,754],{"class":115}," this.",[109,756,757],{"class":548},"$refs",[109,759,760],{"class":119},"[",[109,762,189],{"class":115},[109,764,466],{"class":185},[109,766,189],{"class":115},[109,768,769],{"class":119},"]\n",[109,771,772],{"class":111,"line":217},[109,773,147],{"emptyLinePlaceholder":146},[109,775,776],{"class":111,"line":228},[109,777,778],{"class":129},"        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n",[109,780,781,784,788,791],{"class":111,"line":234},[109,782,783],{"class":115},"        this.",[109,785,787],{"class":786},"s2Zo4","send",[109,789,790],{"class":119},"(",[109,792,793],{"class":115},"{\n",[109,795,796],{"class":111,"line":239},[109,797,798],{"class":129},"            \u002F\u002F specify which action is taking place\n",[109,800,801,804,806,809],{"class":111,"line":249},[109,802,803],{"class":119},"            event",[109,805,179],{"class":115},[109,807,808],{"class":548}," eventType",[109,810,192],{"class":115},[109,812,813],{"class":111,"line":255},[109,814,815],{"class":129},"            \u002F\u002F use Vue's $refs to get the video's currentTime\n",[109,817,818,821,823,825,827],{"class":111,"line":261},[109,819,820],{"class":119},"            time",[109,822,179],{"class":115},[109,824,748],{"class":548},[109,826,79],{"class":115},[109,828,829],{"class":548},"currentTime\n",[109,831,832,835],{"class":111,"line":267},[109,833,834],{"class":115},"        }",[109,836,837],{"class":119},")\n",[109,839,840],{"class":111,"line":276},[109,841,842],{"class":115},"    },\n",[109,844,845,848,850],{"class":111,"line":281},[109,846,847],{"class":119},"    onPlay",[109,849,223],{"class":115},[109,851,170],{"class":115},[109,853,854,856,859,861,863,866,868],{"class":111,"line":291},[109,855,783],{"class":115},[109,857,858],{"class":786},"capture",[109,860,790],{"class":119},[109,862,189],{"class":115},[109,864,865],{"class":185},"play",[109,867,189],{"class":115},[109,869,837],{"class":119},[109,871,872],{"class":111,"line":297},[109,873,842],{"class":115},[109,875,877,880,882],{"class":111,"line":876},23,[109,878,879],{"class":119},"    onPause",[109,881,223],{"class":115},[109,883,170],{"class":115},[109,885,887,889,891,893,895,898,900],{"class":111,"line":886},24,[109,888,783],{"class":115},[109,890,858],{"class":786},[109,892,790],{"class":119},[109,894,189],{"class":115},[109,896,897],{"class":185},"pause",[109,899,189],{"class":115},[109,901,837],{"class":119},[109,903,905],{"class":111,"line":904},25,[109,906,907],{"class":115},"    }\n",[109,909,911],{"class":111,"line":910},26,[109,912,258],{"class":115},[109,914,916],{"class":111,"line":915},27,[109,917,264],{"class":115},[109,919,921,923,925],{"class":111,"line":920},28,[109,922,136],{"class":115},[109,924,155],{"class":119},[109,926,123],{"class":115},[12,928,929,930,932,933,936],{},"With this functionality in place, we can wire the ",[21,931,23],{}," node to a ",[21,934,935],{},"debug"," node, and see the following when we play\u002Fpause the video:",[12,938,939],{},[624,940],{"alt":941,"src":942},"Example debug output when our custom build video player is played\u002Fpaused","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-2.png",[636,944,946],{"id":945},"_2-remote-control-of-playpause-from-node-red","2. Remote control of play\u002Fpause from Node-RED",[12,948,949,950,952,953,955,956,959,960,649,963,966,967,970],{},"We can use the built-in ",[21,951,400],{}," variable to listen for incoming events from Node-RED. When Dashboard 2.0's nodes receive a ",[21,954,394],{}," inside Node-RED, they send a ",[21,957,958],{},"msg-input:\u003Cnode-id>"," event to the Dashboard client. We can listen for this event and then call the ",[21,961,962],{},"play()",[21,964,965],{},"pause()"," methods on the video element, depending on any properties of that message, in this case, the ",[21,968,969],{},"msg.payload.event"," value.",[100,972,974],{"className":102,"code":973,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F listen for incoming msg's from Node-RED\n    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F if the event is \"play\", call the video's play() method\n        if (msg.payload?.event === 'play') {\n            video.play()\n        }\n\n        \u002F\u002F if the event is \"pause\", call the video's pause() method\n        if (msg.payload?.event === 'pause') {\n            video.pause()\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n    this.$socket.off(`msg-input:${this.id}`)\n  }\n}\n\u003C\u002Fscript>\n",[21,975,976,984,992,1006,1014,1019,1023,1031,1036,1041,1083,1087,1109,1113,1118,1152,1164,1169,1173,1178,1206,1216,1220,1227,1231,1239,1244,1275,1279,1284],{"__ignoreMap":105},[109,977,978,980,982],{"class":111,"line":112},[109,979,116],{"class":115},[109,981,155],{"class":119},[109,983,123],{"class":115},[109,985,986,988,990],{"class":111,"line":126},[109,987,164],{"class":163},[109,989,167],{"class":163},[109,991,170],{"class":115},[109,993,994,996,998,1000,1002,1004],{"class":111,"line":133},[109,995,176],{"class":119},[109,997,179],{"class":115},[109,999,182],{"class":115},[109,1001,692],{"class":185},[109,1003,189],{"class":115},[109,1005,192],{"class":115},[109,1007,1008,1010,1012],{"class":111,"line":143},[109,1009,198],{"class":119},[109,1011,179],{"class":115},[109,1013,170],{"class":115},[109,1015,1016],{"class":111,"line":150},[109,1017,1018],{"class":129},"    \u002F\u002F ...\n",[109,1020,1021],{"class":111,"line":160},[109,1022,214],{"class":115},[109,1024,1025,1027,1029],{"class":111,"line":173},[109,1026,220],{"class":119},[109,1028,223],{"class":115},[109,1030,170],{"class":115},[109,1032,1033],{"class":111,"line":195},[109,1034,1035],{"class":129},"    \u002F\u002F listen for incoming msg's from Node-RED\n",[109,1037,1038],{"class":111,"line":205},[109,1039,1040],{"class":129},"    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n",[109,1042,1043,1046,1048,1050,1053,1055,1057,1060,1062,1065,1067,1069,1072,1074,1076,1078,1081],{"class":111,"line":211},[109,1044,1045],{"class":115},"    this.",[109,1047,400],{"class":548},[109,1049,79],{"class":115},[109,1051,1052],{"class":786},"on",[109,1054,790],{"class":119},[109,1056,189],{"class":115},[109,1058,1059],{"class":185},"msg-input:",[109,1061,189],{"class":115},[109,1063,1064],{"class":115}," +",[109,1066,754],{"class":115},[109,1068,388],{"class":548},[109,1070,1071],{"class":115},",",[109,1073,712],{"class":115},[109,1075,394],{"class":715},[109,1077,719],{"class":115},[109,1079,1080],{"class":456}," =>",[109,1082,170],{"class":115},[109,1084,1085],{"class":111,"line":217},[109,1086,740],{"class":129},[109,1088,1089,1091,1093,1095,1097,1099,1101,1103,1105,1107],{"class":111,"line":228},[109,1090,745],{"class":456},[109,1092,748],{"class":548},[109,1094,751],{"class":115},[109,1096,754],{"class":115},[109,1098,757],{"class":548},[109,1100,760],{"class":119},[109,1102,189],{"class":115},[109,1104,466],{"class":185},[109,1106,189],{"class":115},[109,1108,769],{"class":119},[109,1110,1111],{"class":111,"line":234},[109,1112,147],{"emptyLinePlaceholder":146},[109,1114,1115],{"class":111,"line":239},[109,1116,1117],{"class":129},"        \u002F\u002F if the event is \"play\", call the video's play() method\n",[109,1119,1120,1123,1125,1127,1129,1132,1135,1138,1141,1143,1145,1147,1150],{"class":111,"line":249},[109,1121,1122],{"class":163},"        if",[109,1124,712],{"class":119},[109,1126,394],{"class":548},[109,1128,79],{"class":115},[109,1130,1131],{"class":548},"payload",[109,1133,1134],{"class":115},"?.",[109,1136,1137],{"class":548},"event",[109,1139,1140],{"class":115}," ===",[109,1142,182],{"class":115},[109,1144,865],{"class":185},[109,1146,189],{"class":115},[109,1148,1149],{"class":119},") ",[109,1151,793],{"class":115},[109,1153,1154,1157,1159,1161],{"class":111,"line":255},[109,1155,1156],{"class":548},"            video",[109,1158,79],{"class":115},[109,1160,865],{"class":786},[109,1162,1163],{"class":119},"()\n",[109,1165,1166],{"class":111,"line":261},[109,1167,1168],{"class":115},"        }\n",[109,1170,1171],{"class":111,"line":267},[109,1172,147],{"emptyLinePlaceholder":146},[109,1174,1175],{"class":111,"line":276},[109,1176,1177],{"class":129},"        \u002F\u002F if the event is \"pause\", call the video's pause() method\n",[109,1179,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198,1200,1202,1204],{"class":111,"line":281},[109,1181,1122],{"class":163},[109,1183,712],{"class":119},[109,1185,394],{"class":548},[109,1187,79],{"class":115},[109,1189,1131],{"class":548},[109,1191,1134],{"class":115},[109,1193,1137],{"class":548},[109,1195,1140],{"class":115},[109,1197,182],{"class":115},[109,1199,897],{"class":185},[109,1201,189],{"class":115},[109,1203,1149],{"class":119},[109,1205,793],{"class":115},[109,1207,1208,1210,1212,1214],{"class":111,"line":291},[109,1209,1156],{"class":548},[109,1211,79],{"class":115},[109,1213,897],{"class":786},[109,1215,1163],{"class":119},[109,1217,1218],{"class":111,"line":297},[109,1219,1168],{"class":115},[109,1221,1222,1225],{"class":111,"line":876},[109,1223,1224],{"class":115},"    }",[109,1226,837],{"class":119},[109,1228,1229],{"class":111,"line":886},[109,1230,214],{"class":115},[109,1232,1233,1235,1237],{"class":111,"line":904},[109,1234,242],{"class":119},[109,1236,223],{"class":115},[109,1238,170],{"class":115},[109,1240,1241],{"class":111,"line":910},[109,1242,1243],{"class":129},"    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n",[109,1245,1246,1248,1250,1252,1255,1257,1260,1262,1265,1268,1270,1273],{"class":111,"line":915},[109,1247,1045],{"class":115},[109,1249,400],{"class":548},[109,1251,79],{"class":115},[109,1253,1254],{"class":786},"off",[109,1256,790],{"class":119},[109,1258,1259],{"class":115},"`",[109,1261,1059],{"class":185},[109,1263,1264],{"class":115},"${",[109,1266,1267],{"class":115},"this.",[109,1269,388],{"class":548},[109,1271,1272],{"class":115},"}`",[109,1274,837],{"class":119},[109,1276,1277],{"class":111,"line":920},[109,1278,258],{"class":115},[109,1280,1282],{"class":111,"line":1281},29,[109,1283,264],{"class":115},[109,1285,1287,1289,1291],{"class":111,"line":1286},30,[109,1288,136],{"class":115},[109,1290,155],{"class":119},[109,1292,123],{"class":115},[636,1294,1296],{"id":1295},"_3-seeking-to-a-specific-point-in-the-video-from-within-node-red","3. Seeking to a specific point in the video from within Node-RED",[12,1298,1299,1300,1303],{},"With the ",[21,1301,1302],{},"on('msg-input')"," listener in place, we can now extend our handler to handle seeking to a specific point in the video.",[100,1305,1307],{"className":102,"code":1306,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F ...\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F ... other handlers\n\n        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n        if (msg.payload?.event === 'seek') {\n            video.currentTime = msg.payload.currentTime\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F ...\n  }\n}\n\u003C\u002Fscript>\n",[21,1308,1309,1317,1325,1339,1347,1351,1355,1363,1367,1403,1408,1412,1417,1446,1468,1472,1478,1482,1490,1494,1498,1502],{"__ignoreMap":105},[109,1310,1311,1313,1315],{"class":111,"line":112},[109,1312,116],{"class":115},[109,1314,155],{"class":119},[109,1316,123],{"class":115},[109,1318,1319,1321,1323],{"class":111,"line":126},[109,1320,164],{"class":163},[109,1322,167],{"class":163},[109,1324,170],{"class":115},[109,1326,1327,1329,1331,1333,1335,1337],{"class":111,"line":133},[109,1328,176],{"class":119},[109,1330,179],{"class":115},[109,1332,182],{"class":115},[109,1334,692],{"class":185},[109,1336,189],{"class":115},[109,1338,192],{"class":115},[109,1340,1341,1343,1345],{"class":111,"line":143},[109,1342,198],{"class":119},[109,1344,179],{"class":115},[109,1346,170],{"class":115},[109,1348,1349],{"class":111,"line":150},[109,1350,1018],{"class":129},[109,1352,1353],{"class":111,"line":160},[109,1354,214],{"class":115},[109,1356,1357,1359,1361],{"class":111,"line":173},[109,1358,220],{"class":119},[109,1360,223],{"class":115},[109,1362,170],{"class":115},[109,1364,1365],{"class":111,"line":195},[109,1366,1018],{"class":129},[109,1368,1369,1371,1373,1375,1377,1379,1381,1383,1385,1387,1389,1391,1393,1395,1397,1399,1401],{"class":111,"line":205},[109,1370,1045],{"class":115},[109,1372,400],{"class":548},[109,1374,79],{"class":115},[109,1376,1052],{"class":786},[109,1378,790],{"class":119},[109,1380,189],{"class":115},[109,1382,1059],{"class":185},[109,1384,189],{"class":115},[109,1386,1064],{"class":115},[109,1388,754],{"class":115},[109,1390,388],{"class":548},[109,1392,1071],{"class":115},[109,1394,712],{"class":115},[109,1396,394],{"class":715},[109,1398,719],{"class":115},[109,1400,1080],{"class":456},[109,1402,170],{"class":115},[109,1404,1405],{"class":111,"line":211},[109,1406,1407],{"class":129},"        \u002F\u002F ... other handlers\n",[109,1409,1410],{"class":111,"line":217},[109,1411,147],{"emptyLinePlaceholder":146},[109,1413,1414],{"class":111,"line":228},[109,1415,1416],{"class":129},"        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n",[109,1418,1419,1421,1423,1425,1427,1429,1431,1433,1435,1437,1440,1442,1444],{"class":111,"line":234},[109,1420,1122],{"class":163},[109,1422,712],{"class":119},[109,1424,394],{"class":548},[109,1426,79],{"class":115},[109,1428,1131],{"class":548},[109,1430,1134],{"class":115},[109,1432,1137],{"class":548},[109,1434,1140],{"class":115},[109,1436,182],{"class":115},[109,1438,1439],{"class":185},"seek",[109,1441,189],{"class":115},[109,1443,1149],{"class":119},[109,1445,793],{"class":115},[109,1447,1448,1450,1452,1455,1457,1460,1462,1464,1466],{"class":111,"line":239},[109,1449,1156],{"class":548},[109,1451,79],{"class":115},[109,1453,1454],{"class":548},"currentTime",[109,1456,751],{"class":115},[109,1458,1459],{"class":548}," msg",[109,1461,79],{"class":115},[109,1463,1131],{"class":548},[109,1465,79],{"class":115},[109,1467,829],{"class":548},[109,1469,1470],{"class":111,"line":249},[109,1471,1168],{"class":115},[109,1473,1474,1476],{"class":111,"line":255},[109,1475,1224],{"class":115},[109,1477,837],{"class":119},[109,1479,1480],{"class":111,"line":261},[109,1481,214],{"class":115},[109,1483,1484,1486,1488],{"class":111,"line":267},[109,1485,242],{"class":119},[109,1487,223],{"class":115},[109,1489,170],{"class":115},[109,1491,1492],{"class":111,"line":276},[109,1493,1018],{"class":129},[109,1495,1496],{"class":111,"line":281},[109,1497,258],{"class":115},[109,1499,1500],{"class":111,"line":291},[109,1501,264],{"class":115},[109,1503,1504,1506,1508],{"class":111,"line":297},[109,1505,136],{"class":115},[109,1507,155],{"class":119},[109,1509,123],{"class":115},[12,1511,1512],{},"and with that, we now have a Dashboard 2.0 widget to display a video, that can be controlled from Node-RED, and logs details of user activity back into Node-RED.",[12,1514,1515],{},"Other features available with the UI Template are detailed in the online documentation, and include:",[26,1517,1518,1525],{},[29,1519,1520],{},[92,1521,1524],{"href":1522,"rel":1523},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#loading-external-dependencies",[96],"Loading External Dependencies",[29,1526,1527],{},[92,1528,1531],{"href":1529,"rel":1530},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#writing-raw-javascript",[96],"Running raw JavaScript",[81,1533,1535],{"id":1534},"follow-our-progress","Follow our Progress",[12,1537,1538,1539,1542],{},"You can also read the more comprehensive release notes for ",[21,1540,1541],{},"v0.10.0"," release here:",[26,1544,1545],{},[29,1546,1547],{},[92,1548,1551],{"href":1549,"rel":1550},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard\u002Freleases\u002Ftag\u002Fv0.10.0",[96],"0.10.0 Release Notes",[12,1553,1554,1555,79],{},"As always, thanks for reading and your interest in Dashboard 2.0. If you have any feature requests, bugs\u002Fcomplaints or general feedback, please do reach out, and raise issues on our relevant ",[92,1556,1559],{"href":1557,"rel":1558},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard",[96],"GitHub repository",[26,1561,1562,1569],{},[29,1563,1564],{},[92,1565,1568],{"href":1566,"rel":1567},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F1",[96],"Dashboard 2.0 Activity Tracker",[29,1570,1571],{},[92,1572,1575],{"href":1573,"rel":1574},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F4",[96],"Dashboard 2.0 Planning Board",[286,1577,1578],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"title":105,"searchDepth":143,"depth":143,"links":1580},[1581,1584,1592],{"id":83,"depth":126,"text":84,"children":1582},[1583],{"id":366,"depth":133,"text":367},{"id":424,"depth":126,"text":425,"children":1585},[1586,1587],{"id":428,"depth":133,"text":429},{"id":630,"depth":133,"text":631,"children":1588},[1589,1590,1591],{"id":638,"depth":143,"text":639},{"id":945,"depth":143,"text":946},{"id":1295,"depth":143,"text":1296},{"id":1534,"depth":126,"text":1535},null,"2023-12-07","Delve into the possibilities of Dashboard 2.0's new UI Templates with a comprehensive tutorial on building a custom video player.","md","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Ftile-dashboard-0-10-0.png",{"excerpt":1599},{"type":9,"value":1600},[1601,1607],[12,1602,14,1603,19,1605,24],{},[16,1604,18],{},[21,1606,23],{},[26,1608,1609,1611,1615],{},[29,1610,31],{},[29,1612,34,1613,38],{},[21,1614,37],{},[29,1616,41,1617,38],{},[21,1618,37],{},"\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0",{"title":5,"description":1595},{"loc":1619},"blog\u002F2023\u002F12\u002Fdashboard-0-10-0","We've just released the latest version of Dashboard 2.0, with a fully featured UI Templates node which now allows for full definition of a Vue component, external JS dependencies and CSS.",[1625,1626,1627,1628,1629],"posts","releases","community","changelog","dashboard","pFYofgu7OtLAMd_JKnY0DZ51wfEtz4nB2vUmHlVNWhs",[],1785528688939]