Skip to content
Julian Knight edited this page Oct 6, 2017 · 13 revisions

This is an example of using uibuilder with just JQuery. JQuery is included in the node installation by default. These are, in fact, the default master template files that are copied into your local folder (except for uibuilderfe.js which is normally left in the master src folder).

For simple use, JQuery is adequate to enable you to dynamically update the UI as messages come into the node and across to the front end over Socket.IO. For more advanced use, consider using a front-end library such as Moon or Riot, there are examples for these in the uibuilder WIKI.

Note that I will assume your userDir is at ~/.node-red which is the default.

1. Install uibuilder using npm

Open a terminal/command prompt, cd to ~/.node-red (most platforms including Windows PowerShell, %USERPROFILE%\.node-red for Windows cmd prompt).

npm install node-red-contrib-uibuilder --save

2. Restart Node-RED and add a uibuilder node with input and output nodes

After restarting Node-RED, import the flow given below and deploy. Note that the instance of the uibuilder node in the example flow has its URL set to uibuilder which is the default.

3. Copy/amend the template files

You should now have a folder ~/.node-red/uibuilder/uibuilder/src. You need to copy over the files shown below into that folder. You will be replacing the files that the deployment of the node has copied over for you.

Note that you are generally better off using the default master files rather than the ones here as those are more likely to be the latest versions.

4. Open the uibuilder URL

In your favourite browser, navigate to the /uibuilder url. If you are using default settings and running Node-RED on the same machine as the browser, this will be http://localhost:1880/uibuilder.

Now you can use the button in the web page to send a message back to Node-RED, the data will appear in the debug output pane. Then you can use the Inject node to send a message to the browser.

Screenshots

Example Flow

Import this to Node-RED and deploy.

[{"id":"56888c14.108754","type":"inject","z":"106ba95c.ff91e7","name":"","topic":"","payload":"{\"fred\":\"jim\", \"billy\": 500}","payloadType":"json","repeat":"","crontab":"","once":false,"x":130,"y":160,"wires":[["36210ab8.3e1b76"]]},{"id":"c60764c8.90faa8","type":"debug","z":"106ba95c.ff91e7","name":"uibuilder-debug","active":true,"console":"false","complete":"true","x":480,"y":160,"wires":[]},{"id":"36210ab8.3e1b76","type":"uibuilder","z":"106ba95c.ff91e7","name":"a","url":"uibuilder","fwdInMessages":false,"x":280,"y":160,"wires":[["c60764c8.90faa8"]]}]

Example front-end files

These go in ~/.node-red/uibuilder/uibuilder/src

index.html

<!doctype html>
<html lang="en">
  <!--
    This is the default, template html for uibuilder.
    It is only meant to demonstrate the use of JQuery to dynamically
    update the ui based on incoming/outgoing messages from/to the
    Node-RED server.

    You will want to alter this to suite your own needs. To do so,
    copy this file to <userDir>/uibuilder/<url>/src.
  -->
<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="Node-RED UI Builder">

    <!-- 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="Node-RED UI Builder">

    <!-- 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="Node-RED UI Builder">

    <link rel="icon" href="images/node-red.ico">

    <!-- OPTIONAL: Normalize is used to make things the same across browsers. Index is for your styles -->
    <link rel="stylesheet" href="vendor/normalize.css/normalize.css">
    <link rel="stylesheet" href="index.css">

</head>
<body>
    <!-- The "app" element is where the code for dynamic updates is attached -->
    <div id="app">
        <h1>
            Welcome to UIbuilder for Node-RED
        </h1>
        <p>
            This is the default web page. If you open the developer console, you will see some debug output.
            You can also see some dynamic data updating below, thanks to JQuery.
        </p>
        <p>
            Please see the README for the
            <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder">node-red-contrib-uibuilder</a>
            node for details on how to use UIbuilder.
        </p>
        <h2>Dynamic Data (via JQuery)</h2>
        <p>UIBuilder Front-End Version: <span id="feVersion"></span></p>
        <p>Websocket State: <span id="socketConnectedState"></span></p>
        <p>Messages Received: <span id="msgsReceived"></span></p>
        <p>Control Messages Received: <span id="msgsControl"></span></p>
        <p>Messages Sent: <span id="msgsSent"></span></p>
        <p>Last Message Received:</p>
        <code id="showMsg"></code>
        <p>Last Control Message Received:</p>
        <code id="showCtrlMsg"></code>
        <p>Last Message Sent:</p>
        <code id="showMsgSent"></code>
    </div>

    <!-- These MUST be in the right order. Note no leading / -->
    <!-- REQUIRED: Socket.IO is loaded only once for all instances
                   Without this, you don't get a websocket connection -->
    <script src="/uibuilder/socket.io/socket.io.js"></script>
    <!-- Note no leading / -->
    <!-- OPTIONAL: JQuery can be removed if not required -->
    <script src="vendor/jquery/dist/jquery.min.js"></script>
    <!-- REQUIRED: Sets up Socket listeners and the msg object -->
    <script src="uibuilderfe.js"></script>
    <!-- OPTIONAL: You probably want this. Put your custom code here -->
    <script src="index.js"></script>

</body>
</html>

index.css

body {font-family: sans-serif;}
div, p, code { margin:0.3em; padding: 0.3em;}

index.js

/*global document,$,window,io */
/*
  Copyright (c) 2017 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.
 * 
 * Globals set by uibuilderfe.js:
 *   uibuilder: The main global object containing the following...
 *     Methods:
 *       .onChange(attribute, callbackFn) - listen for changes to attribute and execute callback when it changes
 *       .get(attribute)        - Get any available attribute
 *       .set(attribute, value) - Set any available attribute
 *       .msg                   - Shortcut to get the latest value of msg. Equivalent to uibuilder.get('msg')
 *       .sendMsg               - Shortcut to send a msg back to Node-RED manually
 *     Attributes with change events (only accessible via .get method except for msg)
 *       .msg          - Copy of the last msg sent from Node-RED over Socket.IO
 *       .sendMsg      - Copy of the last msg sent by us to Node-RED
 *       .ctrlMsg      - Copy of the last control msg received by us from Node-RED
 *       .msgsReceived - How many standard messages have we received
 *       .msgsSent     - How many messages have we sent
 *       .msgsCtrl     - How many control messages have we received
 *       .ioConnected  - Is Socket.IO connected right now? (true/false)
 *     Attributes without change events 
 *           (only accessible via .get method, reload page to get changes, change in uibuilderfe.js)
 *       .debug       - true/false, controls debug console logging output
 *       ---- You are not likely to need any of these ----
 *       .version     - check the current version of the uibuilder code
 *       .ioChannels  - List of the channel names in use [uiBuilderControl, uiBuilderClient, uiBuilder]
 *       .retryMs     - starting retry ms period for manual socket reconnections workaround
 *       .retryFactor - starting delay factor for subsequent reconnect attempts
 *       .ioNamespace - Get the namespace from the current URL
 *       .ioPath      - make sure client uses Socket.IO version from the uibuilder module (using path)
 *       .ioTransport - ['polling', 'websocket']
 * 
 *   makeMeAnObject(thing, attribute='payload') - Utility function, make sure that 'thing' is an object
 *   debug          -
 *   setIOnamespace -
 */

// When JQuery is ready, update
$( document ).ready(function() {
    // Initial set
    $('#msgsReceived').text( uibuilder.get('msgsReceived') )
    $('#msgsControl').text( uibuilder.get('msgsCtrl') )
    $('#msgsSent').text( uibuilder.get('msgsSent') )
    $('#socketConnectedState').text( uibuilder.get('ioConnected') )
    $('#feVersion').text( uibuilder.get('version') )

    // Turn on debugging
    uibuilder.set('debug', true)
    
    // 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.info('property msg changed!')
        console.dir(newVal)
        $('#showMsg').text(JSON.stringify(newVal))
    })

    // You can get attributes manually
    //console.dir(uibuilder.get('msg'))

    // You can also set things manually:
    //uibuilder.set('msg', {name:'jim'})

    // As noted, we could get the msg here too
    uibuilder.onChange('msgsReceived', function(newVal){
        console.info('New msg sent to us from Node-RED over Socket.IO. Total Count: ', newVal)
        $('#msgsReceived').text(newVal)
        // uibuilder.msg is a shortcut for uibuilder.get('msg')
        //$('#showMsg').text(JSON.stringify(uibuilder.msg))
    })

    // If Socket.IO connects/disconnects
    uibuilder.onChange('ioConnected', function(newVal){
        console.info('Socket.IO Connection Status Changed: ', newVal)
        $('#socketConnectedState').text(newVal)
    })

    // If a message is sent back to Node-RED
    uibuilder.onChange('msgsSent', function(newVal){
        console.info('New msg sent to Node-RED over Socket.IO. Total Count: ', newVal)
        $('#msgsSent').text(newVal)
        $('#showMsgSent').text(JSON.stringify(uibuilder.get('sentMsg')))
    })

    // If we receive a control message from Node-RED
    uibuilder.onChange('msgsCtrl', function(newVal){
        console.info('New control msg sent to us from Node-RED over Socket.IO. Total Count: ', newVal)
        $('#msgsControl').text(newVal)
        $('#showCtrlMsg').text(JSON.stringify(uibuilder.get('ctrlMsg')))
    })

    //Manually send a message back to Node-RED after 2 seconds
    window.setTimeout(function(){
        console.info('Sending a message back to Node-RED - after 2s delay')
        uibuilder.send( { 'topic':'uibuilderfe', 'payload':'I am a message sent from the uibuilder front end' } )
    }, 2000)
})

// ----- UTILITY FUNCTIONS ----- //

// EOF

uibuilderfe.js

This is the magic that allows you to have a much simplified index.js. It does all the heavy lifting with Socket.IO and gives you an onChange function to listen for changes to key variables so that you can update the UI.

Normally, you wouldn't have this file in your local src folder, it would be picked up from the master folder. However, in case you want to play with it or maybe you don't yet have the version of uibuilder with this file in it, it is presented here.

/*global document,window,io */
/*
  Copyright (c) 2017 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 Front-End JavaScript for uibuilder
 * It provides a number of global objects that can be used in your own javascript. 
 * Please use the default index.js file for your own code and leave this as-is 
 * unless you really need to change something.
 * See the master template index.js file for how to use.
 * Inspiration for this came from:
 * // @see https://ponyfoo.com/articles/a-less-convoluted-event-emitter-implementation
 * // @see https://gist.github.com/wildlyinaccurate/3209556
 * 
 * Globals set by uibuilderfe.js:
 *   uibuilder: The main global object containing the following...
 *     Methods:
 *       .onChange(attribute, callbackFn) - listen for changes to attribute and execute callback when it changes
 *       .get(attribute)        - Get any available attribute
 *       .set(attribute, value) - Set any available attribute
 *       .msg                   - Shortcut to get the latest value of msg. Equivalent to uibuilder.get('msg')
 *       .sendMsg               - Shortcut to send a msg back to Node-RED manually
 *     Attributes with change events (only accessible via .get method except for msg)
 *       .msg          - Copy of the last msg sent from Node-RED over Socket.IO
 *       .sendMsg      - Copy of the last msg sent by us to Node-RED
 *       .ctrlMsg      - Copy of the last control msg received by us from Node-RED
 *       .msgsReceived - How many standard messages have we received
 *       .msgsSent     - How many messages have we sent
 *       .msgsCtrl     - How many control messages have we received
 *       .ioConnected  - Is Socket.IO connected right now? (true/false)
 *     Attributes without change events 
 *           (only accessible via .get method, reload page to get changes, change in uibuilderfe.js)
 *       .debug       - true/false, controls debug console logging output
 *       ---- You are not likely to need any of these ----
 *       .version     - check the current version of the uibuilder code
 *       .ioChannels  - List of the channel names in use [uiBuilderControl, uiBuilderClient, uiBuilder]
 *       .retryMs     - starting retry ms period for manual socket reconnections workaround
 *       .retryFactor - starting delay factor for subsequent reconnect attempts
 *       .ioNamespace - Get the namespace from the current URL
 *       .ioPath      - make sure client uses Socket.IO version from the uibuilder module (using path)
 *       .ioTransport - ['polling', 'websocket']
 * 
 *   makeMeAnObject(thing, attribute='payload') - Utility function, make sure that 'thing' is an object
 *   debug          -
 *   setIOnamespace -
 */

const uibuilder = function () {
    const self  = this

    self.version     = '0.4.1'
    self.debug       = false

    /** msg object. Const so you cannot replace it.
     * Updated on Socket.IO events.
     * See the msg.prototype method function definitions
     * Data attributes may ONLY be changed using the msg.set method.
     * METHODS:
     *   msg.set      - updates a data attribute
     *   msg.get      - get the current value of a data attribute
     *   msg.onChange - event method triggered when msg.set is used
     * DATA ATTRIBUTES:
     *   msg.payload
     *   msg.topic
     *   msg._msgId
     * 
     *   Others may be used but care should be taken not to clash with
     *   msg.get/set/onChange (you are prevented from actually overwriting
     *   them if you use msg.set)
     */
    self.msg         = {}
    self.ctrlMsg     = {}
    self.sentMsg     = {}
    
    self.msgsSent    = 0
    self.msgsReceived= 0
    self.msgsCtrl    = 0
    
    self.ioChannels  = {control: 'uiBuilderControl', client: 'uiBuilderClient', server: 'uiBuilder'}
    self.retryMs     = 2000                                                                            // starting retry ms period for manual socket reconnections workaround
    self.retryFactor = 1.5                                                                             // starting delay factor for subsequent reconnect attempts
    self.timerid     = null
    self.ioNamespace = setIOnamespace()            // Get the namespace from the current URL
    self.ioPath      = '/uibuilder/socket.io'      // make sure client uses Socket.IO version from the uibuilder module (using path)
    self.ioTransport = ['polling', 'websocket']
    self.ioConnected = false

    // === Socket.IO processing === //

    // Create the socket - make sure client uses Socket.IO version from the uibuilder module (using path)
    self.socket      = io(self.ioNamespace, { path: self.ioPath, transports: self.ioTransport })

    /** Check whether Socket.IO is connected to the server, reconnect if not (recursive)
     * 
     * @param {integer} delay Initial delay before checking (ms)
     * @param {integer} factor Multiplication factor for subsequent checks (delay*factor)
     */
    self.checkConnect = function(delay, factor) {
        var depth = depth++ || 1
        debug && console.log('checkConnect. Depth: ', depth, ' , Delay: ', delay, ', Factor: ', factor)
        if (self.timerid) window.clearTimeout(self.timerid) // we only want one running at a time
        self.timerid = window.setTimeout(function(){
            debug && console.log('checkConnect timeout. SIO reconnect attempt, timeout: ' + delay + ', depth: ' + depth)
            // don't need to check whether we have connected as the timer will have been cleared if we have
            self.socket.close()    // this is necessary sometimes when the socket fails to connect on startup
            self.socket.connect()  // Try to reconnect
            self.timerid = null
            self.checkConnect(delay*factor, factor) // extend timer for next time round
        }, delay)
    } // --- End of checkConnect Fn--- //

    // When the socket is connected ...
    self.socket.on('connect', function() {
        debug && console.log('uibuilderfe: SOCKET CONNECTED - Namespace: ' + self.ioNamespace)

        self.uiReturn.set('ioConnected', true)

        // Reset any reconnect timers
        if (self.timerid) {
            window.clearTimeout(self.timerid)
            self.timerid = null
        }

    }) // --- End of socket connection processing ---

    // When Node-RED uibuilder node sends a msg over Socket.IO to us ...
    self.socket.on(self.ioChannels.server, function(receivedMsg) {
        debug && console.info('uibuilderfe: socket.on.server - msg received - Namespace: ' + self.ioNamespace)
        debug && console.dir(receivedMsg)

        // Make sure that msg is an object & not null
        receivedMsg = makeMeAnObject( receivedMsg, 'payload' )

        // Save the msg for further processing
        self.uiReturn.set('msg', receivedMsg)

        // Track how many messages have been received
        self.uiReturn.set('msgsReceived', self.msgsReceived + 1)

        // Test auto-response - not really required but useful when getting started
        //if (self.debug) {
        //    self.send({payload: 'From: uibuilderfe - we got a message from you, thanks'})
        //}

    }) // -- End of websocket receive DATA msg from Node-RED -- //

    // Receive a CONTROL msg from Node-RED
    self.socket.on(self.ioChannels.control, function(receivedCtrlMsg) {
        self.debug && console.info('uibuilder:socket.on.control - msg received - Namespace: ' + self.ioNamespace)
        self.debug && console.dir(receivedCtrlMsg)

        // Make sure that msg is an object & not null
        if ( receivedCtrlMsg === null ) {
            receivedCtrlMsg = {}
        } else if ( typeof receivedCtrlMsg !== 'object' ) {
            receivedCtrlMsg = { 'payload': receivedCtrlMsg }
        }

        self.uiReturn.set('ctrlMsg', receivedCtrlMsg)
        self.uiReturn.set('msgsCtrl', self.msgsCtrl + 1)
        
        /*
        switch(receivedCtrlMsg.type) {
            case 'shutdown':
                // Node-RED is shutting down
                break
            case 'server connected':
                // We are connected to the server
                break
            default:
                // Anything else
        }
        */

        // Test auto-response
        if (self.debug) {
            self.sendMsg({payload: 'We got a control message from you, thanks'})
        }

    }) // -- End of websocket receive CONTROL msg from Node-RED -- //

    // When the socket is disconnected ..............
    self.socket.on('disconnect', function(reason) {
        // reason === 'io server disconnect' - redeploy of Node instance
        // reason === 'transport close' - Node-RED terminating
        // reason === 'ping timeout' - didn't receive a pong response?
        debug && console.log('SOCKET DISCONNECTED - Namespace: ' + self.ioNamespace + ', Reason: ' + reason)

        self.uiReturn.set('ioConnected', false)

        // A workaround for SIO's failure to reconnect after a NR redeploy of the node instance
        if ( reason === 'io server disconnect' ) {
            self.checkConnect(self.retryMs, self.retryFactor)
        }
    }) // --- End of socket disconnect processing ---

    /* We really don't need these, just for interest
        socket.on('connect_error', function(err) {
            debug && console.log('SOCKET CONNECT ERROR - Namespace: ' + ioNamespace + ', Reason: ' + err.message)
            //console.dir(err)
        }) // --- End of socket connect error processing ---
        socket.on('connect_timeout', function(data) {
            debug && console.log('SOCKET CONNECT TIMEOUT - Namespace: ' + ioNamespace)
            console.dir(data)
        }) // --- End of socket connect timeout processing ---
        socket.on('reconnect', function(attemptNum) {
            debug && console.log('SOCKET RECONNECTED - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
        }) // --- End of socket reconnect processing ---
        socket.on('reconnect_attempt', function(attemptNum) {
            debug && console.log('SOCKET RECONNECT ATTEMPT - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
        }) // --- End of socket reconnect_attempt processing ---
        socket.on('reconnecting', function(attemptNum) {
            debug && console.log('SOCKET RECONNECTING - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
        }) // --- End of socket reconnecting processing ---
        socket.on('reconnect_error', function(err) {
            debug && console.log('SOCKET RECONNECT ERROR - Namespace: ' + ioNamespace + ', Reason: ' + err.message)
            //console.dir(err)
        }) // --- End of socket reconnect_error processing ---
        socket.on('reconnect_failed', function(data) {
            debug && console.log('SOCKET RECONNECT FAILED - Namespace: ' + ioNamespace)
            console.dir(data)
        }) // --- End of socket reconnect_failed processing ---
        socket.on('ping', function() {
            debug && console.log('SOCKET PING - Namespace: ' + ioNamespace)
        }) // --- End of socket ping processing ---
        socket.on('pong', function(data) {
            debug && console.log('SOCKET PONG - Namespace: ' + ioNamespace + ', Data: ' + data)
        }) // --- End of socket pong processing ---
    */

    /** Send msg back to Node-RED via Socket.IO
     * NR will generally expect the msg to contain a payload topic
     * @param {object} msgToSend The msg object to send.
     */
    self.send = function(msgToSend) {
        debug && console.info('uibuilderfe: msg sent - Namespace: ' + self.ioNamespace)
        debug && console.dir(msgToSend)

        // @TODO: Make sure msgToSend is an object

        // Track how many messages have been sent
        self.uiReturn.set('sentMsg', msgToSend)
        self.uiReturn.set('msgsSent', self.msgsSent + 1)

        self.socket.emit(self.ioChannels.client, msgToSend)
    } // --- End of Send Msg Fn --- //

    // === Our own event handling system === //

    self.events = {}  // placeholder for event listener callbacks by property name
  
    /** Trigger event listener for a given property
     * Called when uibuilder.set is used
     * 
     * @param {any} prop The property for which to run the callback functions
     */
    self.emit = function (prop) {
        var evt = self.events[prop]
        if (!evt) {
            return
        }
        var args = Array.prototype.slice.call(arguments, 1)
        for (var i = 0; i < evt.length; i++) {
            evt[i].apply(self, args)
        }
    }

    // === uibuilder callbacks === //

    // uiReturn contains a set of functions that are returned when this function
    // self-executes (on-load)
    self.uiReturn = {

        // TODO: Swap this to self.set and then reference here (like msg & sendMsg)
        /** Function to set uibuilder properties to a new value
         * Also triggers any event listeners.
         * Example: uibuilder.set('msg', {topic:'uibuilder', payload:'Wow!'})
         * @param {string} prop 
         * @param {any} val 
         */
        set : function(prop, val) {
            // TODO: Add exclusions for protected properties
            
            self[prop] = val
            
            // Trigger this prop's event callbacks (listeners)
            self.emit(prop, val)
            
            //debug('log', `uibuilderfe:uibuilder:set Property: ${prop}, Value: ${val}`)
        },

        /** Function to get the value of a uibuilder property
         * Example: uibuilder.get('msg')
         * @param {string} prop The name of the property to get
         * @returns {any} The current value of the property
         */
        get : function(prop) {
            //if ( prop !== 'debug' ) debug('log', `uibuilderfe:uibuilder:get Property: ${prop}`)
            // TODO: Add warning for non-existent property?
            return self[prop]
        },
        
        /** Register on-change event listeners
         * For any existing property in this object, make it possible to register
         * a function that will be run when the property changes.
         * Example: uibuilder.onChange('msg', function(newValue){ console.log('uibuilder.msg changed! It is now: ', newValue) })
         * 
         * @param {string} prop The property of uibuilder that we want to monitor
         * @param {function} callback The function that will run when the property changes
         */
        onChange : function(prop, callback) {
            // Property has to exist
            if ( self.hasOwnProperty(prop) ) {
                //debug('log', `uibuilderfe:uibuilder:onchange pushing new callback (even listener) for property: ${prop}`)

                // Create a new array or add to the array of callback functions for the property in the events object
                if ( self.events[prop] ) {
                    events[prop].push(callback)
                } else {
                    events[prop] = [callback]
                }
            } else {
                debug('error', `uibuilderfe:uibuilder:onchange property: ${prop} - does not exist`)
            }
        },

        /** Helper fn, shortcut to return current value of msg
         * Use instead of having to do: uibuilder.get('msg')
         * Example: console.log( uibuilder.msg )
         * 
         * @returns {object} msg
         */
        msg : self.msg,

        /** Helper fn, Send a message to NR
         * Example: uibuilder.sendMsg({payload:'Hello'})
         */
        send : self.send

    } // --- End of return callback functions --- //

    // === End of setup, start execution === //

    // Repeatedly check & retry connection until connected (async)
    checkConnect(self.retryMs, self.retryFactor)

    return uiReturn

}(); // --- End of uibuilder self-executing function --- //


// ========== UTILITY FUNCTIONS ========== //

/** Get the Socket.IO namespace from the current URL
 * @returns {string} Socket.IO namespace
 */
function setIOnamespace() {
    var u = window.location.pathname.split('/')

    //if last element is '', take [-1]
    var ioNamespace = u.pop()
    if (ioNamespace === '') ioNamespace = u.pop()

    debug && console.log('uibuilderfe: IO Namespace: /' + ioNamespace)
    
    // Socket.IO namespace HAS to start with a leading slash
    return '/' + ioNamespace
} // --- End of set IO namespace --- //

/** Makes a null or non-object into an object
 * If not null, moves "thing" to {payload:thing}
 * 
 * @param {any} thing Thing to check
 * @param {string} [attribute='payload'] Attribute that "thing" is moved to if not null and not an object
 * @returns {object}
 */
function makeMeAnObject(thing, attribute='payload') {

    if ( thing === null ) {
        thing = {}
    } else if ( typeof thing !== 'object' ) {
        thing = { attribute : thing }
    }

    return thing
} // --- End of make me an object --- //

/** Debugging function
 * @param {string} type One of log|error|info|dir
 * @param {any} msg Msg to send to console
 */
function debug(type,msg) {
    if ( !uibuilder.get('debug') ) return

    let myLog = {}
    switch ( type ) {
        case 'error':
            myLog = console.error
            break
        case 'info':
            myLog = console.info
            break
        case 'dir':
            myLog = console.dir
            break
        default:
            myLog = console.log
    }

    myLog(msg)
} // --- End of debug function --- //

// EOF
Clone this wiki locally