-
-
Notifications
You must be signed in to change notification settings - Fork 86
Examples Vue Complex Home Dashboard
I'm afraid that to actually use this example directly, you would need to have my complex flow that builds the data from a combination of my Drayton Wiser smart home heating system and various custom sensors and controls. So I've not bothered to post the flow that controls all of this but it may be of use if you are struggling with how to use VueJS with Node-RED and uibuilder.
This code is valid with uibuilder v1.2.2. It uses VueJS, and bootstrap-vue. Note the use of Vue components to break up the complexity of the page structure.
<!doctype html>
<html lang="en" manifest="uibuilder.appcache">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<!-- See https://goo.gl/OOhYW5 -->
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#3f51b5">
<!-- Used if adding to homescreen for Chrome on Android. Fallback for manifest.json -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Home Dashboard">
<!-- Used if adding to homescreen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Home Dashboard">
<!-- Homescreen icons for Apple mobile use if required
<link rel="apple-touch-icon" href="/images/manifest/icon-48x48.png">
<link rel="apple-touch-icon" sizes="72x72" href="/images/manifest/icon-72x72.png">
<link rel="apple-touch-icon" sizes="96x96" href="/images/manifest/icon-96x96.png">
<link rel="apple-touch-icon" sizes="144x144" href="/images/manifest/icon-144x144.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/manifest/icon-192x192.png">
-->
<title>Node-RED UI Builder</title>
<meta name="description" content="Home Dashboard">
<link rel="icon" href="./images/node-blue.ico">
<link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />
<link rel="stylesheet" href="./index.css">
</head>
<body>
<script type="text/x-template" id="lights-tab-template">
<div>
<h5>Room Switches</h5>
<div v-for="room in homeData" :key="room.Name" v-if="room.switches">
<b-row class="my-2">
<b-col cols="6" sm="3" md="2">
{{ room.Name }}
</b-col>
<b-col>
<b-button-group >
<b-button v-for="sw in switches" :key="sw.id"
v-if="sw.room === room.Name"
:variant="sw.status === 'On' ? 'success' : ''"
@click="switchClick([sw.id, sw.status])"
v-b-popover.focus.hover.bottomright="{content:`Last Update: ${fmtTime(sw.lastUpdate)}`}">
{{ sw.id.replace('SWITCH','') }} - {{ _.capitalize(sw.status) }}
</b-button>
</b-button-group>
</b-col>
</b-row>
</div>
</div>
</script>
<script type="text/x-template" id="demand-card-template">
<b-card id="demand_card" header-tag="header" footer-tag="footer" class="text-center shadow"
v-b-popover.focus.hover.bottomright="{content:'Bar shows overall % demand. See room details for room demands.'}"
>
<h6 slot="header">Demand</h6>
<b-progress :max="demandMax" height="2rem">
<b-progress-bar :value="percentageDemand" :variant="demandLevel">{{percentageDemand}}%</b-progress-bar>
</b-progress>
<div slot="footer">
<span :class="classDemandActive">
Boiler {{demandOnOffOutput}}
</span>
,
<span :class="classIsBoosted">
Boost {{ isBoostedText }}
</span>
</div>
</b-card>
</script>
<script type="text/x-template" id="device-tab-template">
<div>
<h5>Devices</h5>
<div v-for="device in orderedDevices" :key="device.id">
<b-row class="my-2">
<b-col cols="6" sm="3" md="2">
{{ device.id }}
</b-col>
<b-col>
<b-button :variant="device.status === 'Online' ? 'success' : 'warning'"
v-b-popover.focus.hover.bottomright="{content:`Last Update: ${fmtTime(device.lastUpdate)}`}">
{{ _.capitalize(device.status) }}{{ device.room ? ' - ' : '' }}{{ device.room }}
</b-button>
</b-col>
<b-col>
{{ fmtTime(device.lastUpdate) }}
</b-col>
</b-row>
</div>
</div>
</script>
<!-- The "app" element is where the code for dynamic updates is attached -->
<div id="app">
<b-container id="app_container">
<b-navbar toggleable="md" type="dark" variant="dark">
<b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
<b-navbar-brand href="#" v-b-popover.focus.hover.bottomright="{content:'Heating information and controls.'}">
Home
</b-navbar-brand>
<b-collapse is-nav id="nav_collapse">
<b-navbar-nav>
<b-nav-text
v-b-popover.focus.hover.bottomright="{title:'Last update',content:'A warning will appear if no updates have been received in 2 minutes.'}"
>
{{lastUpdate}}
</b-nav-text>
<b-nav-text v-if="demandOnOffOutput === 'On'"
v-b-popover.focus.hover.bottomright="{content:`Boiler is ${demandOnOffOutput}, Boost is ${isBoostedText}`}"
>
<svg height="24" style="margin-left:1em" class="octicon octicon-flame" viewBox="0 0 12 16" version="1.1" width="24" aria-hidden="true">
<path :style="isBoostedFill" fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path>
</svg>
</b-nav-text>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav-item-dropdown right
v-b-popover.focus.hover.bottomright="{content:'Links to other dashboards.'}">
<template slot="button-content">
<em>Dashboards</em>
</template>
<b-dropdown-item href="/ui">Quick Dashboard</b-dropdown-item>
<b-dropdown-item href="https://pi3.knightnet.co.uk:3000/" onclick="javascript:window.location.port=3000">Detailed
Dashboard</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown right
v-b-popover.focus.hover.bottomright="{content:'Links to admin web pages.'}">
<template slot="button-content">
<em>Admin</em>
</template>
<b-dropdown-item href="/red">Administration</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown right
v-b-popover.focus.hover.bottomright="{content:'Links to direct device web pages.'}">
<template slot="button-content">
<em>Devices</em>
</template>
<b-dropdown-item href="http://192.168.1.152/status">D1M02</b-dropdown-item>
<b-dropdown-item href="http://192.168.1.187/">D1M04</b-dropdown-item>
<b-dropdown-item href="http://192.168.1.188/">D1M05</b-dropdown-item>
<b-dropdown-item href="http://192.168.1.159">POW1</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-container id="warnings">
<b-alert variant="danger" :show="showNoUpdAlert" @dismissed="showNoUpdAlert=false">
<h4 class="alert-heading">Heating Warning:</h4>
<p>
No heating data update received in over 2 minutes.
</p>
<hr>
<p>
Check that the controller (on kitchen wall) is on and isn't showing red lights.
</p>
<p>
If any red lights showing, gently pull forwards the bottom of the controller until the
lights go off, wait 30sec then push the bottom back. The lights should go green after about
a minute.
</p>
This alert will go away when data is received again.
</b-alert>
</b-container>
<b-card no-body id="main">
<b-tabs card id="tabs" v-model="tabIndex" @input="changeTab">
<b-tab title="Lights">
<lights-tab :home-data="homeData" :switches="switches"></lights-tab>
</b-tab>
<b-tab title="Heating">
Sorry, not ready yet
</b-tab>
<b-tab title="Details">
<b-row>
<b-col cols="3">
<demand-card
:percentage-demand="percentageDemand"
:demand-level="demandLevel"
:demand-max="demandMax"
:demand-on-off-output="demandOnOffOutput"
:is-boosted="isBoosted">
</demand-card>
</b-col>
<b-col>
<b-card id="rooms_card" class="shadow">
<b-table responsive flex hover head-variant="dark" small stacked="sm" outlined
:items="homeData" :fields="homeDataFields"
:filter="currentRoomsTblFilter" @row-clicked="onRoomsRowClicked">
<template slot="override" slot-scope="row">
<p class="my-0"
v-b-popover.focus.hover.bottomright="{content:`Override: ${row.value}, Setpoint Origin: ${row.item.SetPointOrigin}`, title:'Heating Override Active?'}"
>
<b-form-checkbox v-model="row.value" disabled></b-form-checkbox>
</p>
</template>
<template slot="details" slot-scope="row" @click="row.toggleDetails">
<b-form-checkbox @click.native.stop @change="row.toggleDetails"
v-model="row.detailsShowing"
v-b-popover.focus.hover.bottomright="{content:`Show Details for ${row.item.Name}`}">
</b-form-checkbox>
</template>
<template slot="row-details" slot-scope="row">
<b-card>
<b-card v-if="row.item.ControlOutputState" border-variant="light">
<b-row class="my-0">
<b-col class="text-sm-right"><b>% Demand:</b></b-col>
<b-col>{{ row.item.percentageDemand }}</b-col>
</b-row>
<b-row class="my-0">
<b-col class="text-sm-right"><b>Ctrl Output State:</b></b-col>
<b-col>{{ row.item.ControlOutputState }}</b-col>
</b-row>
<b-row class="mt-2 mb-0">
<b-col class="text-sm-right">
<b>Current/Scheduled Room Setpoint:</b>
</b-col>
<b-col>
{{ row.item.DisplayedSetPoint === -200 ? 'OFF' : (row.item.DisplayedSetPoint/10) }}°c /
{{ row.item.ScheduledSetPoint === -200 ? 'OFF' : (row.item.ScheduledSetPoint/10) }}°c
</b-col>
</b-row>
<b-row class="my-0">
<b-col class="text-sm-right">
<b>Setpoint Origin:</b>
</b-col>
<b-col>
{{ row.item.SetPointOrigin }}
</b-col>
</b-row>
<b-row class="my-0">
<b-col class="text-sm-right">
<b>Override Type:</b>
</b-col>
<b-col>
{{ row.item.OverrideType }}
</b-col>
</b-row>
</b-card>
<b-card border-variant="light" v-if="row.item.devices.length > 0">
<b-row class="my-0">
<b-col>
<h6>Room Heating Devices</h6>
</b-col>
</b-row> <b-row>
<b-row>
<b-col>
<b-table responsive flex small stacked="sm" class="my-0"
:items="row.item.devices" :fields="hdDetailsFields">
</b-table>
</b-col>
</b-row>
</b-card>
<b-card border-variant="light" v-if="row.item.sensors">
<b-row class="my-0">
<b-col><h6>Room Sensors</h6></b-col>
</b-row>
<b-row class="my-0">
<b-col class="text-sm-right"><b>Temperature:</b></b-col>
<b-col>{{ row.item.sensors.Temperature }}°c</b-col>
</b-row>
<b-row class="my-0">
<b-col class="text-sm-right"><b>Humidity:</b></b-col>
<b-col>{{ row.item.sensors.Humidity }}%</b-col>
</b-row>
<b-row v-if="row.item.sensors.Light" class="my-0">
<b-col class="text-sm-right"><b>Light:</b></b-col>
<b-col>{{ row.item.sensors.Light }} Lux</b-col>
</b-row>
</b-card>
<b-button slot="footer" size="sm" @click="row.toggleDetails">Hide Details</b-button>
</b-card>
</template>
</b-table>
</b-card>
</b-col>
</b-row>
</b-tab>
<b-tab title="Boost">
Sorry, not ready yet
</b-tab>
<b-tab title="Schedules">
Sorry, not ready yet
</b-tab>
<b-tab title="Devices">
<device-tab :home-data="homeData" :devices="devices"></device-tab>
</b-tab>
<b-tab title="Help" v-b-popover.focus.hover.bottomright="{content:'Information on how to use this dashboard.'}">
This is a uibuilder test using <a href="http://vuejs.org/">Vue.js</a> as a front-end
library.
Along with the <a href="https://bootstrap-vue.js.org/docs/">bootstrap-vue</a> component
library.
See the
<a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder">node-red-contrib-uibuilder</a>
README and WIKI for details on how to use UIbuilder.
</b-tab>
</b-tabs>
</b-card>
<b-row no-gutters id="footer" class="text-light p-1 bg-dark">
<b-col>
</b-col>
</b-row>
</b-container>
</div>
<!-- These MUST be in the right order. -->
<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
<script src="./uibuilderfe.min.js"></script>
<!-- === Vendor Libraries - Load in the right order === -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script>
<script src="../uibuilder/vendor/vue/dist/vue.js"></script>
<script src="https://unpkg.com/babel-polyfill@latest/dist/polyfill.min.js"></script>
<script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>
<!-- <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.min.js"></script> -->
<!-- === Custom code goes in here === -->
<script src="./index.js"></script>
</body>
</html>
/*global document,$,window,uibuilder,Vue,_ */
/** Copyright (c) 2019 Julian Knight (Totally Information)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
**/
/** This is the default, template Front-End JavaScript for uibuilder
* It is usable as is though you will want to add your own code to
* process incoming and outgoing messages.
*
* uibuilderfe.js (or uibuilderfe.min.js) exposes the following global object:
* @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods
**/
'use strict'
/** Get a nested property from an object without returning any errors.
* If the property or property chain doesn't exist, undefined is returned.
* Property names with spaces may use either dot or bracket "[]" notation.
* Note that bracketed property names without surrounding quotes will fail the lookup.
* e.g. embedded variables are not supported.
* @param {Object} obj The object to check
* @param {string} prop The property or property chain to get (e.g. obj.prop1.prop1a or obj['prop1'].prop2)
* @returns {*|undefined} The value of the objects property or undefined if the property doesn't exist
*/
function getProp(obj, prop) {
if (typeof obj !== 'object') throw 'getProp: obj is not an object'
if (typeof prop !== 'string') throw 'getProp: prop is not a string'
// Replace [] notation with dot notation
prop = prop.replace(/\[["'`](.*)["'`]\]/g,".$1")
return prop.split('.').reduce(function(prev, curr) {
return prev ? prev[curr] : undefined
}, obj || self)
} // --- end of fn getProp() --- //
// Initialise Bootstrap-Vue: Not needed if loading via CDN
//Vue.use(BootstrapVue)
// Template Components
Vue.component('lights-tab', {
// NB: prop defined as 'home-data' because it is used as an HTML attribute. BUT use as variable 'homeData'
props: ['home-data', 'switches'],
template: '#lights-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
// orderedSwitches: function() {
// return _.orderBy(this.switches, 'room').filter(function (sw) {
// return sw.room === 'NA' ? false : true
// })
// },
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
switchClick: function(clickData) {
let [switchId, switchStatus] = clickData
//console.log('switchClick', switchId, switchStatus)
uibuilder.send({
'topic': `COMMAND/${switchId}`,
'payload': switchStatus.toLowerCase() === 'on' ? 'Off' : 'On'
})
},
},
})
Vue.component('demand-card', {
props: [
'percentage-demand', 'demand-level', 'demand-max',
'demand-on-off-output', 'is-boosted',
],
template: '#demand-card-template',
computed: {
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger font-weight-bold': ''
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
classIsBoosted: function() {
return this.isBoosted ? 'text-danger font-weight-bold': ''
},
},
})
Vue.component('device-tab', {
props: ['home-data', 'devices'],
template: '#device-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
orderedDevices: function() {
// Use LoDash to reorder the object
return _.orderBy(this.devices, 'id')
},
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
},
})
// Initialise Vue
new Vue({
el: "#app",
// We don't really need a function here but you do in components - keeping things consistent
data: function() { return {
// For formatting dates and times
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
// Which tab should be active?
tabIndex: 0,
// heating
lastUpdate : '[None]',
hTimer : null,
showNoUpdAlert : false,
demand : undefined,
percentageDemand : undefined,
demandOnOffOutput : 'N/A',
demandMax : 100,
isBoosted : false,
homeData : [],
// Field definitions - @see https://bootstrap-vue.js.org/docs/components/table#field-definition-reference
homeDataFields : [
{ key: 'floor',
label: 'Floor',
sortable: true,
class: 'border-right text-center',
thStyle: {width: '2em !important'},
},
{ key: 'Name',
label: 'Room',
sortable: true,
class: 'border-right',
// Variant applies to the whole column, including the header and footer
//variant: 'danger'
tdClass: (value, key, item) => {
const c = []
if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
if ( item.outside === true ) c.push('font-italic')
return c.join(' ')
},
tdAttr: {'title':'Blue BG = Room requesting heat. Italic = room is outside'},
},
{ key: 'CalculatedTemperature',
label: '°c',
sortable: true,
class: 'text-right border-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too cold or too hot
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value < 0 )
// Freezing
c.push('bg-danger')
else if ( value < 20 )
// <2
c.push('bg-warning')
else if ( value < 50 )
// <5
c.push(['text-white', 'bg-primary'])
else if ( value > 300 )
// >30
c.push('bg-warning')
} else {
// Indoors
if ( value < 100 )
// <10
c.push('bg-danger')
else if ( value > 230 )
// >23
c.push('bg-warning')
}
//if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
return c.join(' ')
},
tdAttr: {'title':"Temperature. Highlighted if too high or too low."},
},
{ key: 'CalculatedHumidity',
label: 'H%',
sortable: true,
class: 'text-right',
formatter: (value, key, item) => {
let h = Math.round(value)
h = isNaN(h) ? value : h
return h === undefined ? '' : h + '%'
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too high or too low
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value <40 ) c.push(['text-white', 'bg-primary'])
} else {
// Indoors
if ( value <40 ) c.push(['text-white', 'bg-primary'])
else if (value >60 ) c.push('bg-warning')
}
return c.join(' ')
},
tdAttr: {'title':"Humidity. Highlighted if too high or too low."},
},
// A virtual column with custom formatter
{ key: 'override',
label: 'O/ride',
class: 'border-left text-right',
thStyle: {width: '2em !important'},
formatter: (value, key, item) => {
if ( item.SetPointOrigin === undefined ) return false
else return item.SetPointOrigin!=='FromSchedule' ? true : false
},
},
{ key: 'details',
label: 'More',
class: 'text-right',
thStyle: {width: '2em !important'},
},
],
// For the details view of homeData table
hdDetailsFields: [
'ProductType','BatteryLevel','DisplayedSignalStrength',
{ key: 'SetPoint',
label: 'SetPoint °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredTemperature',
label: 'Measured °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredHumidity',
label: 'Measured Humidity',
class: 'text-right',
formatter: (value, key, item) => {
return value ? (value + '%') : ''
},
},
],
// Current Switch Settings
switches: {},
// Current Device statuses
devices: {},
}}, // --- End of data --- //
// computed: dynamic data, used as {{ cName }} - cached
computed: {
// Set the FG & BG of the demand card if demand is on
qDemandBg: function() {
return this.demandOnOffOutput === 'On' ? 'primary': ''
},
qDemandFg: function() {
return this.demandOnOffOutput === 'On' ? 'white': ''
},
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger': ''
},
// colour the demand bar depending on demand level
demandLevel: function() {
let a = null
switch (true) {
case ( typeof this.percentageDemand === 'string' ):
a = 'dark';
break;
case this.percentageDemand <= 30:
a = 'success';
break;
case this.percentageDemand <= 60:
a = 'warning';
break;
default:
a = 'danger';
break;
}
return a
},
// If a room has boost turned on
isBoostedFill: function() {
return this.isBoosted ? 'fill:#dc3545' : 'fill:#ffc107'
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
}, // --- End of computed --- //
// methods:
methods: {
/** Return a setInterval timer for the heating update warning
* @callback cb setInterval function
* @param {number} timeout The timeout to be passed to the setInterval fn. Optional, defaults to 2 minutes.
* @returns {cb} setInterval function
*/
heatingUpdTimer: function(timeout=120000) {
const viewApp = this
return setInterval(function(){
//console.log('Vue:methods:heatingUpdTimer heating update not received in 2 minutes')
viewApp.showNoUpdAlert = true
}, timeout)
},
/** Handle row-clicked event on rooms table
* @param {Object} item The Row data for the clicked row
* @param {number} index The row index for the clicked row
* @param {Object} event The click event data
**/
// TODO: need separate array to maintain display state
onRoomsRowClicked (item, index, event) {
item._showDetails = !item._showDetails
},
// Filter Fn for heating room table - @see https://bootstrap-vue.js.org/docs/components/table#filtering
currentRoomsTblFilter: function(item) {
if ( item.CalculatedTemperature ) return true
else return false
},
/** Invoked when user changes tab - saves current tab - @see mounted
* @param {number} i Selected tab index number
*/
changeTab: function(i) {
// Save to browser's session storage
sessionStorage.currentTab = i
},
// Format date/time
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
// return formatted HTML version of JSON object
syntaxHighlight: function(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
} // --- End of syntaxHighlight --- //
}, // --- End of methods --- //
// Available hooks: init,mounted,updated,destroyed
mounted: function(){
uibuilder.debug(false) // output uibuilderfe debug messages
//console.debug('Vue:mounted - setting up uibuilder watchers')
// Save confusion by keeping a specific reference to this Vue app
const vueApp = this
// Start countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.hTimer = vueApp.heatingUpdTimer()
// On-load Reset the current tab to the one saved in session storage - strange, stored as number but retrieves as a string
vueApp.tabIndex = Number(sessionStorage.currentTab)
// If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
// Note that you can also listen for 'msgsReceived' as they are updated at the same time
// but newVal relates to the attribute being listened to.
uibuilder.onChange('msg', function(newVal){
//console.debug('Vue:mounted:UIBUILDER: property msg changed! ', newVal)
vueApp.msgRecvd = newVal
// What kind of message did we receive?
// Use getProp so we don't pollute the original input. Then tidy the topic
let topic = getProp(newVal, 'topic').replace(/\/SWITCH..$/,'')
if ( topic.substring(0,8) === 'DEVICES/' ) topic = 'DEVICES'
switch (topic) {
// Full homeDetails
case 'Home Details':
//console.debug('UIBUILDER:onChange:msg: homeDetails msg received ', newVal)
/** To update the home details, we are expecting a msg like:
* msg = {
* 'topic' : 'Home Details',
* 'payload' : {
* 'homeDetails': homeDetails, // ARRAY
* 'demand' : demand, // OBJECT
* 'lastUpdate': new Date(),
* },
* }
*/
// for convenience
const data = newVal.payload
// Formatted last update
vueApp.lastUpdate = vueApp.fmtTime(data.lastUpdate)
// clear and restart countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.showNoUpdAlert = false; clearInterval(vueApp.hTimer); vueApp.hTimer = null;
vueApp.hTimer = vueApp.heatingUpdTimer()
vueApp.demand = data.demand
// for convenience ...
vueApp.percentageDemand = data.demand.PercentageDemand
vueApp.demandOnOffOutput = data.demand.DemandOnOffOutput
vueApp.isBoosted = data.demand.isBoosted
//vueApp.qDemand = data.demand.DemandOnOffOutput === 'On' ? true : false
//vueApp.HeatingRelayState = data.demand.HeatingRelayState
//vueApp.IsSmartValvePreventingDemand = data.demand.IsSmartValvePreventingDemand
// Sorted array of home data
vueApp.homeData = data.homeDetails
// vvv NB: The below adds the _showDetails field TOO LATE for it to be
// correctly responsive - now added at source
// Add _showDetails:false to all members of the array for the table display
//vueApp.homeData.map(item => {item._showDetails = false; return item;})
// TODO: Should we null/delete the newVal var? Or would that kill vueApp.homeData as well?
break;
// Individual switch update
case 'COMMAND':
//console.debug('UIBUILDER:onChange:msg: COMMAND/SWITCHnn msg received ', newVal)
let sw = newVal.topic.replace('COMMAND/','')
vueApp.switches[sw].status = newVal.payload
break;
// Full switch update
case 'SWITCHES':
vueApp.switches = newVal.payload
break;
// Individual device update
case 'DEVICES':
//console.debug('UIBUILDER:onChange:msg: DEVICES/+ msg received ', newVal)
let dev = newVal.topic.replace('DEVICES/','')
vueApp.devices[dev].status = newVal.payload
break;
// Full devices update
case 'DEVICESFULL':
vueApp.devices = newVal.payload
break;
// Don't process default
default:
//ignore
}
}) // ---- End of uibuilder.onChange() watcher function ---- //
} // --- End of mounted hook --- //
}) // --- End of app1 --- //
// EOF
pre { background-color: #212121 !important; color: wheat;}
.tcentre { text-align: center; }
.uk-table th {
position: fixed; top: 0; z-index: 1;
background-color:black; color:#EEEEEE;
}
pre .string { color: orange; }
.number { color: white; }
.boolean { color: rgb(20, 99, 163); }
.null { color: magenta; }
.key { color: #069fb3;}
card-l-primary, l-primary, btn-l-primary {background-color: #73b7ff !important;}
.nodisplay { display: none; }
Please feel free to add comments to the page (clearly mark with your initials & please add a commit msg so we know what has changed). You can contact me in the Discourse forum, or raise an issue here in GitHub! I will make sure all comments & suggestions are represented here.
-
Walkthrough 🔗 Getting started
-
In Progress and To Do 🔗 What's coming up for uibuilder?
-
Awesome uibuilder Examples, tutorials, templates and references.
-
How To
- How to send data when a client connects or reloads the page
- Send messages to a specific client
- Cache & Replay Messages
- Cache without a helper node
- Use webpack to optimise front-end libraries and code
- How to contribute & coding standards
- How to use NGINX as a proxy for Node-RED
- How to manage packages manually
- How to upload a file from the browser to Node-RED
-
Vanilla HTML/JavaScript examples
-
VueJS general hints, tips and examples
- Load Vue (v2 or v3) components without a build step (modern browsers only)
- How to use webpack with VueJS (or other frameworks)
- Awesome VueJS - Tips, info & libraries for working with Vue
- Components that work
-
VueJS v3 hints, tips and examples
-
VueJS v2 hints, tips and examples
- Dynamically load .vue files without a build step (Vue v2)
- Really Simple Example (Quote of the Day)
- Example charts using Chartkick, Chart.js, Google
- Example Gauge using vue-svg-gauge
- Example charts using ApexCharts
- Example chart using Vue-ECharts
- Example: debug messages using uibuilder & Vue
- Example: knob/gauge widget for uibuilder & Vue
- Example: Embedded video player using VideoJS
- Simple Button Acknowledgement Example Thanks to ringmybell
- Using Vue-Router without a build step Thanks to AFelix
- Vue Canvas Knob Component Thanks to Klaus Zerbe
-
Examples for other frameworks (check version before trying)
- Basic jQuery example - Updated for uibuilder v6.1
- ReactJS with no build - updated for uibuilder v5/6
-
Examples for other frameworks (may not work, out-of-date)
-
Outdated Pages (Historic only)
- v1 Examples (these need updating to uibuilder v2/v3/v4/v5)