Ui_template HDMI_TV_control - fixed css flashing

The colour change on payload is pretty similar to toggle. Currently used inject nodes can be replaced with any source, just the payload and topic must respect the controller logic.

With that same example and slight change in CSS (removed the !important from remote-button)

[{"id":"bb9bb4b4.35ea68","type":"ui_template","z":"ce26ccc2.6674d","group":"2a7130fc.d53a2","name":"CSS only ","order":7,"width":0,"height":0,"format":"<style>\n    :root {\n      --dashboard-unit-width: 48px;\n      --dashboard-unit-height: 48px;\n      --remote-button-background: black;\n      --remote-button-foreground: #cccccc;\n    }\n    .nr-dashboard-template {\n        padding: 0px;\n    }\n    \n    .remote-button.disabled{\n        cursor: not-allowed;\n        pointer-events: none;\n        color: #aaaaaa !important;\n    }\n    \n    .remote-button:not([disabled]):hover{\n         background-color: #232323 !important;\n    }\n\n    /*   This is the normal button definition  */\n    .remote-button{\n        background-color: var(--remote-button-background);\n        color: var(--remote-button-foreground);\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    /*  This is a sub-set which is invoked by */\n    /*  <md-button class=\"md-button remote-button bigger\"> */\n    /*  note the (space) \"bigger\" at the end.  */\n    .remote-button.bigger{\n        font-weight:bold;\n        font-size:1.5em;\n    }\n    /*  This is for buttons with a lot of text.  `font-size:0.7em` */\n    /*  makes the font 70% normal size  */\n    .remote-button.small{\n        font-size:0.7em;\n    }\n    /*  This is for buttons with just icons, to upsize the size */\n    /*  of the icon with the line: */\n    /*  <i class=\"fa fa-fw fa-plus remote-icon\"> in the other node  */\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    .remote-button.black{\n        background-color: var(--remote-button-background) !important;\n        color: var(--remote-button-foreground) !important;\n    }\n\n    .remote-button.red{\n        background-color: red !important;\n        color: var(--remote-button-foreground) !important;\n    }\n    .remote-button.red:not([disabled]):hover{\n         background-color: orange !important;\n    }\n\n    .remote-button.blinking{\n    \tanimation:blinkingAnim1 0.8s infinite;\n    }\n    @keyframes blinkingAnim1{\n    \t0%{\t\tbackground-color: var(--remote-button-background); color:var(--remote-button-foreground);}\n    \t50%{\tbackground-color: var(--remote-button-foreground); color:var(--remote-button-background);}\n    \t100%{\tbackground-color: var(--remote-button-background);\tcolor:var(--remote-button-foreground);}\n    }    \n     \n    .remote-button.red.blinking{\n    \tanimation:blinkingAnim2 0.8s infinite;\n    }\n    @keyframes blinkingAnim2{\n    \t0%{\t\tbackground-color: red; color:#cccccc;}\n    \t50%{\tbackground-color: #cccccc; color:red;}\n    \t100%{\tbackground-color: red;\tcolor:#cccccc;}\n    }     \n</style>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"global","x":480,"y":360,"wires":[[]]},{"id":"39b5b695.3e656a","type":"ui_template","z":"ce26ccc2.6674d","group":"2a7130fc.d53a2","name":"script for all buttons with class remote-button","order":27,"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    var BUTTON_CLASS = \".remote-button\";\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 + \":not([data-buttontype='repeater'])\") \n        clickButtons.click(function(e){\n            //debugger\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            \n            if(type == \"toggle\"){\n                var newPayload = payload == 1 ? 0 : 1;\n                setButtonState(btn,newPayload)\n                $scope.send({\"topic\":topic,\"payload\":newPayload, \"event\": \"click\"})\n            } else {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            }\n        }); \n        \n\n        var repeatButtons = $(BUTTON_CLASS + \"[data-buttontype='repeater']\") \n        repeatButtons.on('mousedown touchstart',function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            console.log(e.type);//remove me after debugging\n            var btn = this;\n            var $btn = $(btn);\n            if(btn._intervalId) return; //already in operation\n            btn._topic = $btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            btn._payload = $btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            btn._interval = $btn.data(\"interval\");//get the desired repeat duration\n            if(isNumeric(btn._interval) == false || btn._interval < 1) btn._interval = 300;//prevent zero & non numeric timeout value\n            var max = $btn.data(\"max\");//get the max repeat count\n            if(isNumeric(max) && max > 0) \n                btn._max = parseInt(max);\n            else\n                btn._max = undefined;\n            btn._count = 1;\n            $scope.send({\"topic\":btn._topic,\"payload\":btn.payload,\"event\":\"down\", \"count\": btn._count})\n            btn._lastevent = \"down\";\n            //remove any existing timer\n            if(btn._intervalId){\n                clearInterval(btn._intervalId);\n                btn._intervalId = null;\n            }\n            //start the timer if conditions are ok\n            if(!btn._max || (btn._max && btn._count < btn._max)){\n                btn._intervalId = setInterval(function() {\n                    if(btn._max && btn._count >= btn._max){\n                        clearInterval(btn._intervalId);\n                        btn._intervalId = null;\n                        return;\n                    }                    \n                    btn._count = btn._count + 1;\n                    $scope.send({\"topic\":btn._topic,\"payload\":btn.payload,\"event\":\"down\", \"count\": btn._count})\n                },btn._interval); \n            }\n        }).on('mouseup blur focusout mouseout touchend',function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            console.log(e.type);//remove me after debugging\n            var btn = this;\n            var $btn = $(btn);\n            \n            //next, if data-fireonup == true, then fire up event\n            var sendUpEvent = $btn.data(\"fireonup\") == true;\n            if(sendUpEvent && btn._lastevent === \"down\"){\n                btn._lastevent = \"up\"\n                $scope.send({\"topic\":btn._topic,\"payload\":btn._payload,\"event\":\"up\"})\n            }\n            \n            //finally, if the timer id is still valid, clear it (stop repeater)\n            if(btn._intervalId){\n                clearInterval(btn._intervalId);//stop timer\n                btn._intervalId = null;//clear timer id\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\") === \"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        \n        $btn.data(\"payload\", 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;//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    }\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":true,"resendOnRefresh":true,"templateScope":"local","x":570,"y":540,"wires":[["efa774db.22c848"]]},{"id":"efa774db.22c848","type":"debug","z":"ce26ccc2.6674d","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":810,"y":540,"wires":[]},{"id":"a7dda2e3.f6399","type":"ui_template","z":"ce26ccc2.6674d","group":"2a7130fc.d53a2","name":"lock","order":23,"width":"1","height":"1","format":"<div>\n   <md-button class=\"md-button remote-button bigger\" \n              data-payload=\"0\" \n              data-buttontype=\"toggle\"\n              data-topic=\"lock\"\n              data-icon0=\"fa-unlock\"\n              data-icon1=\"fa-lock\"\n              aria-label=\"lock\"\n              >\n      <span class=\"fa fa-unlock\" aria-hidden=\"true\"> </span>\n   </md-button>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":470,"y":420,"wires":[[]]},{"id":"cdcdf10b.a0385","type":"inject","z":"ce26ccc2.6674d","name":"toggleClass blinking, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"toggleClass\",\"value\":\"blinking\"}","payloadType":"json","x":220,"y":500,"wires":[["39b5b695.3e656a"]]},{"id":"9ee376a6.930f58","type":"inject","z":"ce26ccc2.6674d","name":"addClass red, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"addClass\",\"value\":\"red\"}","payloadType":"json","x":240,"y":540,"wires":[["39b5b695.3e656a"]]},{"id":"fde928e1.9d7358","type":"inject","z":"ce26ccc2.6674d","name":"removeClass red, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"removeClass\",\"value\":\"red\"}","payloadType":"json","x":250,"y":580,"wires":[["39b5b695.3e656a"]]},{"id":"feedb55e.f4d598","type":"inject","z":"ce26ccc2.6674d","name":"addClass disabled, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"addClass\",\"value\":\"disabled\"}","payloadType":"json","x":250,"y":620,"wires":[["39b5b695.3e656a"]]},{"id":"16ec8c00.3ba3b4","type":"inject","z":"ce26ccc2.6674d","name":"removeClass disabled, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"removeClass\",\"value\":\"disabled\"}","payloadType":"json","x":260,"y":660,"wires":[["39b5b695.3e656a"]]},{"id":"2a7130fc.d53a2","type":"ui_group","z":"","name":"Full_Remote2","tab":"c8123658.c35e48","order":3,"disp":false,"width":"3","collapse":false},{"id":"c8123658.c35e48","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

Many thanks. I have finally figured it out. You were right about important. It was not possible I think to allow all classes to mix if it is used everywhere.

It now blinks with/without colour and with/without enabled. Here is the flow for others.

[{"id":"f3f92789.a63df8","type":"tab","label":"Flow 4","disabled":false,"info":""},{"id":"702e0d42.fd6f74","type":"ui_template","z":"f3f92789.a63df8","group":"a09afa7e.0abe68","name":"script for all buttons with class remote-button","order":27,"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    var BUTTON_CLASS = \".remote-button\";\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 + \":not([data-buttontype='repeater'])\") \n        clickButtons.click(function(e){\n            //debugger\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            \n            if(type == \"toggle\"){\n                var newPayload = payload == 1 ? 0 : 1;\n                setButtonState(btn,newPayload)\n                $scope.send({\"topic\":topic,\"payload\":newPayload, \"event\": \"click\"})\n            } else {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            }\n        }); \n        \n\n        var repeatButtons = $(BUTTON_CLASS + \"[data-buttontype='repeater']\") \n        repeatButtons.on('mousedown touchstart',function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            console.log(e.type);//remove me after debugging\n            var btn = this;\n            var $btn = $(btn);\n            if(btn._intervalId) return; //already in operation\n            btn._topic = $btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            btn._payload = $btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            btn._interval = $btn.data(\"interval\");//get the desired repeat duration\n            if(isNumeric(btn._interval) == false || btn._interval < 1) btn._interval = 300;//prevent zero & non numeric timeout value\n            var max = $btn.data(\"max\");//get the max repeat count\n            if(isNumeric(max) && max > 0) \n                btn._max = parseInt(max);\n            else\n                btn._max = undefined;\n            btn._count = 1;\n            $scope.send({\"topic\":btn._topic,\"payload\":btn.payload,\"event\":\"down\", \"count\": btn._count})\n            btn._lastevent = \"down\";\n            //remove any existing timer\n            if(btn._intervalId){\n                clearInterval(btn._intervalId);\n                btn._intervalId = null;\n            }\n            //start the timer if conditions are ok\n            if(!btn._max || (btn._max && btn._count < btn._max)){\n                btn._intervalId = setInterval(function() {\n                    if(btn._max && btn._count >= btn._max){\n                        clearInterval(btn._intervalId);\n                        btn._intervalId = null;\n                        return;\n                    }                    \n                    btn._count = btn._count + 1;\n                    $scope.send({\"topic\":btn._topic,\"payload\":btn.payload,\"event\":\"down\", \"count\": btn._count})\n                },btn._interval); \n            }\n        }).on('mouseup blur focusout mouseout touchend',function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            console.log(e.type);//remove me after debugging\n            var btn = this;\n            var $btn = $(btn);\n            \n            //next, if data-fireonup == true, then fire up event\n            var sendUpEvent = $btn.data(\"fireonup\") == true;\n            if(sendUpEvent && btn._lastevent === \"down\"){\n                btn._lastevent = \"up\"\n                $scope.send({\"topic\":btn._topic,\"payload\":btn._payload,\"event\":\"up\"})\n            }\n            \n            //finally, if the timer id is still valid, clear it (stop repeater)\n            if(btn._intervalId){\n                clearInterval(btn._intervalId);//stop timer\n                btn._intervalId = null;//clear timer id\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\") === \"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        \n        $btn.data(\"payload\", 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;//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    }\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":true,"resendOnRefresh":true,"templateScope":"local","x":470,"y":280,"wires":[["41950e27.73b81"]]},{"id":"41950e27.73b81","type":"debug","z":"f3f92789.a63df8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":710,"y":280,"wires":[]},{"id":"c2f58574.2135c8","type":"ui_template","z":"f3f92789.a63df8","group":"a09afa7e.0abe68","name":"lock","order":23,"width":"1","height":"1","format":"<div>\n   <md-button class=\"md-button remote-button bigger\" \n              data-payload=\"0\" \n              data-buttontype=\"toggle\"\n              data-topic=\"lock\"\n              data-icon0=\"fa-unlock\"\n              data-icon1=\"fa-lock\"\n              aria-label=\"lock\"\n              >\n      <span class=\"fa fa-unlock\" aria-hidden=\"true\"> </span>\n   </md-button>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":370,"y":160,"wires":[[]]},{"id":"3b0f8ff8.fc079","type":"inject","z":"f3f92789.a63df8","name":"toggleClass blinking, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"toggleClass\",\"value\":\"blinking\"}","payloadType":"json","x":160,"y":160,"wires":[["702e0d42.fd6f74"]]},{"id":"209d07b3.ae4688","type":"inject","z":"f3f92789.a63df8","name":"addClass red, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"addClass\",\"value\":\"red\"}","payloadType":"json","x":140,"y":280,"wires":[["702e0d42.fd6f74"]]},{"id":"fbb0fdc0.493f9","type":"inject","z":"f3f92789.a63df8","name":"removeClass red, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"removeClass\",\"value\":\"red\"}","payloadType":"json","x":150,"y":320,"wires":[["702e0d42.fd6f74"]]},{"id":"7f59f749.696728","type":"inject","z":"f3f92789.a63df8","name":"addClass disabled, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"addClass\",\"value\":\"disabled\"}","payloadType":"json","x":150,"y":360,"wires":[["702e0d42.fd6f74"]]},{"id":"96819625.3b3be8","type":"inject","z":"f3f92789.a63df8","name":"removeClass disabled, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"removeClass\",\"value\":\"disabled\"}","payloadType":"json","x":160,"y":400,"wires":[["702e0d42.fd6f74"]]},{"id":"cd608217.5d4db","type":"ui_template","z":"f3f92789.a63df8","group":"3696abc8.ad5bf4","name":"CSS only ","order":1,"width":0,"height":0,"format":"<style>\n    :root {\n      --dashboard-unit-width: 48px;\n      --dashboard-unit-height: 48px;\n      --remote-button-background: black;\n      --remote-button-foreground: #cccccc;\n    }\n    .nr-dashboard-template {\n        padding: 0px;\n    }\n   \n    .remote-button:not([disabled]):hover{\n         background-color: #232323 !important;\n    }\n\n    .remote-button.red:not([disabled]) {\n        background-color: #cc0000;\n        color: var(--remote-button-foreground);\n    }\n    .remote-button.green:not([disabled]) {\n        background-color: #00bb00;\n        color: var(--remote-button-foreground);\n    }\n    \n\n    /*   This is the normal button definition  */\n    .remote-button{\n        background-color: var(--remote-button-background) !important;\n        color: var(--remote-button-foreground);\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    /*  This is a sub-set which is invoked by */\n    /*  <md-button class=\"md-button remote-button bigger\"> */\n    /*  note the (space) \"bigger\" at the end.  */\n    .remote-button.bigger{\n        font-weight:bold;\n        font-size:1.5em;\n    }\n    /*  This is for buttons with a lot of text.  `font-size:0.7em` */\n    /*  makes the font 70% normal size  */\n    .remote-button.small{\n        font-size:0.7em;\n    }\n    /*  This is for buttons with just icons, to upsize the size */\n    /*  of the icon with the line: */\n    /*  <i class=\"fa fa-fw fa-plus remote-icon\"> in the other node  */\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    .remote-button.red{\n        background-color: #cc0000 !important;\n    }\n    .remote-button.green{\n        background-color: #00bb00 !important;\n    }\n    .remote-button.blue{\n        background-color: #0000dd !important;\n    }\n    .remote-button.red.disabled {\n        background-color: #440000 !important;\n        color: #666666 !important;\n    }\n    .remote-button.green.disabled {\n        background-color: #004400 !important;\n        color: #666666 !important;\n    }\n    .remote-button.blue.disabled {\n        background-color: #000066 !important;\n        color: #666666 !important;\n    }\n    .remote-button.disabled {\n        cursor: not-allowed;\n        pointer-events: none;\n        background-color: #222222;\n        color: #666666;\n    }\n\n    .remote-button.blinking {\n        animation: blink-animation 1s steps(5, start) infinite;\n        animation-timing-function: ease;\n        -webkit-animation: blink-animation 1s steps(5, start) infinite;\n    }\n    @keyframes blink-animation {\n        to { visibility: hidden; }\n    }\n    @-webkit-keyframes blink-animation {\n        to { visibility: hidden; }\n    }\n.remote-button.warning {\n  color: red;\n}\n\n.remote-button.animated .remote-button.warning {\n  -webkit-animation: pulse 1s infinite;\n  animation: pulse 1s infinite;\n}\n\n@-webkit-keyframes pulse {\n  0% {\n    opacity: 0.2\n  }\n\n  35% {\n    opacity: 1.0\n  }\n\n  100% {\n    opacity: 0.2\n  }\n}\n\n@keyframes pulse {\n  0% {\n    opacity: 0.2\n  }\n\n  35% {\n    opacity: 1.0\n  }\n\n  100% {\n    opacity: 0.2\n  }\n}\n\n</style>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"global","x":380,"y":80,"wires":[[]]},{"id":"ecc3560a.b345a8","type":"inject","z":"f3f92789.a63df8","name":"toggleClass red, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"toggleClass\",\"value\":\"red\"}","payloadType":"json","x":140,"y":200,"wires":[["702e0d42.fd6f74"]]},{"id":"e53aec98.c115","type":"inject","z":"f3f92789.a63df8","name":"toggleClass disabled, on lock","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"lock","payload":"{\"command\":\"toggleClass\",\"value\":\"disabled\"}","payloadType":"json","x":160,"y":240,"wires":[["702e0d42.fd6f74"]]},{"id":"a09afa7e.0abe68","type":"ui_group","z":"","name":"Full_Remote2","tab":"97aedddc.a3a72","order":3,"disp":false,"width":"3","collapse":false},{"id":"3696abc8.ad5bf4","type":"ui_group","z":"","name":"Full_Remote2","tab":"f0631d88.423c4","order":3,"disp":false,"width":"3","collapse":false},{"id":"97aedddc.a3a72","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false},{"id":"f0631d88.423c4","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

Next:
How do I simply (without adding too much code) synchronise the flashes ?
Can I supply a user generated colour instead of predefined ones ?
How can the button state determine which colour is used ?

  1. You can't sync the frame animation at object level. You'll need to turn every currently blinking off and then turn all needed blinking's on simultaneously. https://stackoverflow.com/questions/42946700/css-synchronise-keyframe-animations

  2. Colors are specified using predefined color names, or RGB, HEX, HSL, RGBA, HSLA values. https://www.w3schools.com/css/css_colors.asp

  3. Button should be dummy. No need for button to know about its look or color or anything like that. It is your decision which class is applied at every moment/situation to make the look of button. Controlling it is whole thing of programming.

May be I didn't understand the third one so you can open it up a little.

I had a few goes at animate sync but no luck.

A little background: I developed a keypad function node for a home control system. This currently allows button colour to be set by the user and save it in a theme. Buttons can also be members of groups (radio group/colour group etc). It also handles flash for button selection or alert purposes. The node also maintains each buttons state and holds a colour for 1 and a colour for 0.

Using a function node and possibly my design has a few disadvantages. The code is 500+ lines. Performance can be tricky because the hardware is minimal and there is a lot of wiring to and from all the buttons.

![image|570x375](upload://Lb3KEOoToXzQF2Q7N4Iw8Ybiy.png) 

The out come is not bad but as always I am looking for a more elegant way of achieving the same end result.

To expand on button state and colour/animation. I am trying to leverage as much from css as I can. Not because I like it but it seems a much more efficient (cpu related) method. Currently I have:

[{"id":"a87458b2.431108","type":"ui_template","z":"6f8127d2.ec1808","group":"dc863d2d.5cc17","name":"toggle1|21:30/22:30 Off,21:30/22:30 21-0","order":18,"width":"2","height":"1","format":"\n<md-button class=\"touch filled smallfont rounded\" \n    style=\"background-color:{{msg.keypad.colour[0].btn}}\"\n    ng-bind-html=\"msg.keypad.caption[0]\"\n    ng-click=\"send({payload: 0, colour_group: 'cg4', topic: 'toggle1'})\"> \n</md-button> \n\n","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":420,"y":320,"wires":[["f24f5691.e5c228"]]},{"id":"dc863d2d.5cc17","type":"ui_group","z":"","name":"Underfloor Heating","tab":"db68c740.395a08","order":1,"disp":false,"width":"8","collapse":false},{"id":"db68c740.395a08","type":"ui_tab","z":"","name":"Underfloor Heating","icon":"dashboard","order":2,"disabled":false,"hidden":false}]

If the button template specifies the source of the colour when state=0 and a different colour source for state=1 then there should be enough information for automatic css colour change.
I am trying to take the best from both approaches.

1 Like

If I understand correctly, you want to have dynamic style definitions
It is not very common of course but still I think doable.

See a little example I did.

[{"id":"64a32b89.4f8ca4","type":"function","z":"ce26ccc2.6674d","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":730,"y":740,"wires":[["5d0742e4.47d91c"]]},{"id":"3faeb5dc.4b5f5a","type":"function","z":"ce26ccc2.6674d","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":550,"y":740,"wires":[["64a32b89.4f8ca4"]]},{"id":"21e460f.e104ea","type":"inject","z":"ce26ccc2.6674d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":400,"y":740,"wires":[["3faeb5dc.4b5f5a"]]},{"id":"5d0742e4.47d91c","type":"ui_template","z":"ce26ccc2.6674d","group":"2a7130fc.d53a2","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":930,"y":740,"wires":[[]]},{"id":"2a7130fc.d53a2","type":"ui_group","z":"","name":"Full_Remote2","tab":"c8123658.c35e48","order":3,"disp":false,"width":"3","collapse":false},{"id":"c8123658.c35e48","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

By running this, the modified stylestring is made and applied to the document head

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