Displaying an image on UIBuilder page from Node-red dashboard

Hi, I'm trying to create an escape room gm/hint system and one feature I'd like to have is to send through an image (preferably selected from a dropdown menu) to display on the UIBuilder page along with the timer. I've managed to get text hints to work but can't for the life of me figure out how to do it with images. I thought it work work the same sort of way, by passing the url of the image in the message, but whatever I try either doesn't seem to read the message and convert it to an image url, or it just kills the rest of the page completely.

What I have already is using vue and I've tried reading through a lot of their tutorials but can't find anything relevant to this. Am I just missing something somewhere, or is this actually a lot more difficult than I'm imagining?

Here is the flow (just for the text and image hints) I've got currently:

[
    {
        "id": "2a872390.448ac4",
        "type": "ui_dropdown",
        "z": "2ad15a22.7b6726",
        "name": "",
        "label": "Image Hints",
        "tooltip": "",
        "place": "Select option",
        "group": "afbc44fd.ff57c8",
        "order": 5,
        "width": 0,
        "height": 0,
        "passthru": true,
        "multiple": false,
        "options": [
            {
                "label": "clocks",
                "value": "http://localhost:1880/clocks1.jpg",
                "type": "str"
            },
            {
                "label": "table",
                "value": "<img src=http://localhost:1880/hint_screen_back.jpg>",
                "type": "str"
            }
        ],
        "payload": "",
        "topic": "",
        "x": 90,
        "y": 360,
        "wires": [
            [
                "acaa2182.89e168"
            ]
        ]
    },
    {
        "id": "acaa2182.89e168",
        "type": "change",
        "z": "2ad15a22.7b6726",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "imageSrc",
                "pt": "msg",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 510,
        "y": 400,
        "wires": [
            [
                "68d5ae2d.f92c6"
            ]
        ]
    },
    {
        "id": "68d5ae2d.f92c6",
        "type": "join",
        "z": "2ad15a22.7b6726",
        "name": "",
        "mode": "custom",
        "build": "merged",
        "property": "",
        "propertyType": "full",
        "key": "topic",
        "joiner": "\\n",
        "joinerType": "str",
        "accumulate": true,
        "timeout": "",
        "count": "1",
        "reduceRight": false,
        "reduceExp": "",
        "reduceInit": "",
        "reduceInitType": "num",
        "reduceFixup": "",
        "x": 950,
        "y": 460,
        "wires": [
            [
                "90030e4c.fe9ed"
            ]
        ]
    },
    {
        "id": "90030e4c.fe9ed",
        "type": "uibuilder",
        "z": "2ad15a22.7b6726",
        "name": "Game Timer and Clue Display",
        "topic": "",
        "url": "lab",
        "fwdInMessages": false,
        "allowScripts": false,
        "allowStyles": false,
        "copyIndex": true,
        "showfolder": false,
        "x": 1130,
        "y": 460,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "3c3317af.5f16b8",
        "type": "change",
        "z": "2ad15a22.7b6726",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "clueText",
                "pt": "msg",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 710,
        "y": 500,
        "wires": [
            [
                "68d5ae2d.f92c6",
                "aeccdd77.be2a9"
            ]
        ]
    },
    {
        "id": "888f472c.94d1c8",
        "type": "ui_text_input",
        "z": "2ad15a22.7b6726",
        "name": "",
        "label": "Hint",
        "tooltip": "",
        "group": "afbc44fd.ff57c8",
        "order": 1,
        "width": 0,
        "height": 0,
        "passthru": false,
        "mode": "text",
        "delay": "0",
        "topic": "",
        "x": 70,
        "y": 480,
        "wires": [
            [
                "1a77f5dd.9db63a",
                "3c3317af.5f16b8"
            ]
        ]
    },
    {
        "id": "afbc44fd.ff57c8",
        "type": "ui_group",
        "z": "",
        "name": "Hints",
        "tab": "1c294719.eeb109",
        "order": 2,
        "disp": false,
        "width": "10",
        "collapse": false
    },
    {
        "id": "1c294719.eeb109",
        "type": "ui_tab",
        "z": "",
        "name": "Home",
        "icon": "dashboard",
        "disabled": false,
        "hidden": false
    }
]

The HTML:

<!doctype html>
<html lang="en">
<head>
    <!-- Put your own custom styles in here -->
    <link rel="stylesheet" href="./index.css" media="all">
    <!-- Include Webfont to style text in custom font -->
    <link href="https://fonts.googleapis.com/css?family=Staatliches&display=swap" rel="stylesheet"> 
</head>
<body>
    <!-- The "app" element contains any content that receives dynamic updates -->
    <div id="app">
        <div style="height:30px"></div>
        <div style="width:50%;padding-left:25%;padding-right:25%;">
        <div class="timerText">{{msg.formattedTimeRemaining}}</div>
        </div>
        <div style="height:30px"></div>
        <div class="clueText">{{msg.clueText}}</div>
        <div class="imageHint"><img src="{{msg.imageSrc}}"></div>
    </div>
    <!-- uibuilder script libraries -->
    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script>
    <script src="./uibuilderfe.min.js"></script>
    <!-- Put any additional custom code in here -->
    <script src="./index.js"></script>
</body>
</html>

And the JS:

var app = new Vue({
    // The HTML element to attach to
	el: '#app',
    // Variables defined here will be avalable and updated within the HTML
	data: {
		msg: '[No Message Received Yet]',
	},
    // Callback function when Vue library is fully loaded
	mounted: function() {
	    // Start up uibuilder
		uibuilder.start();
		// Keep a reference to the Vue app
		var vueApp = this;
        // Callback triggered when node receives a (non-control) msg
		uibuilder.onChange('msg', function(msg) {
			vueApp.msg = msg;
		});
	},
	
});

Hi, can you please post code wrapped in back-ticks.

Assuming that your images are kept in the uibuilder instances src folder (or dist if you are using a build step), what you will need to do is have some sort of dynamic loading routine. So that the image is loaded into the pages DOM when the appropriate msg is received.

Vue should make this simple (note that I've not tested this code so I could be wrong). Assuming you have a data variable in index.js called myimg:

index.js

  ...
  data: {
    myimg: { url: "", alt: "" },
    ...
  },
  ...

index.html

<img v-if="myimg.url !== ''" :src="myimg.url" :alt="myimg.alt">

Set the myimg variable using a uibuilder.onChange("msg", function .... ). VueJS should do the clever bits for you.

The v-if part should only show the img if the url isn't blank.

If the image is fairly small, you could also consider converting it to a data attribute which can be sent in a msg.

Sorry! Hopefully that's better now.

I did have the image stored in a folder just within the .node-red folder - I thought I'd read somewhere that's where any files to be shared should go. I'll try moving it into the UIbuilder folder instead.

Still no luck though.

I already have a msg data element, and an onChange function. This is working for both the {{msg.formattedTimeRemaining}} and {{msg.clueText}}, but nor for the {{msg.imageSrc}}. It doesn't change with the messages, and gives a broken link icon. The element inspector just shows the html as:
<img src="{{msg.imageSrc}}">
If I put the whole <img> tag in the message, then it changes in the html, but it only displays as a text string.

With what you suggested I now have this:
<div><img v-if="myimg.url !== ''" :src="myimg.url" :alt="myimg.alt"></div>

var app = new Vue({
    // The HTML element to attach to
	el: '#app',
    // Variables defined here will be avalable and updated within the HTML
	data: {
		msg: '[No Message Received Yet]',
		myimg: { url: "", alt: "" },
	},
    // Callback function when Vue library is fully loaded
	mounted: function() {
	    // Start up uibuilder
		uibuilder.start();
		// Keep a reference to the Vue app
		var vueApp = this;
        // Callback triggered when node receives a (non-control) msg
		uibuilder.onChange('msg', function(msg) {
			vueApp.msg = msg;
			vueApp.myimg = msg.imageSrc;
		});
	},
	
});

Like this, I still get no image displayed. But there's no broken link icon this time and the element inspector shows:

<div>
  <img>
</div>
uibuilder.onChange('msg', function(msg) {
			vueApp.msg = msg;
			vueApp.myimg = msg.imageSrc;
		});

add a console.log(msg) and use the web inspector > console to verify that you receive data from node-red

eg.

uibuilder.onChange('msg', function(msg) {
			vueApp.msg = msg;
			vueApp.myimg = msg.imageSrc;
			console.log(msg);
		});

Could you give an example of the URL you are providing?

Right, I've just tested my code and it works. Using the default template code and just changing the logo image. I created 3 192x192 px images based on the default blue logo image - blue, green and red.

index.html

<b-img v-if="myimg.url !== ''" :src="myimg.url" rounded left v-bind="imgProps" :alt="myimg.alt" class="mt-1 mr-2"></b-img>

index.js

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

        myimg: {url:'', alt:''},

    ...

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

            if (msg.topic === 'img') {
                vueApp.myimg = msg.payload
            }
        })

...

Then I created 3 injects with the following format:

Activating each inject, changes the image dynamically.

1 Like

Thank you so much guys. It looks like it's finally working. The biggest problem was obviously my complete lack of knowledge and trying to send to url through as a string rather than JSON. So now it works from an inject node, and I have managed to get it to work from a dropdown menu (we will have around 20 odd pictures we need to choose from), but I wonder if there's a simpler method...

The dropdown menu only seems to allow a string boolean or number, so I have the url set as a string, and then a change node to change the msg.payload from a string to a JSON, but I have to put the url for each image in the search and replace fields. Is there something I can enter to just search "whatever text" and replace it with the same?

Glad you are making progress. We all have to start somewhere :slight_smile:

I just realised that, in the last example I gave you, there is an even slicker answer hidden within!

The default template already has some image properties embedded. Namely the width & height. Here, I simplify the HTML to just use the imgProps data variable and take out the class, src and alt attributes.

<b-img v-if="imgProps.url !== ''" rounded left v-bind="imgProps" :title="imgProps.alt"></b-img>

Now we can add the class, src and alt attributes to the imgProps data variable.

imgProps: { src:'./images/node-blue-192x192.png', alt:'uibuilder blue logo', width: 75, height: 75, class: 'mt-1 mr-2' },

I've put the default back in as well. Of course, you then need to adjust the incoming msg processing to use the imgProps variable.

            if (msg.topic === 'img') {
                Object.assign(vueApp.imgProps, msg.payload) // merges the properties - not for IE though
            }

Using Object.assign here so that the msg.payload is merged into the imgProps object rather than just overwriting it - which saves you having to handle each property separately or always send all properties. You now have to use msg.payload.src rather than msg.payload.url.

Neat. Using the data binding capability of Vue to simplify the HTML and still keep the JavaScript code simple and consistent.

Thankfully, JavaScript has a perfectly formed replace function for that.

Yeah, see now you're losing me! :crazy_face: :joy:

Is the replace code something I need to be doing in the index.js, or is it something I can put in the change node setting?

This is what's working for me at the moment:
Drop down menu:

Then change node:

So, I don't need to change any text within the message, just from a string to a JSON, but this way would mean needing a different rule for each image I want. I assume there's a code I can use in here, but I can't get my head around it!

This is basically all this page is doing, so if I have to put in 20 odd different rules for each pic, it's not really a big deal. It only needs to be done once, it's not like it's pictures that are going to be changed every day or week!

That seems to be a Dashboard node? Don't you wan the dropdown in your uibuilder front-end?

Bootstrap-vue has a nice dropdown component: https://bootstrap-vue.org/docs/components/dropdown

Whether you put the data for this in Node-RED is up to you. Typically, I would manage the data in Node-RED and send it through to my front-end when a user connects or reloads. After all, uibuilder is all about creating data-driven UI's (if your UI isn't data-driven, you would simply create static pages).

A drop-down can typically manage 2 values for each entry. 1 is the display value, e.g. "Clocks" and the other is the returned value, e.g. "./images.clocks.png". So you shouldn't really need to mess with replacing any values. If driving the data from Node-RED, manage a retained variable in Node-RED and pass this to the front-end on user connection/reload (the control outputs from your uibuilder node inform you when someone connects or reloads a page). The data will be an array of objects where each entry of the array contains an object something like {"url": "./images.clocks.png", "display": "Clocks", "alt": "Clock image", "title": "Display the clock image"}. Give the msg a suitable topic and check for that in your onChange function - setting the values for the dropdown accordingly.

Processing of the dropdown will happen purely in the front-end so make sure that you cancel the default action of the dropdown element so that your browser doesn't try to do something with it (see the button handler in the default template). You use the change event of the dropdown to trigger a function that cancels the default action and then changes the img tag by updating the myImage variable (or whatever you are using) from the dropdown.

No. The UIBuilder page won't have any interaction. It is just a display screen. This is for an escape room timer/hint system, so the plan is for me to be able to use the dashboard to control the timer and send hints through to the players screen in the game. So the actual "user interface" doesn't actually need to be particularly attractive, just functional as it's only for me. The players will just see a screen with a background image, timer and any hints we send them.

OK, in that case, your Dashboard dropdown can return either an identifier (e.g. "clocks") or maybe a number. If you use a number, you could use that as a lookup in a master array. If you use an identifier, your master will be an object with the identifier as a key.

ui_dropdown-->returns offset number--
  -->change node retrieves image data from master array variable--
  -->uibuilder node--

  -->index.js (onChange)-->update image data variable from incoming msg

I know I don't know what I'm talking about, but I thought all this was sounding too complicated for what I was trying to achieve. Maybe I wasn't explaining it properly?

I've just realised there's a json parser node. All I had to do was send the url from the dropdown menu through the parser node to the uibuilder. It works perfectly!

I still wouldn't have got there without your help though, so thank you very much!!

One more different question:
I have a log display on the dashboard to log when hints are sent and puzzles are solved, using the following code:

var log_entry = [
    new Date(),
    msg.topic,
    msg.payload
];
msg.payload = log_entry;

return msg;

I'd like to make the timestamp a lot simpler though. It currently reads out everything as follows:
"2020-09-03T13:34:14.624Z"

All I really want it to display is the hh:mm. I know there's methods for just getting minutes and hours etc. but I can't seem to make them work within this code.

Glad you are getting there.

For the time, you could use the moment node or JSONata to just pick out the time. However, if you are happy to live with UTC times, you should note that the example you give is an ISO standard date/time format and is easily parsed as text. You will find that JavaScript's string slice() function will let you extract just the time part.

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