Nodered table on bootstrap

Hi guys,

I'm trying to create an editable table using Uibuilder and Bootstrap.
I've added a sample sqlite table from nodered using axios.get. I needed to make the table editable by adding links to the cells, showing forms to edit the table contents and create new row using the add new button. I'm also attaching my code. Thanks

Can anyone please guide me with this?
Thank you

<template>
    <b-container id="app_container" class="mt-5">
    <div class="container mt-5"> 
        <!--margin-top:3%;margin-left:0%;margin-right:5%;-->
        <div  class="col-md-25">
            <button class="btn btn-primary float-right">Add New Expense </button>    
        </div>
        <h4 style="margin-top:2%;margin-bottom:2%;margin-right:5%;">Expenses Table</h4> 
        <b-row style="align-self:start;">
            
        <b-col> 
            <div id="content">

            <b-table id="control" 
                     class="table" 
                     bordered 
                     hover 
                     condensed 
                     :items="items"
                     small 
                     sort-by="Expenses_ID"
                     thead-class="navy text-white">
                     <tbody>
                        {% for items in items %}
                            <tr>
                                <th scope="row">{{ items.Expense_Id }}</th>
                                <td href="#">{{ items.Expense_Name }}</td>
                                <td href="#">{{ items.Expense_Amount }}</td>
                                <td href="#">{{ items.Expense_DateTime }}</td>
                                <td>
                                    <button type="button" class="btn btn-outline-primary"
                                        data-s-toggle="modal" data-target="#Modal-MoreInfo">
                                        More Info
                                    </button>
                                </td>
                            </tr>
                        {% endfor %}
                     </tbody>  
            </b-table>
            </div>
        </b-col>
        </b-row>
    </div>
    </b-container>
</template>
<!-- Disabled since it doesn't work for SO snippets.-->
<style scoped>
.navy, .table thead th, thead, th{
    background-color:#13294b !important;
    }
</style>
<script>

module.exports = {
    data() {
        return {             
        items: [],
        fields:[]
        };
    },
    mounted(){
        this.retrieveData();
    },
    computed: {
      rows() {
        return this.items.length
      }
    },
    methods: {
        retrieveData(){
            axios.get('/tuts/expenses')
            .then((resp) => { 
                //console.log(resp.data)
                this.items = resp.data;
                //this.currentPage = this.resp.data.length;
            })
            .catch(errors => { console.error(errors); });;
        }
    }, 
}
</script>


index.html

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

   
    <meta name="description" content="Node-RED UI Builder - Blank template">
    <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 type="text/css" rel="stylesheet" href="./index.css" media="all">

</head><body>
<div id="app">
    
    <myheader>
    </myheader>
    <b-container fluid>
        <router-view></router-view>
    </b-container>
     </div>
    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="../uibuilder/vendor/vue/dist/vue.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue-icons.js"></script>
    <script src="../uibuilder/vendor/http-vue-loader/src/httpVueLoader.js"></script>
    <script src="../uibuilder/vendor/vuex/dist/vuex.js"></script>
    <script src="../uibuilder/vendor/vue-router/dist/vue-router.js"></script>
    <script src="../uibuilder/vendor/axios/dist/axios.js"></script>
    <script src="./uibuilderfe.min.js"></script>
    <script src="./index.js" type="module"></script>

</body></html>

router.js

What is this? That isn't bootstrap-vue. Instructions for b-table are here: Table | Components | BootstrapVue (bootstrap-vue.org)

You can use cell templates to replace the normal cell formatting with an input field and you can create a custom column with control buttons for add/delete/edit of each row. Depends on how you want to do things.

There are also additional modules that will enable editable tables. An internet search will show several options. bootstrap vue editable table - Google Search

There is a bootstrap-vue forum as well so you might want to look there for bootstrap-vue help. Can I make an editable table on Bootstrap-Vue? - Get Help - Vue Forum (vuejs.org)

1 Like

I tried getting the values from the table with this. couldn't get it to work.
{% for items in items %}

creating a link from the cell of the table is not actually working, its going into "localhost:1880/(cell value)" instead of actually going into the cell value. Getting through that slowly.

creating a form was another task i'm havng trouble with, cause i ned the form to be prefilled with the row data to change it and sending it back to Node-RED.
@TotallyInformation can you plz guide me in how do get those link woking and the forms?

Well it wouldn't, it isn't valid VueJS code. I shared the link to the bootstrap-vue documentation for b-table.

You should get things working on a simple page first. Introducing a router adds complexity. This WIKI page has some info on using Vue Router (Vue v2).
Using Vue Router · TotallyInformation/node-red-contrib-uibuilder Wiki (github.com)

Not sure what you mean by "going into the cell value"?

Not really, I've not really used bootstrap-vue for a while except to update uibuilder's examples. But here are some snippets from an old heating dashboard that I no longer use. For help with bootstrap-vue, I recommend their forum.

<b-table hover head-variant="dark" sort-by="room" :fields="battFields" :items="Object.values(batteryStatus)">
    <template #cell(BatteryVoltage)="data">
        {{ data.value.toFixed(1) }}
    </template>
</b-table>
/* jshint browser: true, asi:true */
/* globals Vue, uibuilder */
'use strict'

// @ts-ignore
new Vue({
    el: '#app',
    
    data: {
        
        batteryStatus: {},
        battFields: [
            "room",
            { key: 'device_id', tdClass: 'text-center', thClass: 'text-center' },
            { key: 'BatteryVoltage', tdClass: 'text-center', thClass: 'text-center' },
            { key: 'BatteryLevel', tdClass: 'text-center', thClass: 'text-center' },
            { key: 'RSSI', tdClass: 'text-center', thClass: 'text-center' },
        ],
        wiserConfig: {
            batteryLow: false,
            voltageLevels: {
                // n-2.7=one third, 2.8=two thirds, 2.9+=normal
                min: 2.1,
                danger: 2.1,
                warn: 2.8,
            },
            controllerStatus: {},
        },
        wiserFull: {},
        wiserDiff: {},

        // Vue-Tabulator/tabulator-tables
        roomdata: [
        ],
        rdfields: [
        ],

        tabIndex: 0,
        
        //#region debug
        startMsg    : 'Vue has started, waiting for messages',
        feVersion   : '',
        counterBtn  : 0,
        inputText   : null,
        inputChkBox : false,
        socketConnectedState : false,
        serverTimeOffset     : '[unknown]',
        imgProps             : { width: 75, height: 75 },

        msgRecvd    : '[Nothing]',
        msgsReceived: 0,
        msgCtrl     : '[Nothing]',
        msgsControl : 0,

        msgSent     : '[Nothing]',
        msgsSent    : 0,
        msgCtrlSent : '[Nothing]',
        msgsCtrlSent: 0,
        //#endregion
        
    }, // --- End of data --- //
    
    computed: {        
        hLastRcvd: function() {
            var msgRecvd = this.msgRecvd
            if (typeof msgRecvd === 'string') return 'Last Message Received = ' + msgRecvd
            else return 'Last Message Received = ' + this.syntaxHighlight(msgRecvd)
        },
        hLastSent: function() {
            var msgSent = this.msgSent
            if (typeof msgSent === 'string') return 'Last Message Sent = ' + msgSent
            else return 'Last Message Sent = ' + this.syntaxHighlight(msgSent)
        },
        hLastCtrlRcvd: function() {
            var msgCtrl = this.msgCtrl
            if (typeof msgCtrl === 'string') return 'Last Control Message Received = ' + msgCtrl
            else return 'Last Control Message Received = ' + this.syntaxHighlight(msgCtrl)
        },
        hLastCtrlSent: function() {
            var msgCtrlSent = this.msgCtrlSent
            if (typeof msgCtrlSent === 'string') return 'Last Control Message Sent = ' + msgCtrlSent
            //else return 'Last Message Sent = ' + this.callMethod('syntaxHighlight', [msgCtrlSent])
            else return 'Last Control Message Sent = ' + this.syntaxHighlight(msgCtrlSent)
        },
    }, // --- End of computed --- //
    
    methods: {
        /** Convert an ISO8601 timestamp string into humanised format
         * @param {string} isoTs "2020-11-01T20:28:39.902Z"
         * @return {string} Humanised format of input string
         */
        humanTimestamp: function(isoTs) {
            if (isoTs) return isoTs.replace('T', ' ').slice(0,16)
        },

        /** Convert a duration in ms to humanised format
         * @param {number} ms Duration in milliseconds
         * @returns {string} Humanised duration string
         */
        duration: function(ms) {
            let out = '--' //-1
            const t = {}
            if (ms) {
                //out = Math.floor(ms/60000)
                var seconds = ms/1000
                t.y = Math.floor(seconds / 31536000)
                t.d = Math.floor((seconds % 31536000) / 86400)
                t.h = Math.floor(((seconds % 31536000) % 86400) / 3600)
                t.m = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
                t.s = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60)
                //console.log('t',t)
                out = t.y>0 ? t.y + ' y ' : ''
                out += t.d>0 ?  t.d + ' d ' : ''
                out += t.h.toString().padStart(2,'0') + ':'
                out += t.m.toString().padStart(2,'0') + ':'
                out += t.s.toString().padStart(2,'0') + ''
            }
            //return `Duration ${out<0 ? '--' : out} minutes`
            return `Duration ${out}`
        },
        /** An alternative to the above */
        msToTime: function(ms) {
            var seconds = (ms/1000);
            var minutes = parseInt(seconds/60, 10);
            seconds = seconds%60;
            var hours = parseInt(minutes/60, 10);
            minutes = minutes%60;
            var days = parseInt(hours/24, 10);
            hours = hours%24;
            
            return days + 'd, ' + hours + ':' + minutes + ':' + seconds;
        },

        /** */
        signalStr: function(rssi) {
            return `${rssi} (${this.wiserConfig.controllerStatus.rssiMin} <-> ${this.wiserConfig.controllerStatus.rssiMax})`
        },
        
        /** Handle button click to reset all rooms back to their default (scheduled) state
         *  NB: Can use $event in html if needing to also pass other data.
         *      Make sure you add an id attribute to the button, it is passed back as the topic to Node-RED
         * @param {MouseEvent} evt 
         */
        changeRoom: function(evt) {
            /** resetAllRooms, emmaWorking, julianWorking */
            const chgType = evt.path[0].id

            console.log({chgType, evt})

            uibuilder.send({
                'topic': chgType,
                'payload': evt,
            })
        }, // --- End of resetAllRooms --- //

        /** When a tab change event occurs, save the new tab to localstorage
         *  so that it can be reloaded on startup.
         */
        saveActiveTab: function(newTab) {
            // localStorage.setItem('todos', JSON.stringify(this.todos))
            localStorage.setItem('currentTab', newTab)
        }, // --- end of saveActiveTab --- //

    }, // --- End of methods --- //

    // Available hooks: beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed, activated,deactivated, errorCaptured
    created: function() {
        this.feVersion = uibuilder.get('version')
        //console.log('uibuilderfe version: ', this.feVersion)

        // only required for the old uibuilderfe client, not valid for the new client.
        uibuilder.start(this) // Single param passing vue app to allow Vue extensions to be used.
        
         // Restore current tab to the last used on when the page (re)loads
        this.tabIndex = Number(localStorage.getItem('currentTab'))
    },
    
    mounted: function(){
        const app = this

        /** If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
         *  newVal relates to the attribute being listened to.
         */
        // @ts-ignore
        uibuilder.onChange('msg', function(msg){

            app.msgRecvd = msg // debug

            //console.log('msg',msg.topic,msg)

            if ( msg.topic.startsWith('wiser/battery') || msg.topic.startsWith('wiser/signals') ) {
                doBattery(msg, app)
            } else if ( msg.topic === 'controllerStatus' ) {

                app.wiserConfig.controllerStatus = msg.payload

            } else if ( msg.topic === 'Heating_Diff' ) {

                console.log('msg::Heating_Diff', msg)
                app.wiserDiff = msg.payload
                _.merge(app.wiserFull, app.wiserDiff)

            } else if ( msg.topic === 'wiser_full' ) {

                console.log('msg::wiser_full', msg)
                app.wiserFull = msg.payload

            } else {
                console.log('Unmanaged msg', msg)
            }

        }) // --- End of onChange --- //

    } // --- End of mounted hook --- //

}) // --- End of new Vue --- //

// ===== Utility Functions ===== //
function doBattery(msg,app) {
    //console.log('msg::doBattery', {msg,app})

    // NB: If payload === '', the specific topic has been deleted
    //     Topics can contain blanks
    
    const topicSplit = msg.topic.replace('wiser/','').replace('battery/','').replace('signals/','').split('/')
    const room = topicSplit[0]
    const deviceId = topicSplit[1]
    
    let type
    if ( topicSplit[2] ) type = topicSplit[2]
    else type = 'RSSI'

    if ( ! app.batteryStatus[`${room}-${deviceId}`] ) {
        app.batteryStatus[`${room}-${deviceId}`] = {
            room: room,
            device_id: deviceId,
        }
    }
    let v = msg.payload
    if ( type === 'BatteryVoltage') {
        v = parseFloat(msg.payload)
        // n-2.7=one third, 2.8=two thirds, 2.9+=normal
        if ( v < app.wiserConfig.voltageLevels.danger ) { //2.1
            app.batteryStatus[`${room}-${deviceId}`]._rowVariant = 'danger'
        } else if ( v < app.wiserConfig.voltageLevels.warn ) {
            app.batteryStatus[`${room}-${deviceId}`]._rowVariant = 'warning'
        } else {
            app.batteryStatus[`${room}-${deviceId}`]._rowVariant = null
        }
    }
    if ( type ) app.batteryStatus[`${room}-${deviceId}`][type] = v

    // Check to see if any batteries are low and add warning flag to tab name if so
    let batChk = false
    Object.values(app.batteryStatus).forEach( item => {
        if ( item.BatteryVoltage < app.wiserConfig.voltageLevels.min ) batChk = true
    })
    if ( batChk === true ) app.wiserConfig.batteryLow = true
    else app.wiserConfig.batteryLow = false

    //console.log(app.batteryStatus)

} // --- end of doBattery() --- //

// EOF