UIBuilder - VueJS not reacting to modifed data

I’m building a UI using UIBuilder with Vue, Bootstrap-Vue. A weird issue has cropped up; I’m not sure if its a issue with my code, UIBuilder, Vue, Bootstrap, etc. The UI code has gotten a bit complex, so rather than posting all the code, flow, etc I’m going to try to describe whats happening and post code excerpts.

The hardware system is Rasp Pi 4 with Node Red, UIBuilder, Vue, Bootstrap-Vue, and a PLC, which communicate over ethernet using MQTT & UDP. The data is being communicated as expected, and in fact the system works fine until I introduce the Computed property code described below.

I am new to UIBuilder, Vue, etc. I started the UI with simple features, adding more as I learned and correcting mistakes with the previous features.

The UI consists of Radio Buttons that set a value to be used as a data filter, and several b-tabs each with tables that list the filtered data.

The PLC currently sends two data arrays to Node Red, both Arrays of JSON Objects. These arrays are stored in Global Context. In earlier, simpler versions of the UI, these arrays simply loaded into elements.

On RowSelect, a copy of the data in the selected row is sent to an editable form and a "Save" button sends the modifed data from the form to the PLC via uibulder.send. The PLC receives the modified data, and sends it back to Node Red which updates the Global Context and sends the update to UIBuilder. UIBuilder/Vue/Bootstrap reactively updates the associated table as expected. So far, its all good. To be clear, I'm not modifying the original data array but copying that data from the selected row to a form. The update to the array occurs when Node Red receives it from the PLC.

I then added filtering to the tables. Initially, I used filtering property with a filter-function. All worked fine with this also, but I needed one more level of filtering. What I ended-up doing was writing a Computed property to build a new, filtered array and then filtering again with a second property using filtering property. All is working fine at this point: data can be edited, saved/sent to the PLC, received back, global context is updated and table in UI reactively updates.

You'll see in "mounted > uibuilder.onChange" the two arrays of JSON objects. The data is different, but the functionally is essentially the same. When the Radio Button is selected, both arrays need to be filtered by the same property using a Computed property. Then, each filtered array is then loaded into a separate table. Both arrays are edited and save/send the modifications to the PLC the same way, and the PLC returns the data to Node Red which stores it in Global Context. The only difference, other than the data itself, is that arrScene is filtered again using filter-function as described above.

Filtering arrSBank using Computed property to create the filtered ftrSBanksByZN array is were the issue is introduced, but I'm not sure why. With both data arrays filtered using their Computed property, when I edit/save/send the data in the ftrScenesbyZN array (filtered version of arrScene), the tbSNlist table does not reactively update with the modifed data. The modifed date is being sent to the PLC, the PLC is sending it back and Node Red is storing the updated data in Global Context. Removing the second filter (b-table filter-function), doesn't eliminate the issue. Neither the arrScene array or its filtered znScene array is updating with the modified data.

Here's where it gets weird. If I remove the Computed property for ftrSBanksByZN, and change the :items in tbSBlist to arrSBanks (the unfiltered array), then the data for arrScene reactively updates as expected, as does the Computed property ftrScenesbyZN. If I add back the Computed property for ftrSBanksByZN, the arrScene does not update (but the updated data is in the Global context). To be clear, the Computed property ftrSBanksByZN seems to be triggering the issue with arrScene not updating but I haven't figured out what the nexus is.

I think the issue may be in "mounted > uibuilder.onChange" but I'm not sure. Maybe its an issue with my coding, or maybe a bug in bootstrap-vue.

(Code Excerpts)

<!-- Table Scenes-->
<b-table	id = "tbSNlist"
		selectable select-mode="single"
        :items="ftrScenesbyZN" 
        :filter="tab" :filter-function="filterDevice"
        @row-selected="onSelect">
</b-table>

<!-- Table Sbank-->
<b-table	id = "tbSBlist"
		selectable select-mode="single"
		:items="ftrSBanksByZN "
		@row-selected="onSelect_sb">
</b-table>


var app = new Vue({
    // The HTML element to attach to
	el: '#app',
	
	data() {
		return {
			arrSBank:[],
			arrScene: [],			
		};
	},
	
	computed: {
		ftrScenesbyZN: function () {
				let scenz = this.arrScene; //data array to be filtered
				let znSelect1 = this.znSelect[0]; //property to be filtered, based on radio button selection
				znScenesA = scenz.filter(scene =>{
					return scene.zone == znSelect1;
				});
				return znScenesA;
		 },

		ftrSBanksByZN : function () {
				let sbnk = this.arrSBank; //data array to be filtered
				let znSelect2 = this.znSelect[0]; //property to be filtered, based on radio button selection
				znSBanksA = sbnk.filter(bank =>{
					return bank.sw_zone == znSelect2;
				});
				return znSBanksA;
		  },
	},

	mounted: function() {     // Called when Vue is fully loaded
		
		uibuilder.onChange('msg', function(msg) {
		// Triggered when the node on the Node-RED server recieves a (non-control) msg
			app.arrScene = msg.scenes; 
			app.arrSBank = msg.sbanks;
		});
	},
});

Hi, hope you don't mind but I tweaked the title to show that it is VueJS that isn't responding rather than uibuilder.

Really, this is more of a VueJS question & I'm not actually that expert at VueJS so I'm not sure I can really help that much I'm afraid.

Usually, problems with reactivity are due to handing complex objects that haven't been defined as such up front (in data). In those cases, Vue can't detect that something deep inside the object has changed and therefore doesn't respond. There are various ways to avoid this issue including a function that forces Vue to understand what is changing. You will find the documentation for these on the Vue docs site though it may take you a few readings to fully understand - it certainly did me and I still don't really know if I actually understand it properly. :frowning:

1 Like

No problem, and thank you for responding. I did see the "reactivity in depth" article, but declaring the properties didn't work. I wasn't sure if it was a vue issue or what so I thought I start here. The weird thing is the subject data updates if I remove an mostly unrelated computed property.

Hello ..

would it be possible to share some sample data ? (enough to show the issue when filters are applied)
also i dont see in your code a methods section with the onSelect and onSelect_sb functions or the radio buttons. please share a more workable code excerpt in order to try to replicate this

FYI: I'm using the latest uibuilder version with Vue and it works fine for me but I had a similar issue with apexchart v3.30.0 not rendering. I went back to v3.22.2 and this solved it.

Thank you for offering to replicate the issue. I was only showing excerpts of the code, becuase the way data bounces back and forth between devices I thought it might be challenging to replicate. But acutally I guess one could use Inject nodes. I think I may refactor the code first, and possibly try writing the computed properties differently to see if that works (i'm not confident about using fat arrows and other short-hand)

1 Like

I'm not sure but In your function mounted there is no uibuilder.start()

	mounted: function() {     // Called when Vue is fully loaded
		
        uibuilder.start()

		uibuilder.onChange('msg', function(msg) {
		// Triggered when the node on the Node-RED server recieves a (non-control) msg
			app.arrScene = msg.scenes; 
			app.arrSBank = msg.sbanks;
		});
	},

Now best practice to put the start into the created property instead. Also best practice to include this as a sole parameter to start because that tells the uib library that you are using VueJS and gives it access. Then you can use things like the Toast notification helper

    /** Called after the Vue app has been created. A good place to put startup code */
    created: function() {

        uibuilder.start(this) // Single param passing vue app to allow Vue extensions to be used.

    }, // --- End of created hook --- //

Assuming, of course, you are using a current version of uibuilder :grinning:

In data(), if I comment-out arrSBank:[] (see below), the computed function updates as expected. If I also comment-out arrScene:[], the page doesn't render at all. In other words, including arrSBank in Data() appears to have been preventing the computed property from updating.

(this following is just an excerpt from my code)

var app = new Vue({
    // The HTML element to attach to
	el: '#app',
	
	data() {
		return {
			// arrSBank:[],
			arrScene: [],			
		};
	},
	
	computed: {
		ftrScenesbyZN: function () {
				let scenz = this.arrScene; //data array to be filtered
				let znSelect1 = this.znSelect[0]; //property to be filtered, based on radio button selection
				znScenesA = scenz.filter(scene =>{
					return scene.zone == znSelect1;
				});
				return znScenesA;
		 },

		ftrSBanksByZN : function () {
				let sbnk = this.arrSBank; //data array to be filtered
				let znSelect2 = this.znSelect[0]; //property to be filtered, based on radio button selection
				znSBanksA = sbnk.filter(bank =>{
					return bank.sw_zone == znSelect2;
				});
				return znSBanksA;
		  },
	},

	mounted: function() {     // Called when Vue is fully loaded
		
		uibuilder.onChange('msg', function(msg) {
		// Triggered when the node on the Node-RED server recieves a (non-control) msg
			app.arrScene = msg.scenes; 
			app.arrSBank = msg.sbanks;
		});
	},
});

You will need to provide a couple of example msgs if you want us to help further.

However, here is an example of a filtered table - it shows all of the devices ever seen on my network.

<b-table 
    striped hover small bordered caption-top
    :items="devs.items"         :fields="devs.cols" :primary-key="devs.key"
    :busy="devs.busy"        :head-row-variant="devs.headRowVariant"
    :sort-by="devs.key"        :filter="devs.filter"
    :per-page="devs.perPage" :current-page="devs.currentPage"
    @filtered="onFiltered"   @row-dblclicked="rowDblClicked"
    v-model="devs.model"
>

Notice that my items is plain data not a method and that filtering is done separately.

Here is the full js:

var vueApp = new Vue({
    el: '#app',
    data: {

        // Devices table
        devs: {
            items: [], // table content
            cols: [],  // column definitions
            key: '',   // primary key field name
            headVariant: 'dark',
            headRowVariant: 'primary',
            busy: false, // Table busy?
            perPage: 10,
            currentPage: 1,
            totalRows: 1,
            filter: null,
            showEdit: false,
            inlineEdit: true, // Allow edits of editable fields in-line?
            /** Holds visible items from devs.items (b-table v-model)
             * Indexing on the model matches the index returned by b-table
             */
            model: null,
        },
        editFrm: {},

    }, // --- End of data --- //
    computed: {

        frmCol: function(chg, index, column, event) {
            console.log({chg, index, column, event})
            return devices
        },
        
    }, // --- End of computed --- //
    methods: {

        /** Called from modal form
         * NOTE: If no params were passed, the system would provide the new value
         */
        cellEdit(chg, row, event, devsModel) {
            console.log({chg, row, event, devsModel})
            //devsModel[row.index][row.field.key] = event.target.value
            uibuilder.send({topic: 'admin/change', payload: devsModel[row.index]})
        },

        // Trigger pagination to update the number of buttons/pages due to filtering
        rowDblClicked(item, index, event) {
            console.log({item, index, event}, this.devsModel)
            this.editFrm = item
            this.devs.showEdit = true // !this.devs.showEdit
        }, // --- End of onFiltered --- //

        /** Handle table edit button
         * @param {Object} item The data content of the row being edited
         * @param {number} index The filtered/paginated row index (not the full data index)
         * @param {Object} event The event object
         * @param {Object} devsModel Reference to the table v-model data (FILTERED)
         */
        tblRowEdit(row, event, devsModel) {
            /** editFrm referenced by input's in form
             * maps to table data model.
             */
            this.editFrm = row.item
            this.devs.showEdit = true
            console.log('Table Row Edit Button Pressed', {row, event, devsModel})
        }, // --- End of tblRowEdit --- //
        frmEdit: function(chg, index, column, event) {
            const value = event.target.value
            console.log({chg, value, index, column, event})
            //this.$emit(chg, { ...this.local, [key]: value })
        },
        // Handle OK button on edit modal dialog
        modalEditOk(bvModalEvt) {
            console.log('Edit Modal Dialog: OK Button Pressed', 'DATA: ', this.editFrm)
            // Update main data table
            this.devs.items[this.editFrm._idx] = this.editFrm
            // Send to NR
            uibuilder.send(this.editFrm)
            // If error, prevent modal from closing
            //bvModalEvt.preventDefault()
        }, // --- End of tblItemAdd --- //

        // Handle table edit button
        tblRowDelete(item, index, event) {
            console.log('Table Row Delete Button Pressed', {item, index, event})
        }, // --- End of tblRowDelete --- //

        // Handle table add new button
        tblItemAdd(data, event) {
            this.devs.showEdit = true
            console.log('Table Item Add Button Pressed', {data, event})
        }, // --- End of tblItemAdd --- //

        // Trigger pagination to update the number of buttons/pages due to filtering
        onFiltered(filteredItems) {
            this.devs.totalRows = filteredItems.length
            this.devs.currentPage = 1
        }, // --- End of onFiltered --- //

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

    // Available hooks: init,created,mounted,updated,destroyed

    /** Called after the Vue app has been created. A good place to put startup code */
    created: function() {

        uibuilder.start(this) // Single param passing vue app to allow Vue extensions to be used.

    }, // --- End of created hook --- //

    mounted: function(){
        var vueApp = this

        // Example of retrieving data from uibuilder
        vueApp.feVersion = uibuilder.get('version')

        uibuilder.onChange('msg', function(msg){
            //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)
            vueApp.msgRecvd = msg

            // May need to use Vue.set(vm.items, indexOfItem, newValue)

            if (msg.topic === 'network') {
                vueApp.devs.busy = true
                console.log('NEW NETWORK DATA RECEIVED')

                //#region ------ COLUMNS ---- //
                // Grab the schema to define the columns
                if ( msg.schema ) {
                    // Convert from object to array if needed
                    if ( Array.isArray(msg.schema) ) vueApp.devs.cols = msg.schema
                    else vueApp.devs.cols = Object.values(msg.schema)
                }

                // Enrich the columns definitions as required
                vueApp.devs.cols.forEach(function (column, i, arr) {
                    // If col not specified as object, make it one
                    if ( ! (column != null && column.constructor.name === "Object") ) column = arr[i] = {'key': column}
                    // Add sortable to column spec unless already specified
                    if (! column.sortable) column.sortable = true
                    // Default headerTitle addrib to key if not present
                    if (column.label && !column.headerTitle) column.headerTitle = column.key
                    // Set primary key for table
                    if (column.primary) {
                        vueApp.devs.key = column.key
                        column.headerTitle += ' [Primary Key]'
                    }
                    //TODO Format or dataType specified?
                    if ( column.dataType.toLowerCase() === 'date' && !column.formatter ) {
                        column.dataType = 'text'
                        column.formatter = function(value, key, item) {
                            if (value === '') {
                                var d = new Date('fred')
                                consonsole.log(d)
                                return ''
                            } else {
                                var d = new Date(value)
                                return d.toLocaleString()
                            }
                        }
                    }
                    // if ( column.format || column.dataType ) {

                    // }
                    //TODO Editable?
                    if (column.editable) {
                        column.headerTitle += ' (EDITABLE)'
                    }

                })

                // If col definitions don't specify a primary key, choose the 1st col
                if (vueApp.devs.key === '') {
                    vueApp.devs.key = vueApp.devs.cols[0].key
                    vueApp.devs.cols[0].headerTitle += ' [Primary Key]'
                }

                // Add actions column WARN: Assumes cols table rebuilt so action col not exist
                vueApp.devs.cols.push({
                    key: 'actions',
                    label: '',
                    sortable: false,
                })

                //#endregion ---- COLUMNS ---- //

                //#region ------- DATA ------- //

                // Set the data (convert from object to array if needed)
                if ( Array.isArray(msg.payload) ) vueApp.devs.items = msg.payload
                else vueApp.devs.items = Object.values(msg.payload)
                vueApp.devs.items.forEach(function (row, i) {
                    // Add an index field to make editing easier when using filtered view
                    row._idx = i
                })
                // Set row count for pagination
                vueApp.devs.totalRows = vueApp.devs.items.length

                //#endregion ----- DATA ----- //

                //console.log(vueApp.devCols,vueApp.devices, vueApp.devs.key)
                vueApp.devs.busy = false
            } // --- End of devices --- //

        }) // ----- End of msg received ----- //

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

}) // --- End of vueApp --- //

This isn't really the "production" flow, just an attempt to make this presentable. The injection nodes are simulations of the messages received from the other hardware controller. If you click the Injection Node "SB-A" (simulating receipt of a message), the browser console indicates there is an issue around line 27 of index.js. I think that's at least part of the issue.

If you click a line in one of the tables, it should open an editable form. On save, it will send (uibuilder.send) the edited object to the debug node. In production, the json object would be sent to the the other controller. The other controller would process the update, and return the updated result to this Node Red. This returned result is what the injection node mentioned above is simulating.

I've been trying different things all day, but so far no joy. Any advice would be appreciated. My "production" version is behaving similarity, but I can attest that the other hardware device is sending and receiving the json messages as expected.

Edit: Improved the Flow. Issue may be related to data types

[
    {
        "id": "773898f99a74b037",
        "type": "tab",
        "label": "PLC Simulator",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "74c68d52933ac6d0",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "PLC-A",
        "props": [
            {
                "p": "plc_in",
                "v": "{\"x\":1}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "plc_in",
        "x": 150,
        "y": 320,
        "wires": [
            [
                "2a41e072020d15db"
            ]
        ]
    },
    {
        "id": "2a41e072020d15db",
        "type": "uibuilder",
        "z": "773898f99a74b037",
        "name": "haConfig",
        "topic": "",
        "url": "haConfig",
        "fwdInMessages": false,
        "allowScripts": false,
        "allowStyles": false,
        "copyIndex": true,
        "templateFolder": "blank",
        "extTemplate": "",
        "showfolder": false,
        "useSecurity": false,
        "sessionLength": 432000,
        "tokenAutoExtend": false,
        "reload": true,
        "sourceFolder": "src",
        "x": 540,
        "y": 340,
        "wires": [
            [
                "76d13b2ea2efec56",
                "42846a895b17d58e"
            ],
            [
                "c0e0b076ef3de7e5",
                "bc4f4c2422b1b7b3"
            ]
        ]
    },
    {
        "id": "4fde8d8c6d5ecfc5",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "SB-A",
        "props": [
            {
                "p": "sbanks",
                "v": "{\"id\":1,\"zone\":1,\"config\":1,\"sw-a\":0,\"sw-b\":0,\"sw-c\":0}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "sbanks",
        "x": 150,
        "y": 180,
        "wires": [
            [
                "d7e75187777ab46a"
            ]
        ]
    },
    {
        "id": "c0e0b076ef3de7e5",
        "type": "function",
        "z": "773898f99a74b037",
        "name": "get Global Data",
        "func": "if (msg.uibuilderCtrl === \"ready for content\") {\n\n    let newMsg = {}\n    \n    newMsg.scenes = global.get('tbSN_ex')\n    newMsg.sbanks = global.get('tbSB_ex')\n    \n    newMsg.plc_in = {\"x\":997}\n    newMsg._socketId = msg._socketId\n\n    return newMsg\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 760,
        "y": 440,
        "wires": [
            [
                "2a41e072020d15db"
            ]
        ]
    },
    {
        "id": "f2f69e79a7cb131e",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "SN-B",
        "props": [
            {
                "p": "scenes",
                "v": "{\"id\":1,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":1,\"bri\":15}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "scenes",
        "x": 150,
        "y": 500,
        "wires": [
            [
                "d15264bc8d4e844b"
            ]
        ]
    },
    {
        "id": "f269242d834d9450",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "SN-A",
        "props": [
            {
                "p": "scenes",
                "v": "{\"id\":1,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":1,\"bri\":255}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "scenes",
        "x": 150,
        "y": 460,
        "wires": [
            [
                "d15264bc8d4e844b"
            ]
        ]
    },
    {
        "id": "ef27294314e9b34c",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "SB-B",
        "props": [
            {
                "p": "sbanks",
                "v": "{\"id\":1,\"zone\":1,\"config\":1,\"sw-a\":8,\"sw-b\":16,\"sw-c\":32}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "sbanks",
        "x": 150,
        "y": 220,
        "wires": [
            [
                "d7e75187777ab46a"
            ]
        ]
    },
    {
        "id": "b12443af76aecf5b",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "PLC-B",
        "props": [
            {
                "p": "plc_in",
                "v": "{\"x\":2}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "plc_in",
        "x": 150,
        "y": 360,
        "wires": [
            [
                "2a41e072020d15db"
            ]
        ]
    },
    {
        "id": "d15264bc8d4e844b",
        "type": "function",
        "z": "773898f99a74b037",
        "name": "set tbSN Global",
        "func": "var tbSN_ex = global.get('tbSN_ex') || [];\n\nvar rID = msg.scenes.id;\nvar rData = msg.scenes\n\ntbSN_ex[rID] = rData;\n\nmsg.scenes = tbSN_ex;\nglobal.set('tbSN_ex',msg.scenes);\n\nnode.warn(msg)\n\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 340,
        "y": 460,
        "wires": [
            [
                "2a41e072020d15db",
                "dc8a250d0b92de66"
            ]
        ]
    },
    {
        "id": "d7e75187777ab46a",
        "type": "function",
        "z": "773898f99a74b037",
        "name": "set tbSB Global",
        "func": "var tbSB_ex = global.get('tbSB_ex') || [];\n\nvar rID = msg.sbanks['id'];\nvar rData = msg.sbanks\n\ntbSB_ex[rID] = rData;\n\nmsg.sbanks = tbSB_ex;\nglobal.set('tbSB_ex',msg.sbanks);\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 340,
        "y": 220,
        "wires": [
            [
                "2a41e072020d15db",
                "cfe4e233376550f7"
            ]
        ]
    },
    {
        "id": "334b9626184d7d2f",
        "type": "function",
        "z": "773898f99a74b037",
        "name": "Init Global Data",
        "func": "// var tbSB_ex = global.get('tbSB_ex') || [];\n// var tbSN_ex = global.get('tbSN_ex') || [];\n\n// msg.sbanks = [{\"id\":0,\"zone\":1,\"config\":1,\"sw-a\":0,\"sw-b\":0,\"sw-c\":0,\"layer\":1,\"state\":1},{\"id\":1,\"zone\":1,\"config\":2,\"sw-a\":16,\"sw-b\":0,\"sw-c\":0,\"layer\":1,\"state\":1}]\nglobal.set('tbSB_ex',msg.sbanks);\n\n// msg.scenes = [{\"id\":0,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":0,\"bri\":0},{\"id\":1,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":1,\"bri\":32},{\"id\":2,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":1,\"state\":0,\"bri\":0},{\"id\":3,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":1,\"state\":1,\"bri\":255},{\"id\":4,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":0,\"bri\":0},{\"id\":5,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":1,\"bri\":255},{\"id\":6,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":0,\"bri\":0},{\"id\":7,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":1,\"bri\":255},{\"id\":8,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":2,\"bri\":64},{\"id\":9,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":2,\"bri\":0}]\nglobal.set('tbSN_ex',msg.scenes);\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 380,
        "y": 40,
        "wires": [
            []
        ]
    },
    {
        "id": "24edb6c2a8088861",
        "type": "inject",
        "z": "773898f99a74b037",
        "name": "Initialize Global",
        "props": [
            {
                "p": "sbanks",
                "v": "[{\"id\":0,\"zone\":0,\"config\":0,\"sw-a\":0,\"sw-b\":0,\"sw-c\":0},{\"id\":1,\"zone\":1,\"config\":1,\"sw-a\":16,\"sw-b\":0,\"sw-c\":0},{\"id\":2,\"zone\":1,\"config\":2,\"sw-a\":18,\"sw-b\":0,\"sw-c\":0}]",
                "vt": "json"
            },
            {
                "p": "scenes",
                "v": "[{\"id\":0,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":0,\"bri\":0},{\"id\":1,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":0,\"state\":1,\"bri\":255},{\"id\":2,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":1,\"state\":0,\"bri\":0},{\"id\":3,\"zone\":1,\"layer\":1,\"type\":1,\"addr\":1,\"state\":1,\"bri\":255},{\"id\":4,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":0,\"bri\":0},{\"id\":5,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":1,\"bri\":255},{\"id\":6,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":0,\"bri\":0},{\"id\":7,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":1,\"bri\":255},{\"id\":8,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":3,\"state\":2,\"bri\":64},{\"id\":9,\"zone\":1,\"layer\":2,\"type\":1,\"addr\":2,\"state\":2,\"bri\":0}]",
                "vt": "json"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "x": 160,
        "y": 40,
        "wires": [
            [
                "334b9626184d7d2f"
            ]
        ]
    },
    {
        "id": "76d13b2ea2efec56",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 710,
        "y": 180,
        "wires": []
    },
    {
        "id": "bc4f4c2422b1b7b3",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 730,
        "y": 480,
        "wires": []
    },
    {
        "id": "f023c1ac6c0ffb8e",
        "type": "uibuilder",
        "z": "773898f99a74b037",
        "name": "simpUI",
        "topic": "",
        "url": "simpUI",
        "fwdInMessages": false,
        "allowScripts": false,
        "allowStyles": false,
        "copyIndex": true,
        "templateFolder": "blank",
        "extTemplate": "",
        "showfolder": false,
        "useSecurity": false,
        "sessionLength": 432000,
        "tokenAutoExtend": false,
        "reload": false,
        "sourceFolder": "src",
        "x": 720,
        "y": 40,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "c1c09b47305fcc37",
        "type": "delay",
        "z": "773898f99a74b037",
        "name": "Simulate PLC",
        "pauseType": "delay",
        "timeout": "2",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 1020,
        "y": 240,
        "wires": [
            [
                "f5d70e8d7b34610f"
            ]
        ]
    },
    {
        "id": "0c08e60c2167e6a0",
        "type": "link out",
        "z": "773898f99a74b037",
        "name": "simScenes",
        "mode": "link",
        "links": [
            "0b561ac888045f28"
        ],
        "x": 1375,
        "y": 400,
        "wires": []
    },
    {
        "id": "f5d70e8d7b34610f",
        "type": "switch",
        "z": "773898f99a74b037",
        "name": "",
        "property": "topic",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "cfgSB",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "cfgSN",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 1090,
        "y": 320,
        "wires": [
            [
                "ec57a858b905b980"
            ],
            [
                "b8909ecfb4b43f22"
            ]
        ]
    },
    {
        "id": "93a7bd057735b52a",
        "type": "link out",
        "z": "773898f99a74b037",
        "name": "simSBanks",
        "mode": "link",
        "links": [
            "1badc681b25a2eec"
        ],
        "x": 1375,
        "y": 240,
        "wires": []
    },
    {
        "id": "0b561ac888045f28",
        "type": "link in",
        "z": "773898f99a74b037",
        "name": "",
        "links": [
            "0c08e60c2167e6a0"
        ],
        "x": 175,
        "y": 420,
        "wires": [
            [
                "d15264bc8d4e844b",
                "6c1524772d0fd6f0"
            ]
        ]
    },
    {
        "id": "1badc681b25a2eec",
        "type": "link in",
        "z": "773898f99a74b037",
        "name": "",
        "links": [
            "93a7bd057735b52a"
        ],
        "x": 175,
        "y": 260,
        "wires": [
            [
                "d7e75187777ab46a",
                "7471422559690b51"
            ]
        ]
    },
    {
        "id": "b84fc83364365663",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1410,
        "y": 320,
        "wires": []
    },
    {
        "id": "ec57a858b905b980",
        "type": "change",
        "z": "773898f99a74b037",
        "name": "cfg SBanks",
        "rules": [
            {
                "t": "move",
                "p": "payload",
                "pt": "msg",
                "to": "sbanks",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1230,
        "y": 280,
        "wires": [
            [
                "93a7bd057735b52a",
                "b84fc83364365663"
            ]
        ]
    },
    {
        "id": "b8909ecfb4b43f22",
        "type": "change",
        "z": "773898f99a74b037",
        "name": "cfg Scenes",
        "rules": [
            {
                "t": "move",
                "p": "payload",
                "pt": "msg",
                "to": "scenes",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1230,
        "y": 360,
        "wires": [
            [
                "0c08e60c2167e6a0",
                "b84fc83364365663"
            ]
        ]
    },
    {
        "id": "7471422559690b51",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 310,
        "y": 260,
        "wires": []
    },
    {
        "id": "6c1524772d0fd6f0",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 310,
        "y": 420,
        "wires": []
    },
    {
        "id": "42846a895b17d58e",
        "type": "json",
        "z": "773898f99a74b037",
        "name": "",
        "property": "payload",
        "action": "",
        "pretty": false,
        "x": 710,
        "y": 340,
        "wires": [
            [
                "c1c09b47305fcc37"
            ]
        ]
    },
    {
        "id": "dc8a250d0b92de66",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 530,
        "y": 460,
        "wires": []
    },
    {
        "id": "cfe4e233376550f7",
        "type": "debug",
        "z": "773898f99a74b037",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 530,
        "y": 220,
        "wires": []
    }
]
<!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">

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

</head>

<body>
    	
	<div id="app">

		<b-card>Input: {{ plc_in }}</b-card>
	
		<b-card-group>
			<b-card bg-variant="light">
				<b-form-group label="Select Zone">
					<b-form-radio-group size="lg" buttons v-model="ftrZone">
						<b-form-radio v-for="zone in zones" :value=zone :key="zone" inline>{{ zone }}</b-form-radio>
					</b-form-radio-group>
				</b-form-group>
				<p>For Testing, Select Zone 1!</p>
			</b-card>
		
			<b-card bg-variant="light">
				<p>Future Use</p>
			</b-card>
		</b-card-group>

		<b-card bg-variant="light">
			<b-tabs>

				<b-tab lazy title="Switches">
					<b-card bg-variant="default"
						title="Switch Bank Edit">

						<b-card-text>
							<p>Add note</p>
							<b-button variant="success"      class="float-right mx-2" @click="saveSB">Save</b-button>
							<b-btn    varient="danger"       class="float-right mx-2" @click="sbank = []">Reset</b-btn>
						</b-card-text>

						<!-- Switch Modify -->
						<!-- requires sbank to be array, form (above) requires object -->
						<b-table	id = "tbSBedit"
									striped hover small fixed outlined
									thead-class="d-none"							
									:items="sbank"
									caption="">
		
							<template v-slot:cell()="cellData">			
							  <b-form-input v-model="sbank[cellData.index][cellData.field.key]" type="number"></b-form-input>
							</template>
		
						</b-table>

						<!-- Switch Table Header Only-->
						<b-table	id = "tbSBheader"
									small fixed
									tbody-class="d-none"
									:items="ftrSBbyZN">		
						</b-table>

						<!-- Switch Bank Table -->
						<b-table	id = "tbSBlist"
									striped hover small fixed outlined
									selectable select-mode="single"
									thead-class="d-none"
									:items="ftrSBbyZN"
									@row-selected="selectSB">
						</b-table>
					
					</b-card>
				</b-tab>
			
				<b-tab v-for="tab in layers" :key="tab" lazy :title="`Layer `+tab">

					<b-card bg-variant="default"
						title="Scene Edit">
						<b-card-text>
							<p>Add note</p>
							<b-button variant="success"      class="float-right px-4 mx-2" @click="saveSN">Save</b-button>
							<b-btn    varient="danger"       class="float-right px-4 mx-2" @click="scene = []">Reset</b-btn>
						</b-card-text>

					<!-- Scene Modify -->
					<!-- requires scene to be array, form (above) requires object -->
					<b-table	id = "tbSNedit"
								striped hover small fixed outlined
								thead-class="d-none"							
								:items="scene"
								caption="">
	
						<template v-slot:cell()="cellData">		
						  <b-form-input v-model="scene[cellData.index][cellData.field.key]" type="number"></b-form-input>
						</template>
	
					</b-table>
					
					<!-- Scene Table Header Only-->
					<b-table	id = "tbSNheader"
								small fixed
								tbody-class="d-none"
								:items="scenes">
	
					</b-table>
				
					<!-- Scene Table -->
					<!-- First filter scene data by zone set in compute, then filter by later using b-table filter-->
					<b-table	id = "tbSNlist"
								hover small fixed outlined
								selectable select-mode="single"
								thead-class="d-none"
								:items="ftrSNbyZN" 
								:filter="tab" :filter-function="ftrSNLayer" sort-by="layer"
								@row-selected="selectSN">
						
					</b-table>					
				</b-card>
				
				</b-tab>	
			
				<b-tab title="msg log">
					<pre id="msg" class="syntax-highlight">Waiting for a message from Node-RED</pre>
				</b-tab>
				
			</b-tabs>
		</b-card>
	</div>















<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
<script src="../uibuilder/vendor/vue/dist/vue.js"></script> <!-- dev version with component compiler -->
<script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script> <!-- Dev version -->
<script src="./uibuilderfe.min.js"></script>
<script src="./index.js"></script>

</body>
</html>
/* jshint browser: true, esversion: 6, asi: true */
/* globals uibuilder */
// @ts-nocheck

var app = new Vue({
	el: '#app',	
	
	data: {
			scenes: [{"id": 0,"zone": 0, "layer":0}], // init
			sbanks: [{"id": 0,"zone": 0, "layer":0}], // init
			plc_in: null, // init
            ftrZone: [0],
			scene: [], // needed so that table row will be editable on select
			sbank: [], // needed so that table row will be editable on select
	},

	computed: {
	    
	  zones: function () {
			// compute unique zones
			
            let izones = [0]; // leave 0 so there is always 0 zone available to select
            let scenz = this.scenes; // ** this appears to sometimes be undefined ** 
			console.info('czn', JSON.stringify(scenz))

	      // loop thru each scene to build array of zones
            scenz.forEach(s => {
                if (s != null) { izones.push(s.zone) }
            })

	      // to-do: loop thru switch table

	      // create array of unique zone numbers
	        let uzones = [... new Set(izones)];
            
			return uzones.sort();
	  },
	  
	  layers: function () {
			// compute unique layers in scene
			
			let ilayers = [];
            let scenz = this.scenes;
        
			scenz.forEach(i => {
        	    if (i != null) { ilayers.push(i.layer) }
        	    });
                
      	    let ulayers = [... new Set(ilayers)];
            
			return ulayers;
	  },

	  ftrSNbyZN: function () {
                //compute scenes, filtered by selected zone
				
				let sbnk = this.sbanks;  //even though this isn't necessary, without it the table doesnt react
                let scenz = this.scenes;
                let selectedSN = this.ftrZone;
				
                fSNbyZN = scenz.filter(scene =>{
                    return scene.zone == selectedSN;
                });
                
				return fSNbyZN;
	  },

	  ftrSBbyZN: function () {
                //compute sbanks, filtered by selected zone
				
                let sbnk = this.sbanks;
                let scenz = this.scenes; //even though this isn't necessary, without it the table doesnt react
                let selectedSN = this.ftrZone;
				
                fSBbyZN = sbnk.filter(bank =>{
                    return bank.zone == selectedSN;
                });
                
				return fSBbyZN;
	  },
	},

	methods: {
	  
        ftrSNLayer(row, filter) {
            // filter scenes by layer for each table in layer tabs
			// b-table filter takes 2 parameters, row and row filter. returns boolean
			
			if (row.layer == filter) {
                return true;
            } else {
                return false;
            }
        },
		
		selectSN(selSN) {
			// create new Object from selected scene to be edited
			this.scene = [{...selSN[0]}];
		},
        
        saveSN(data) {
            // save/send edited scene data
			payload=JSON.stringify(this.scene[0])
			uibuilder.send( {
                'topic':'cfgSN',
                'payload': payload
                }
            );
			
			console.info('sent',payload) //debug!
			
        },

		selectSB(selSB) {
			// create new Object from selected SBank to be edited
			this.sbank = [{...selSB[0]}];
		},

        saveSB(data) {
            // save/send edited SBank data
			payload=JSON.stringify(this.sbank[0])
            uibuilder.send( {
                'topic':'cfgSB',
                'payload': payload
                }
            );
        },				
	},
})	



/** Minimalist code for uibuilder and Node-RED */
// return formatted HTML version of JSON object
function syntaxHighlight(json) {
    json = JSON.stringify(json, undefined, 4)
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    json = 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>'
    })
    return json
} // --- End of syntaxHighlight --- //

// run this function when the document is loaded
window.onload = function() {
    // Start up uibuilder - see the docs for the optional parameters
    uibuilder.start()

    // Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function(msg){
        
		app.scenes=msg.scenes;
		app.sbanks=msg.sbanks;
		app.plc_in=msg.plc_in;

        console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)

        // dump the msg as text to the "msg" html element
        const eMsg = document.getElementById('msg')
        eMsg.innerHTML = syntaxHighlight(msg)

    })
}

I must admit I didnt fully understand the whole logic of your application
.. serves me right for asking for the full code :wink:

Vue is complaining about the filter prop value being a Number

So try to force it to a String :filter="tab.toString()"

<b-table
                id="tbSNlist"
                hover
                small
                fixed
                outlined
                selectable
                select-mode="single"
                thead-class="d-none"
                :items="ftrSNbyZN"
                :filter="tab.toString()"
                :filter-function="ftrSNLayer"
                sort-by="layer"
                @row-selected="selectSN"
              >
</b-table>

This error is because when you click the SB-A button, that triggers the "set tbSB Global" Function node, you are not sending msg.scenes with the msg.
(With the get Global Data Function you are sending the correct information)

So if you dont have msg.scenes in the msg, when its time to receive it in :

// Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function(msg){
        
		app.scenes=msg.scenes;
		app.sbanks=msg.sbanks;
		app.plc_in=msg.plc_in;

.. it is undefined and you get errors when the related Computed or Method functions are run.

I recommend you install in your browser the Vue Devtools plugin that help you with debugging by seeing at any point what is stored in Vue data.

Thank you. I'll admit that javascript befundles me. I saw that error in the browser console, but wasn't sure how to address it though it didn't seem to be causing a problem (the logic still worked). So thank you for showing me the simple solution to that.

IDK if you used the orginal flow I posted, or the updated flow (edited in my OP) but the updated flow makes a bit more sense. Since I corrected the issue with your suggestion, there's a new "undefined" error being reported through I have not had a chance to chase that down yet. Making progress!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.