Ui_template mvc and scope of context/flow/global stores

Is there a simple diagram showing the whole of this ?

The diagram would be two circles that don't overlap at all. The ui_template node has no direct access to context - you have to pass in any information you want it to have via messages in the flow.

1 Like

where does button state exist ?

If you mean a ui_button, then it holds its own state internally... although what state are you referring to for a button?

I am trying to create a button and controller function that will easily facilitate the construction of keypad groups that can maintain their state through outages and restarts.

I have already done this using a single keypad function node wired to the ins and outs of lots of ui_template button nodes. This initial approach has a few drawbacks namely wiring but also as the umber of buttons and keypads grow the cpu load becomes a problem in this embedded application.

These are not your average keypads. They can be themed through the dash by the user, perform save and restore memory functions, have flexible captions as well as the usual flash and visual feedback of state change.

Then I came across keypad examples using one ui_template for css, one ui_template controller, one ui_template for each button and no wiring.

I am struggling to discover the optimal way of achieving my goal without an overall picture of where most of the mips are going.

One possible way would be if I could reduce the msg traffic to just button state changes.

Since one can leverage pure css push/toggle/radio buttons with static css.

@hotNipi showed a way:

[{"id":"df724f21.31d9c","type":"function","z":"503e64bc.acab6c","name":"make dynamic style","func":"let stylestring;\nlet defaultBranding;\nlet key;\n\n\n// default values\ndefaultBranding = {\n    buttonColor: '#EAEAEA',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#B0B0B0',\n    buttonTextColor: '#000000',\n    buttonTextColorDisabled: '#AAAAAA',\n};\n\n//override default branding with incoming brand values \nfor (key in defaultBranding) {\n    if (key in msg.payload) {\n        defaultBranding[key] = msg.payload[key];\n    }\n}\n\n// style string template\nstylestring = `\n.remote-button{\n    background-color: [buttonColor];\n    color: [buttonTextColor];\n    height: var(--dashboard-unit-height);\n    width: 100%;\n    border-radius: 10px;\n    font-size:1.0em;\n    font-weight:normal;\n    margin: 0;\n    min-height: 36px;\n    min-width: unset;\n    line-height: unset;\n}\n.remote-button:not([disabled]):hover{\n    background-color: [buttonColorHover];\n}\n.remote-button.disabled{\n    background-color: [buttonColorDisabled];\n    cursor: not-allowed;\n    pointer-events: none;\n    color: [buttonTextColorDisabled];\n}\n`;\n\n// replace template values with actual values\n\nfor (key in defaultBranding) {\n    while (stylestring.indexOf('['+key+']') != -1) {\n        stylestring = stylestring.replace('['+key+']', defaultBranding[key]);\n    }\n}\n\nmsg.payload = stylestring;\nreturn msg\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":310,"y":80,"wires":[["f8c69ec5.8552a"]]},{"id":"5e9a81e5.64c3a","type":"function","z":"503e64bc.acab6c","name":"brand","func":"msg.payload = {\n    buttonColor: '#CCCCCC',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#999999',\n    buttonTextColor: '#FF0000',\n    buttonTextColorDisabled: '#CCCCCC',\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":390,"y":40,"wires":[["df724f21.31d9c"]]},{"id":"c4e35e8a.52bdf","type":"inject","z":"503e64bc.acab6c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":40,"wires":[["5e9a81e5.64c3a"]]},{"id":"f8c69ec5.8552a","type":"ui_template","z":"503e64bc.acab6c","group":"3c73f954.0df8c6","name":"apply styles","order":1,"width":0,"height":0,"format":"\n<div></div>\n<script>\n(function(scope) {\n  scope.$watch('msg', function(msg) {\n    if (msg) {\n        var stylesheet = document.createElement('style');\n        stylesheet.id = 'overrided-styles';\n        stylesheet.innerHTML = msg.payload;\n        document.head.appendChild(stylesheet);\n    }\n  });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":330,"y":120,"wires":[[]]},{"id":"3c73f954.0df8c6","type":"ui_group","z":"","name":"Full_Remote2","tab":"7389379e.6ef168","order":3,"disp":false,"width":"3","collapse":false},{"id":"7389379e.6ef168","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

but I cannot get it to work (not a criticism of the example but all to do with my lack of understanding of the big picture).

  1. If I can get it to work would the last css_override persist ?
  2. How expensive (in relation to mips) is the action of a css_override (would it be a huge burdon if it were updated once per second for example) ?
  3. Does it cause a display flicker ?

Edit: Further progress made:

  1. If I can get it to work would the last css_override persist ?
  • It seems to persist a browser refresh but I loose the state of my toggle buttons.
  1. How expensive (in relation to mips) is the action of a css_override (would it be a huge burdon and abuse if it were updated once per second for example) ?
  • It gets dodgy if > 1Hz.
  1. Does it cause a display flicker ?
  • Not that I can tell.

Any comments would be welcome.

I am still not sure about performance. Could I use this technique to automatically toggle the caption text on button state change ?.

Many thanks in advance.
Current TEST flow

[{"id":"503e64bc.acab6c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"5666698b.c679c8","type":"inject","z":"503e64bc.acab6c","name":"toggleClass disabled, on 10","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"10","payload":"{\"command\":\"toggleClass\",\"value\":\"disabled\"}","payloadType":"json","x":320,"y":380,"wires":[["1a49a0c3.61fc3f"]]},{"id":"9ba89faf.73394","type":"inject","z":"503e64bc.acab6c","name":"toggleClass green, on 10","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"10","payload":"{\"command\":\"toggleClass\",\"value\":\"green\"}","payloadType":"json","x":310,"y":420,"wires":[["1a49a0c3.61fc3f"]]},{"id":"c3be9084.fc1fe","type":"inject","z":"503e64bc.acab6c","name":"10:0","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"10","payload":"0","payloadType":"num","x":250,"y":460,"wires":[["1a49a0c3.61fc3f"]]},{"id":"35d1aadc.5941e6","type":"inject","z":"503e64bc.acab6c","name":"10:1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"10","payload":"1","payloadType":"num","x":410,"y":460,"wires":[["1a49a0c3.61fc3f"]]},{"id":"2f8b4bb4.988a54","type":"debug","z":"503e64bc.acab6c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":460,"y":540,"wires":[]},{"id":"d287d591.e7ebd8","type":"comment","z":"503e64bc.acab6c","name":"CSS - only .remote-button3 is significant","info":"","x":320,"y":240,"wires":[]},{"id":"bef9867d.b18028","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"10","order":4,"width":"2","height":1,"format":"<!--\n<div>\n   <label>\n      <input type=\"checkbox\" value=\"1\"><span>red</span>\n   </label>\n</div>\n<div>\n   <md-button class=\"md-button remote-button\" \n              data-payload=\"1\" \n              data-buttontype=\"toggle\"\n              data-topic=\"mute\"\n              data-icon0=\"volume_off\"\n              data-icon1=\"volume_mute\"\n              aria-label=\"volume mute\"\n              >\n      <i class=\"material-icons md-48\">volume_mute</i>\n   </md-button>\n</div>\n<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-buttontype='radio'\n    data-radiogroup='group1'\n    data-topic=\"13\"\n    data-icon0=\"fa fa-circle-o\"\n    data-icon1=\"fa fa-check-circle-o\"\n    data-payload=\"13\">\n       <i class=\"fa fa-circle-o\"></i> 13\n   </md-button>\n</div>\n//-->\n<div>\n   <md-button class=\"md-button remote-button3 bigger\"\n    style=\"background-color={{msg.keypad.colour}}\"\n    ng-bind-html=\"msg.keypad.caption\"\n    data-buttontype=\"toggle\"\n    data-topic=\"10\"\n    data-payload=\"1\">\n    <label>\n        <input type=\"checkbox\" value=\"1\">\n        <span>10</span>\n    </label>\n   </md-button>\n</div>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":220,"y":340,"wires":[[]]},{"id":"2d11d614.7012fa","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"CSS only ","order":1,"width":0,"height":0,"format":"/!--\nCheck box colour change example.\n================================\n#ck-button {\n  margin: 4px;\n  background-color: #EFEFEF;\n  border-radius: 4px;\n  border: 1px solid #D0D0D0;\n  overflow: auto;\n  float: left;\n}\n\n#ck-button:hover {\n  margin: 4px;\n  background-color: #EFEFEF;\n  border-radius: 4px;\n  border: 1px solid red;\n  overflow: auto;\n  float: left;\n  color: red;\n}\n\n#ck-button label {\n  float: left;\n  width: 4em;\n}\n\n#ck-button label span {\n  text-align: center;\n  padding: 3px 0px;\n  display: block;\n}\n\n#ck-button label input {\n  position: absolute;\n  top: -20px;\n}\n\n#ck-button input:checked + span {\n  background-color: #911;\n  color: #fff;\n}\n\n-->\n<style>\n    :root {\n      --dashboard-unit-width: 48px;\n      --dashboard-unit-height: 48px;\n      --dashboard-min-height: 35px;\n      --dashboard-min-width: 36px;\n      --remote-button-background: black;\n      --remote-button-foreground: #cccccc;\n    }\n    .nr-dashboard-template {\n        padding: 0px;\n    }\n.remote-buttonx{\n    background-color: [buttonColor];\n    color: [buttonTextColor];\n    height: var(--dashboard-unit-height);\n    width: 100%;\n    border-radius: 10px;\n    font-size:1.0em;\n    font-weight:normal;\n    margin: 0;\n    min-height: 36px;\n    min-width: unset;\n    line-height: unset;\n}\n.remote-buttonx:not([disabled]):hover{\n    background-color: [buttonColorHover];\n}\n.remote-buttonx.disabled{\n    background-color: [buttonColorDisabled];\n    cursor: not-allowed;\n    pointer-events: none;\n    color: [buttonTextColorDisabled];\n}    \n.remote-button3 {\n    background-color: var(--remote-button-background) !important;\n    height: var(--dashboard-unit-height);\n    width: 100%;\n    border-radius: 10px;\n    font-size:1em;\n    font-weight:normal;\n    min-height: var(--dashboard-min-height);\n    min-width: unset;\n    line-height: unset;\n  border: 1px solid #D0D0D0;\n  overflow: auto;\n  float: left;\n} \n.remote-button3.disabled {\n    pointer-events: none;\n    opacity: 0.3;\n}\n.remote-button3:hover {\n        /* apply hover effect here */\n        margin:4px;\n        background-color:#EFEFEF;\n        border-radius:10px;\n        border:1px solid #c00;\n        overflow:auto;\n        float:left;\n  }\n.remote-button3.green {\n    background-color: #0c0 !important;\n}\n.remote-button3.bigger{\n    font-weight:bold;\n    font-size:1.5em;\n}\n.remote-button3:disabled {\n    cursor: not-allowed;\n    pointer-events: none;\n    background-color: #222222;\n    color: #666666;\n}\n\n.remote-button3 label {\n    float:left;\n    width: 100%;\n    background-size: contain;\n}\n.remote-button3 label span {\n    border-radius:8px;\n    line-height: var(--dashboard-min-height);\n    width: 100%;\n    padding: 5px;\n    margin: -5px;\n    text-align:center;\n    display:block;\n}\n.remote-button3 label input {\n    position:absolute;\n    top:-20px;\n}\n.remote-button3 input:checked + span {\n    background-color:#c00;\n    color:#fff;\n}\n    .remote-icon{\n        font-size:2.0em;\n    }\n    /*  This is the same as the other one, but it makes the icon smaller  */\n    .remote-iconS{\n        font-size:0.5em;\n    }\n\n</style>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"global","x":230,"y":280,"wires":[[]]},{"id":"1a49a0c3.61fc3f","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"script for all buttons with class remote-button","order":26,"width":"1","height":"1","format":"<div>\n<!--diliberately emtpy - only need the script below -->\n</div>\n\n<script>\n\n(function($scope) {\n//debugger\n\n    //cause a small delay while things load \n    //ideally this would be an init event or on all parts of document loaded\n    //(may not be necessary!)\n    setTimeout(function() {\n        //debugger\n        $scope.init();\n    },100);\n    \n    \n    //SETTINGS...\n    var BUTTON_CLASS = \".remote-button3\";\n    var SEND_0_FOR_CLEARED_RADIO_BUTTONS = true;\n    var SEND_ONLY_STATE_CHANGES_RADIO_BUTTONS = true;\n    \n    \n    /** \n     * Initialise all buttons with class BUTTON_CLASS\n    */\n    $scope.init = function () {\n        //debugger\n        console.log(\"$scope.init called. Adding event handlers to all buttons with class '\" + BUTTON_CLASS + \"'.\");\n        var clickButtons = $(BUTTON_CLASS) \n        clickButtons.click(function(e){\n            \n            var btn = $(this)\n            var type = btn.data(\"buttontype\");//get the button type from attribute data-buttontype=\"xxxxx\"\n            var topic = btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            var payload = btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            // debugger\n            if(type == \"radio\"){\n                setRadioButton(btn, true);\n            } else if(type == \"toggle\"){\n                var state = btn.data(\"state\") || 0;//get the last state payload from attribute data-state=\"n\"\n                var newPayload = state == 1 ? 0 : 1;\n                var colour = [];\n                setButtonState(btn,newPayload)\n                colour[0]=\"#000066\";\n                colour[1]=\"#0000cc\";\n                var tm = {\"keypad\": \n                    {\"caption\": state == 0 ? topic + \" Off\" : topic + \" On\",\n                     \"colour\": state == 0 ? colour[0] : colour[1]}, \"class\": BUTTON_CLASS, \"type\": type, \"topic\":topic,\"payload\":newPayload, \"event\": \"click\"}\n                console.log(\"Sending msg for clicked toggle button\", tm)\n                $scope.send(tm)\n            } else {\n                var sm = {\"class\": BUTTON_CLASS, \"new type\": type, \"topic\":topic,\"payload\":payload, \"event\": \"click\"}\n                console.log(\"Sending msg for clicked button\", tm)\n                $scope.send(sm)\n            }\n        });\n        \n    };\n\n    //watch for node-red msgs\n    $scope.$watch('msg', function(msg) {\n        //debugger\n        if(!msg){ //if no msg \n            console.log(\"$scope.$watch('msg', ...) - msg is empty\");\n            return;\n        }\n        if(!msg.topic){ //if no topic set found\n            console.log(\"msg.topic is empty - cannot match this to any button\")\n            return; //stop processing!\n        }\n\n        var buttonSelector =  BUTTON_CLASS + \"[data-topic='\" + msg.topic + \"']\" \n        var $btn = $(buttonSelector);//get the button\n        \n        if(!$btn.length){ //if no button found\n            console.log(buttonSelector + \" not found - cannot set state\")\n            return; //stop processing!\n        }\n        \n        if($btn.length > 1){ //if MORE than one button found\n            console.log(buttonSelector + \" found more than 1 button - is this intended? Do you have the same data-topic set on multiple buttons?\")\n        }\n        \n        //see if this is a command - if so, process the command\n        if(typeof msg.payload === \"object\" && msg.payload.command){\n            processCommand($btn, msg.payload)\n            return;\n        }        \n        \n        if($btn.data(\"buttontype\") === \"radio\"){\n//            setRadioButton($btn, false);\n            setRadioButton($btn, true);\n        }\n        \n        if($btn.data(\"buttontype\") === \"toggle\"){\n            if(msg.payload == \"1\"){\n                setButtonState($btn, 1);\n            } else if(msg.payload == \"0\"){\n                setButtonState($btn, 0);\n            } else {\n               console.log(\"Invalid toggle value in msg.payload, cannot set \" + buttonSelector + \". Ensure msg.payload is either 0 or 1\") \n            }\n        }\n\n    }); \n    \n    /** \n    * helper function to set the correct icon & update the \"data-payload\" memory \n    */\n    function setButtonState($btn, state){\n        console.log(\"setButtonState\", $btn[0], state);\n        var oldState = $btn.data(\"state\");\n        $btn.data(\"state\", state);//set data-payload to new state value (used as a memory)\n        \n        //determine the opposite state\n        var oppositeState;\n        if(state == \"1\" || state === 1){\n            state = 1; //normalise to a number\n            oppositeState = 0;\n        } else {\n            state = 0; //normalise to a number\n            oppositeState = 1;\n        }\n        \n        var $icon = $btn.find(\"i\"); //get the <i> element\n        if(!$icon.length){\n            $icon = $btn.find(\"span\"); //get the <span> element instead!\n        }\n        if(!$icon.length){\n            console.log(\"<i> or <span> not found inside button - cant toggle the icon!\")\n            return oldState;//exit this function - nothing to toggle!\n        }\n        \n        //get the old icon and new icon names\n        var oldIcon = $btn.data(\"icon\" + oppositeState); //get icon1 or icon2 depending on oppositeState\n        var newIcon = $btn.data(\"icon\" + state); //get icon1 or icon2 depending on newPayload\n       \n        //if we have newIcon and an actual DOM element ($icon) - update it...\n        if(newIcon && $icon.length){\n            if(newIcon.includes(\"fa-\")){ \n                $icon.removeClass(oldIcon).addClass(newIcon);  // fontawesome\n            } else { \n                $icon.text(newIcon); // MDI\n            }\n        }\n        return oldState;\n    }\n\n\n    /** \n    * helper function \n    */\n    function setRadioButton($btn, sendMsgs){\n        var topic = $btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n        var group = $btn.data(\"radiogroup\");\n        var groupBtns = $(\"[data-radiogroup='\"+group+\"']\");\n        var m = null;\n        for(var i = 0; i < groupBtns.length; i++) {\n            var oldState;\n            var rb = $(groupBtns[i]);\n            if(!rb || !rb.length) continue;\n            var t = rb.data(\"topic\");\n            if(t == topic) continue;//skip clicked button\n            oldState = setButtonState(rb,0);\n            if(sendMsgs && SEND_0_FOR_CLEARED_RADIO_BUTTONS) {\n                if(SEND_ONLY_STATE_CHANGES_RADIO_BUTTONS && oldState == 0) continue;\n                m = {\"type\": \"radio\", \"topic\":t,\"payload\":0, \"event\": \"click\"};\n                console.log(\"Sending 0 payload for other radio button in group\", m)\n                $scope.send(m);\n            }\n        }\n        //if(sendMsgs) {\n//            var p = $btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n//            var m = {\"type\": \"radio\", \"topic\":topic, \"payload\": p, \"event\": \"click\"};\n            var m = {\"type\": \"radio\", \"topic\":topic, \"payload\": 1, \"event\": \"click\"};\n            console.log(\"Sending msg for radio button\", m)\n            $scope.send(m);\n    //    }\n        setButtonState($btn,1);//set state of this button\n/*\n        setTimeout(function(){\n            setButtonState($btn,1);//set state of this button    \n        },200)        \n*/\n    }\n        \n    function processCommand($btn, payload){\n        //first check payload is correct format...\n        if(!payload || !payload.command || !payload.value){\n            console.log(\"Cannot process command. Expected a payload object with .command and .value. \")\n        }\n        var cmd = payload.command.trim();\n        switch(cmd){\n            case \"addClass\":\n                $btn.addClass(payload.value); //this calls the jquery function by name (specified in .command) on the $btn and passes in .value\n                break;\n            case \"toggleClass\":\n                $btn.toggleClass(payload.value); //this calls the jquery function by name (specified in .command) on the $btn and passes in .value\n                break;\n            case \"removeClass\":\n                $btn.removeClass(payload.value); //this calls the jquery function by name (specified in .command) on the $btn and passes in .value\n                break;\n            default:\n                console.log(\"command '\" + payload.command + \"' is not supported\")\n        }\n    }        \n        \n    /** \n    * helper function to determine a value is REALLY a number \n    */\n    function isNumeric(n){\n        if(n === \"\") return false;\n        if(n === true || n === false) return false;\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n   \n\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":350,"y":500,"wires":[["2f8b4bb4.988a54","45f8ba3d.353504"]]},{"id":"45f8ba3d.353504","type":"function","z":"503e64bc.acab6c","name":"keypad","func":"function arrayClone( arr ) {\n\n    var i, copy;\n\n    if( Array.isArray( arr ) ) {\n        copy = arr.slice( 0 );\n        for( i = 0; i < copy.length; i++ ) {\n            copy[ i ] = arrayClone( copy[ i ] );\n        }\n        return copy;\n    } else if( typeof arr === 'object' ) {\n        throw 'Cannot clone array containing an object!';\n    } else {\n        return arr;\n    }\n}\nvar pad = flow.get(node.name) || \n    {\"buttons\": 0, \"states\": 0, \"captions\": [\"\"], \"colours\": [[]]};\nvar button = parseInt(msg.topic,10);\nvar state = parseInt(msg.payload,10);\nvar caption = [];\nvar colour = [];\n\ncaption[0] = msg.caption ? msg.caption[0] : button + \" Off\";\ncaption[1] = msg.caption ? msg.caption[1] : button + \" On\";\n\ncolour[0] = msg.colour ? msg.colour[0] : \"#330000\";\ncolour[1] = msg.colour ? msg.colour[1] : \"#cc0000\";\n\nvar bit = 1<<button;\n\npad.captions[button]=arrayClone(caption);\npad.buttons |= bit;\npad.colours[button]=arrayClone(colour);\nif(state == 1) {\n   pad.states |= bit; \n} else {\n   pad.states &= ~bit; \n}\nflow.set(node.name,pad);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":230,"y":540,"wires":[[]]},{"id":"ade7cf66.d681b","type":"debug","z":"503e64bc.acab6c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":470,"y":180,"wires":[]},{"id":"5b3ef804.f1bbf8","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"11","order":4,"width":"2","height":1,"format":"<!--\n<div>\n   <label>\n      <input type=\"checkbox\" value=\"1\"><span>red</span>\n   </label>\n</div>\n<div>\n   <md-button class=\"md-button remote-button\" \n              data-payload=\"1\" \n              data-buttontype=\"toggle\"\n              data-topic=\"mute\"\n              data-icon0=\"volume_off\"\n              data-icon1=\"volume_mute\"\n              aria-label=\"volume mute\"\n              >\n      <i class=\"material-icons md-48\">volume_mute</i>\n   </md-button>\n</div>\n<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-buttontype='radio'\n    data-radiogroup='group1'\n    data-topic=\"13\"\n    data-icon0=\"fa fa-circle-o\"\n    data-icon1=\"fa fa-check-circle-o\"\n    data-payload=\"13\">\n       <i class=\"fa fa-circle-o\"></i> 13\n   </md-button>\n</div>\n//-->\n\n<div>\n   <md-button class=\"md-button remote-button3 bigger\"\n\n    data-buttontype=\"toggle\"\n    data-topic=\"11\"\n    data-payload=\"11\">\n    <label>\n        <input type=\"checkbox\" value=\"1\">\n        <span>11</span>\n    </label>\n   </md-button>\n</div>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":340,"y":340,"wires":[[]]},{"id":"b85642c8.42ff9","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"12","order":4,"width":"2","height":1,"format":"<!--\n<div>\n   <label>\n      <input type=\"checkbox\" value=\"1\"><span>red</span>\n   </label>\n</div>\n<div>\n   <md-button class=\"md-button remote-button\" \n              data-payload=\"1\" \n              data-buttontype=\"toggle\"\n              data-topic=\"mute\"\n              data-icon0=\"volume_off\"\n              data-icon1=\"volume_mute\"\n              aria-label=\"volume mute\"\n              >\n      <i class=\"material-icons md-48\">volume_mute</i>\n   </md-button>\n</div>\n<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-buttontype='radio'\n    data-radiogroup='group1'\n    data-topic=\"13\"\n    data-icon0=\"fa fa-circle-o\"\n    data-icon1=\"fa fa-check-circle-o\"\n    data-payload=\"13\">\n       <i class=\"fa fa-circle-o\"></i> 13\n   </md-button>\n</div>\n//-->\n\n<div>\n   <md-button class=\"md-button remote-button3 bigger\"\n\n    data-buttontype=\"toggle\"\n    data-topic=\"12\"\n    data-payload=\"12\">\n    <label>\n        <input type=\"checkbox\" value=\"1\">\n        <span>12</span>\n    </label>\n   </md-button>\n</div>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":460,"y":340,"wires":[[]]},{"id":"df724f21.31d9c","type":"function","z":"503e64bc.acab6c","name":"make dynamic style","func":"let stylestring;\nlet defaultBranding;\nlet key;\n\n\n// default values\ndefaultBranding = {\n    buttonColor: '#EAEAEA',\n    buttonOnColor: '#CC0000',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#B0B0B0',\n    buttonTextColor: '#000000',\n    buttonTextColorDisabled: '#AAAAAA',\n};\n\n//override default branding with incoming brand values \nfor (key in defaultBranding) {\n    if (key in msg.payload) {\n        defaultBranding[key] = msg.payload[key];\n    }\n}\n\n// style string template\nstylestring = `\n    :root {\n      --dashboard-unit-width: 48px;\n      --dashboard-unit-height: 48px;\n      --dashboard-min-height: 35px;\n      --dashboard-min-width: 36px;\n      --remote-button-background: black;\n      --remote-button-foreground: #cccccc;\n    }\n    .nr-dashboard-template {\n        padding: 0px;\n    }\n\n.remote-button3 {\n    background-color: [buttonColor] !important;\n    height: var(--dashboard-unit-height);\n    width: 100%;\n    border-radius: 10px;\n    font-size:1em;\n    font-weight:normal;\n    min-height: var(--dashboard-min-height);\n    min-width: unset;\n    line-height: unset;\n  border: 1px solid #D0D0D0;\n  overflow: auto;\n  float: left;\n} \n.remote-button3.disabled {\n    pointer-events: none;\n    opacity: 0.3;\n}\n.remote-button3:hover {\n        /* apply hover effect here */\n        margin:4px;\n        background-color:#EFEFEF;\n        border-radius:10px;\n        border:1px solid #c00;\n        overflow:auto;\n        float:left;\n  }\n.remote-button3.green {\n    background-color: #0c0 !important;\n}\n.remote-button3.bigger{\n    font-weight:bold;\n    font-size:1.5em;\n}\n.remote-button3:disabled {\n    cursor: not-allowed;\n    pointer-events: none;\n    background-color: #222222;\n    color: #666666;\n}\n\n.remote-button3 label {\n    float:left;\n    width: 100%;\n    background-size: contain;\n}\n.remote-button3 label span {\n    border-radius:8px;\n    line-height: var(--dashboard-min-height);\n    width: 100%;\n    padding: 5px;\n    margin: -5px;\n    text-align:center;\n    display:block;\n}\n.remote-button3 label input {\n    position:absolute;\n    top:-20px;\n}\n.remote-button3 input:checked + span {\n    background-color:[buttonOnColor];\n    color:#fff;\n}\n    .remote-icon{\n        font-size:2.0em;\n    }\n    /*  This is the same as the other one, but it makes the icon smaller  */\n    .remote-iconS{\n        font-size:0.5em;\n    }\n\n`;\n\n// replace template values with actual values\n\nfor (key in defaultBranding) {\n    while (stylestring.indexOf('['+key+']') != -1) {\n        stylestring = stylestring.replace('['+key+']', defaultBranding[key]);\n    }\n}\n\nmsg.payload = stylestring;\nreturn msg\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":270,"y":140,"wires":[["f8c69ec5.8552a"]]},{"id":"5e9a81e5.64c3a","type":"function","z":"503e64bc.acab6c","name":"brand 1","func":"msg.payload = {\n    buttonColor: '#CCCCCC',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#999999',\n    buttonTextColor: '#FF0000',\n    buttonTextColorDisabled: '#CCCCCC',\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":240,"y":80,"wires":[["df724f21.31d9c"]]},{"id":"c4e35e8a.52bdf","type":"inject","z":"503e64bc.acab6c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"0.800","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":40,"wires":[["5e9a81e5.64c3a","4700af0c.062a7"]]},{"id":"f8c69ec5.8552a","type":"ui_template","z":"503e64bc.acab6c","group":"c3500612.c24038","name":"apply styles","order":1,"width":0,"height":0,"format":"\n<div></div>\n<script>\n(function(scope) {\n  scope.$watch('msg', function(msg) {\n    if (msg) {\n        var stylesheet = document.createElement('style');\n        stylesheet.id = 'overrided-styles';\n        stylesheet.innerHTML = msg.payload;\n        document.head.appendChild(stylesheet);\n    }\n  });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":430,"y":140,"wires":[["ade7cf66.d681b"]]},{"id":"e2f30ea5.63bb2","type":"function","z":"503e64bc.acab6c","name":"brand 2","func":"msg.payload = {\n    buttonColor: '#000000',\n    buttonOnColor: '#0000CC',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#999999',\n    buttonTextColor: '#FF0000',\n    buttonTextColorDisabled: '#CCCCCC',\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":440,"y":80,"wires":[["df724f21.31d9c"]]},{"id":"4700af0c.062a7","type":"delay","z":"503e64bc.acab6c","name":"","pauseType":"delay","timeout":"300","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":520,"y":40,"wires":[["e2f30ea5.63bb2","c09f09d8.620998"]]},{"id":"67539af2.4de894","type":"function","z":"503e64bc.acab6c","name":"brand 3","func":"msg.payload = {\n    buttonColor: '#000000',\n    buttonOnColor: '#00CC00',\n    buttonColorHover: '#FFFFFF',\n    buttonColorDisabled: '#999999',\n    buttonTextColor: '#FF0000',\n    buttonTextColorDisabled: '#CCCCCC',\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":640,"y":80,"wires":[["df724f21.31d9c"]]},{"id":"c09f09d8.620998","type":"delay","z":"503e64bc.acab6c","name":"","pauseType":"delay","timeout":"300","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":730,"y":40,"wires":[["67539af2.4de894"]]},{"id":"c3500612.c24038","type":"ui_group","z":"","name":"Btn1","tab":"94f74e9f.6a717","order":1,"disp":true,"width":"2","collapse":false},{"id":"94f74e9f.6a717","type":"ui_tab","z":"503e64bc.acab6c","name":"TEST","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

Change the whole CSS that often is not correct way. Mostly the CSS is static. And it contains all what is needed to decorate all elements all the way. The option to change the CSS on fly (like change branding) will not make that rule any different. It still must hold all what is needed and should be able to stay as static.
So make one static CSS to support one branding and then if you like you can change the brand.

And adding the new CSS by doing so is also bad idea, if it exists already, just the content should be replaced.

Thank you. I suspected that would be an abusive use case. I still do not really understand the big picture yet.

I can toggle the button colour in the browser and allow branding.

I have a pure css text toggle but cannot get it to work in a ui_template.

The snippet of html is:

    input#cb3.tgl.tgl-skewed(type = "checkbox")
    label.tgl-btn(data-tg-off = "OFF", data-tg-on = "ON", for = "cb3")

And the css is:

.tgl {
	display: none;
  
	// add default box-sizing for this scope
	&,
  &:after,
  &:before,
	& *,
  & *:after,
  & *:before,
	& + .tgl-btn {
		box-sizing: border-box;
		&::selection {
			background: none;
		}
	}
  
	+ .tgl-btn {
		outline: 0;
		display: block;
		width: 4em;
		height: 2em;
		position: relative;
		cursor: pointer;
    user-select: none;
		&:after,
    &:before {
			position: relative;
			display: block;
			content: "";
			width: 50%;
			height: 100%;
		}
    
		&:after {
			left: 0;
		}
    
		&:before {
			display: none;
		}
	}
  
	&:checked + .tgl-btn:after {
		left: 50%;
	}
}

// themes
.tgl-light {
	+ .tgl-btn {
		background: #f0f0f0;
		border-radius: 2em;
		padding: 2px;
		transition: all .4s ease;
		&:after {
			border-radius: 50%;
			background: #fff;
			transition: all .2s ease;
		}
	}
  
	&:checked + .tgl-btn {
		background: #9FD6AE;
	}
}

.tgl-ios {
	+ .tgl-btn {
		background: #fbfbfb;
		border-radius: 2em;
		padding: 2px;
		transition: all .4s ease;
		border: 1px solid #e8eae9;
		&:after {
			border-radius: 2em;
			background: #fbfbfb;
			transition:
        left .3s cubic-bezier(
          0.175, 0.885, 0.320, 1.275
        ),
        padding .3s ease, margin .3s ease;
			box-shadow:
        0 0 0 1px rgba(0,0,0,.1),
        0 4px 0 rgba(0,0,0,.08);
		}
    
    &:hover:after {
      will-change: padding;
    }
    
		&:active {
			box-shadow: inset 0 0 0 2em #e8eae9;
			&:after {
				padding-right: .8em;
			}
		}
	}
  
	&:checked + .tgl-btn {
    background: #86d993;
    &:active {
      box-shadow: none;
      &:after {
        margin-left: -.8em;
      }
    }
	}
}

.tgl-skewed {
	+ .tgl-btn {
		overflow: hidden;
		transform: skew(-10deg);
		backface-visibility: hidden;
		transition: all .2s ease;
		font-family: sans-serif;
		background: #888;
		&:after,
    &:before {
			transform: skew(10deg);
			display: inline-block;
			transition: all .2s ease;
			width: 100%;
			text-align: center;
			position: absolute;
			line-height: 2em;
			font-weight: bold;
			color: #fff;
			text-shadow: 0 1px 0 rgba(0,0,0,.4);
		}
    
		&:after {
			left: 100%;
			content: attr(data-tg-on);
		}
    
		&:before {
			left: 0;
			content: attr(data-tg-off);
		}
    
		&:active {
			background: #888;
			&:before {
				left: -10%;
			}
		}
	}
  
	&:checked + .tgl-btn {
    background: #86d993;
    &:before {
      left: -100%;
    }

    &:after {
      left: 0;
    }

    &:active:after {
      left: 10%;
    }
	}
}

Not sure how to implement this under md. Any ideas ?

I have successfully used this method to inject text and colour:

<md-button class="touch filled smallfont rounded" 
    style="background-color:{{msg.keypad.colour[2].btn}}"
    ng-bind-html="msg.keypad.caption[2]"
    ng-click="send({payload: 2, colour_group: 'cg4', topic: 'toggle3'})"> 
</md-button> 

but I cannot figure out how to do this:

https://codepen.io/mallendeo/pen/eLIiG/

What do you think ?

Well that is not pure CSS but seems to be SASS which is really not my cup of tea. And I'm not in the mood to take any lessons about it right now. So sorry...

No problem. I will have a go.

Will using SASS pull in any additional js libraries to a standard core app ?

Must admit when you start wanting to get this far under the lid you may want to look at the ui builder nodes that let you have complete control of the front end tools and libraries you may wish to use to create a dashboard.

As far I know it takes compiling step so no way directly to use it as css. And to red out and implement something based on somebody's sass needs to knowledge base which I don't have.

Just ran it through a SASS to CSS and it does not look too shabby. Will check tomorrow.

Hi thanks for your comment. I am quite happy with the dashboard in an embedded app. Also I do not have the skill set.

Can you tell me the most efficient way of updating content ?

With ui_template like this

<script>
(function(scope) {
  scope.$watch('msg', function(msg) {
    if (msg) {
        //check if style exists
        var stylesheet = document.getElementById('overrided-styles') 
        if(!stylesheet){
            // if not found, create one
         stylesheet = document.createElement('style');
            stylesheet.id = 'overrided-styles';
            document.head.appendChild(stylesheet);
        }
        // write/overwrite css content
        stylesheet.innerHTML = msg.payload;
    }
  });
})(scope);
</script>

Thank you. This is for header css I assume. I am trying to form a picture of where and what is going on but it is difficult.

What about body content like button text ?

Maybe it is better to find it out by reading some documents. I doubt I can do better.
And then it is maybe chance to discuss about more specifically targeted problems.


Thank you. My problem is visualizing at a higher level and within the context of node-red dashboard where the code is actually running. Nothing at w3s will tell me anything about where a dashboard ui_template fits in.

Where is the ui_template in the mvc message loop for instance ?

I just wish to tkow what is the most efficient way (in cpu terms) of updating ui_template md button text ?

No no. All inside ui_template is html css and javascript. And for every problem there is answer in w3s. Only if it is angular specific then yes that also adds a bit but basic is still same. There isn't much node-red specific.

And runs in the browser.

1 Like