Does ui-template script have access to context?

I have a ui-template node in which I'd like to access some data using flow.get() from within script tags. Should that be possible? I have a very simplified flow to test that with just a plain varaiable but it doesn't work - execution stops doing flow.get()

[{"id":"5f1dce96.0c0ea8","type":"ui_template","z":"5025a90b.1be418","group":"6646c330.b6a0a4","name":"qwerty","order":0,"width":"10","height":"15","format":"\n\n<style>\n    .buttons {\n        width: 160px;\n        height: 30px;\n    }\n    [value=\"off\"] {\n        /*background-color: #999999;*/\n        color: -internal-light-dark(black, white);\n        background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));\n        border-color: -internal-light-dark(rgb(118, 118, 118),rgb(133, 133, 133));\n    }\n    [value=\"on\"] {\n        /*background-color: #000000;*/\n        color: -internal-light-dark(black, white);\n        background-color: rgb(178, 178, 178);\n        border-color: -internal-light-dark(rgb(118, 118, 118),rgb(133, 133, 133));\n    }\n    .holder {\n        display: flex;\n        flex-direction: column;\n    }\n    .deviceList {   /* A set of radio buttons */\n        background-color: #a1a1a1;\n        min-height: 200px;\n        min-width: 490px; \n        margin-top: 10px;\n        margin-bottom: 10px;\n    }\n    .deviceList input[type=\"radio\"] {\n        display: none;\n    }\n    .deviceList label {\n        display: flex;\n        flex-direction: column;\n        background-color: #a1a1a1;\n        padding: 4px 11px;\n        font-family: Arial;\n        font-size: 16px;\n        cursor: pointer;\n    }\n    .deviceList input[type=\"radio\"]:checked+label {\n        background-color: #76cf9f;\n    }\n    .readClearDataButtonHolder {\n        display: flex;\n        flex-direction: row;\n        justify-content: space-between;\n    }\n    .alignButtonRight {\n    }\n    .textArea {\n        background-color: #a1a1a1;\n        min-height: 220px;\n        min-width: 480px; \n        margin-top: 10px;\n        margin-bottom: 10px;\n        resize: none;\n        padding: 0;\n    }\n</style>\n\n\n<!-- FIND DEVICES -->\n<div id=\"findDevicesButtonHolder\">\n    <button id=\"findDevicesButton\" class=\"buttons\">Find devices</button>\n</div>\n<div id=\"deviceListHolder\" class=\"deviceList\">\n</div>\n    <!-- DEVICE INFO -->\n<div id=\"infoButtonHolder\">\n    <button id=\"infoButton\" class=\"buttons\">Device info</button>\n</div>\n<div id=\"infoAreaHolder\" class=\"infoAreaHolder\">\n    <textarea id=\"infoArea\" name=\"infoArea\" class=\"textArea\" rows=\"1\" cols=\"49\" disabled></textarea>\n</div>\n    <!-- DEVICE DATA -->\n<div id=\"readClearDataButtonHolder\" class=\"readClearDataButtonHolder\">\n    <div class=\"alignButtonLeft\">\n        <button id=\"readDataButton\" class=\"buttons\" value=\"off\">Read data</button>\n    </div>\n    <div class=\"alignButtonRight\">\n        <button id=\"clearDataButton\" class=\"buttons\">Clear</button>\n    </div>\n</div>\n<div id=\"dataAreaHolder\" class=\"dataAreaHolder\">\n    <textarea id=\"dataArea\" name=\"dataArea\" class=\"textArea\" rows=\"1\" cols=\"49\" disabled></textarea>\n</div>\n\n<script>\n    (function(scope) {\n        let timer;\n        \n        infoButton.addEventListener('click', function (e) {\n            if (event.detail === 1) {\n                timer = setTimeout(() => {\n                  infoArea.value = infoArea.value + \"\\nCLICK\";\n                }, 300);\n            }\n        });\n        infoButton.addEventListener('dblclick', function (e) {\n            clearTimeout(timer);\n            infoArea.value = infoArea.value + \"\\nDBLCLICK\";\n        });\n        \n        scope.$watch('msg', function (msg) {\n            if (msg.payload == \"newDevice\") {\n                dataArea.value = dataArea.value + \"\\nAAA \" + msg.payload;\n\n                // Testing flow context here\n                var avar = flow.get(\"atest\") || 5;\n                dataArea.value = dataArea.value + \"\\nZZZZZ \" + avar;\n\n                \n            }\n        });\n        \n        \n    })(scope);\n    \n</script>\n\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":670,"y":200,"wires":[[]]},{"id":"6646c330.b6a0a4","type":"ui_group","name":"Default","tab":"ba47fdb1.029a08","order":1,"disp":false,"width":"10","collapse":false},{"id":"ba47fdb1.029a08","type":"ui_tab","name":"X-keys Device Finder","icon":"dashboard","disabled":false,"hidden":false}]

even though context tab in right side bar correctly displays availability of the "atest" flow context. When injecting a newDevice event, I would expect the script in the ui-template to access "atest" (as set by "set/log context var" function node) and display the value in the bottom text area of the dashboard display.

Presuming that context is not available from within the script tags, is there an alternative method to share access to objects between nodes? The test flow uses just a simple variable but my real world usage involves a reasonably complex object.

Thanks for any advice,
chris

Right, the context is not available inside the script tags (hence you got an error in browser console stating that Flow is not valid or something like that). Keep in mind that the code inside the script is running in the browser. Your nodes are part of a Node.JS application running in your Node-RED instance.

Well, code running in the browser can use the browser Fetch API to request resources from an HTTP server. You can therefore create an HTTP endpoint in Node-RED that will produce the required object from the context.

Then the alternative method to explore is 1- Create the HTTP endpoint. 2-Make a fetch request inside the script tags.

This is how the code inside the ui_temple looks like. It is missing error handling to not overcomplicate the explanation.

<script>

    (function(scope) {
        scope.$watch('msg', function (msg) {
            if (msg.payload == "newDevice") {
                
                async function fetchContext() {
                    fetchResult = fetch('http://127.0.0.1:1880/atest');
                    const response = await fetchResult;
                    const jsonData = await response.json();
                    console.log(jsonData);
                }
            fetchContext();
            }
        });
    })(scope);
    
</script>

Testing flow:

[{"id":"41d2d5e53c0c2d08","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"ca0da7891f7d4a10","type":"http in","z":"41d2d5e53c0c2d08","name":"","url":"/atest","method":"get","upload":false,"swaggerDoc":"","x":160,"y":200,"wires":[["9d606b989fed5e67"]]},{"id":"1110814b9c0934ef","type":"inject","z":"41d2d5e53c0c2d08","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":80,"wires":[["b0b49d39e2eea84b"]]},{"id":"b0b49d39e2eea84b","type":"change","z":"41d2d5e53c0c2d08","name":"","rules":[{"t":"set","p":"atest","pt":"flow","to":"{\"Company\":\"aiot\",\"Name\":\"Andrei Ochmat\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":80,"wires":[[]]},{"id":"9d606b989fed5e67","type":"change","z":"41d2d5e53c0c2d08","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"atest","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":200,"wires":[["d5e48b22b1b9fbdf","c08c04abbe04278b"]]},{"id":"d5e48b22b1b9fbdf","type":"http response","z":"41d2d5e53c0c2d08","name":"","statusCode":"","headers":{},"x":510,"y":200,"wires":[]},{"id":"c08c04abbe04278b","type":"debug","z":"41d2d5e53c0c2d08","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":490,"y":240,"wires":[]},{"id":"478d58c9f20c1f95","type":"ui_template","z":"41d2d5e53c0c2d08","group":"6646c330.b6a0a4","name":"","order":1,"width":0,"height":0,"format":"<script>\n\n    (function(scope) {\n        scope.$watch('msg', function (msg) {\n            if (msg.payload == \"newDevice\") {\n                \n                async function fetchContext() {\n                    fetchResult = fetch('http://127.0.0.1:1880/atest');\n                    const response = await fetchResult;\n                    const jsonData = await response.json();\n                    console.log(jsonData);\n                }\n            fetchContext();\n            }\n        });\n    })(scope);\n    \n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":360,"y":340,"wires":[[]]},{"id":"d943c52c82f33c79","type":"inject","z":"41d2d5e53c0c2d08","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"newDevice","payloadType":"str","x":180,"y":340,"wires":[["478d58c9f20c1f95"]]},{"id":"7aba4c506e18f203","type":"comment","z":"41d2d5e53c0c2d08","name":"Store an object in the flow context","info":"","x":230,"y":40,"wires":[]},{"id":"eda547de712428dc","type":"comment","z":"41d2d5e53c0c2d08","name":"Endpoint to expose a way to retrieve the context","info":"","x":280,"y":160,"wires":[]},{"id":"0dfc89e83692a653","type":"comment","z":"41d2d5e53c0c2d08","name":"Example of code that will fetch the URL and get the object from the HTTP server","info":"","x":380,"y":300,"wires":[]},{"id":"6646c330.b6a0a4","type":"ui_group","name":"Default","tab":"ba47fdb1.029a08","order":1,"disp":false,"width":"10","collapse":false},{"id":"ba47fdb1.029a08","type":"ui_tab","name":"X-keys Device Finder","icon":"dashboard","disabled":false,"hidden":false}]

Probably not a comprehensive (and still late) response but hopefully can help you move on.

1 Like

Thanks for the reply Andrei. I worked around that particular problem by making the data (that I had wanted to access via context) available with a bigger msg.payload.

However I now have a similar problem, this time with making data from the runtime available to the configuration editor where I'd like to prefill options in a dropdown selector. I have created an HTTP endpoint for that and so far been trying to use $.getJSON('/xkeys/devices', function(data) {...}) to retrieve the data on the editor side. I have the selector working with pretend options following the TypedInput Select box example at Node edit dialog : Node-RED but can't successfully use real options with the data from $.getJSON yet.

What is the difference between the $.getJSON construct and the fetch('http://127.0.0.1:1880/atest') method? Any pros/cons worth knowing about?

Thanks again,
chris

You should use $.getJSON rather than fetch as the jQuery utility functions are setup to handle authentication if adminAuth is enabled.

  1. make sure the path you use is namespaced to your node to avoid any potential clashes.
  2. in the editor, do not start the path with / - so you should use xkeys/devices. This ensures the request is relative to the page and will still work if the user has set httpAdminRoot to move the root path of the editor.

Thanks, I have that working now.

About the adminAuth (which I don't think I need but am curious) - I see in the serial port node, which is often pointed to as an example of making/using an http endpoint, that the call to RED.httpAdmin.get has RED.auth.needsPermission('serial.read') as one of the parameters. At the reading level it's pretty clear what is intended but what's not clear is who needs the permission to read the serial port? Is it the node (which is doing the reading) or the browser/editor which is asking for the details? Also, what is serial.read - where is it set up and what would I use instead of serial.read to (dis)allow access for other types of data?

In my case, the data being passed around has been obtained by the node from a separate server which mediates access to the actual devices on the USB bus i.e. the node itself doesn't directly communicate with the devices or the USB bus.

chris

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