Handling partial msg.payload items in a 2.0 Template node

I have an instance where msg.payload will not have all the keys defined for items which I want to show in a Dashboard 2.0 Template node. In the example below (greatly simplified from my needs) I will receive a message where msg.payload.x may be defined OR msg.payload.y may be defined, but not both together. This gets more complicated in that I will eventually have (still simplified):

  • msg.payload.zone1.x
  • msg.payload.zone1.y
  • msg.payload.zone2.x
  • msg.payload.zone2.y

and want to save them as different object:

  • zone1.x
  • zone1.y
  • zone2.x
  • zone2.y

My problem is that when I receive msg.payload.x it clears out any existing msg.payload.y. I am guessing the is a Vue way to handle this but cannot figure it out inside the node. Admittedly, I have never used Vue prior to recent attempts to do things with the Template node.

Below is the template for my basic example. What am I missing?

<template>
    <p>X = {{ msg.payload?.x }}</p>
    <p>Y = {{ msg.payload?.y }}</p>
    <p>X+Y = <div v-html="addValue"></div></p>
    <p>Sensor Angle = {{ msg.angle }}</p>
</template>

<script>
    export default {
        watch: {
            //Watch for any changes to msg and act on them.
            //This is run when the dashboard is loaded as well
            msg: function () {
                this.addValue = this.msg.payload.x + this.msg.payload.y;
            }
        },
    }
</script>

Here is a sample flow with inject nodes I have used to test with:

[{"id":"f3564535b8fc6abf","type":"inject","z":"01a9e096a69c7202","name":"","props":[{"p":"payload.x","v":"3","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1590,"y":460,"wires":[["cef846095e925344"]]},{"id":"1cdd9cec45819237","type":"inject","z":"01a9e096a69c7202","name":"","props":[{"p":"payload.y","v":"6","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1590,"y":500,"wires":[["cef846095e925344"]]},{"id":"b9441e0457637205","type":"inject","z":"01a9e096a69c7202","name":"","props":[{"p":"payload.x","v":"5","vt":"num"},{"p":"payload.y","v":"7","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1590,"y":540,"wires":[["cef846095e925344"]]},{"id":"194cdce48b33540a","type":"inject","z":"01a9e096a69c7202","name":"","props":[{"p":"angle","v":"55","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1590,"y":580,"wires":[["cef846095e925344"]]},{"id":"cef846095e925344","type":"ui-template","z":"01a9e096a69c7202","group":"0db8d9f5bdc794eb","page":"","ui":"","name":"Template test 3","order":3,"width":"12","height":"6","head":"","format":"<template>\n    <p>X = {{ msg.payload?.x }}</p>\n    <p>Y = {{ msg.payload?.y }}</p>\n    <p>X+Y = <div v-html=\"addValue\"></div></p>\n    <p>Sensor Angle = {{ msg.angle }}</p>\n</template>\n\n<script>\n    export default {\n        watch: {\n            //Watch for any changes to msg and act on them.\n            //This is run when the dashboard is loaded as well\n            msg: function () {\n                this.addValue = this.msg.payload?.x + this.msg.payload?.y;\n            }\n        },\n    }\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1840,"y":500,"wires":[[]]},{"id":"0db8d9f5bdc794eb","type":"ui-group","name":"My Group","page":"92d2e5483adf6001","width":"12","height":"6","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"92d2e5483adf6001","type":"ui-page","name":"Simulation","ui":"4c2bb00f7172c414","path":"/page7","icon":"home","layout":"grid","theme":"d8f055e104cc1454","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":2,"className":"","visible":true,"disabled":"false"},{"id":"4c2bb00f7172c414","type":"ui-base","name":"My 1st 2.0 Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","notificationDisplayTime":5},{"id":"d8f055e104cc1454","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

My personal preference is not to rely on behind-the-scenes parsing of moustach-ed variables (e.g. {{ msg.payload }} ) in the HTML (<template>) area, but rather handle the message in the javascript area, and have full visibility & control on how I handle incoming data.

I set a message listener in the mounted() area of the template. When I receive a message, I can inspect it (then filter, transform, enrich etc.) and set the relevant msg properties into the template's data() area, which is mapped to the visual HTML elements, and automatically updates them upon change.

The reason your existing code does not do what you want is that it is evaluated each time you get a new message. Data from previous messages is not retained. One easy way to make it work is to build a merged set of properties from all previous messages before passing it to the template. Add a Function node containing this code in front of the template

// fetch previous combined payload
let previousPayload = context.get("previous") ?? {}
// merge the new payload parts into the previous
// Edit, corrected the next line, I had the merge the wrong way round
msg.payload = {...previousPayload, ...msg.payload}
// save the merged payload
context.set("previous", msg.payload)
return msg

Oops, I had the merge the wrong way round, it should be
msg.payload = {...previousPayload, ...msg.payload}

I will adjust the post above.

Alternatively (and better) you can do the same thing with a Join node configured like:

For either of these solutions to work you will also, of course, have to move any additional properties that you want (such as angle) into the payload.

My ultimate goal is to display a plotly graph and dynamically change a couple items as their value changes. I believe merging all the data will cause the entire graph to redraw but I do know that I can provide a modified variable and have just that one item redeaw. My current "solution" is to set data outside of msg.payload and use values such as msg.angle. I was hoping there is a way with vue to assign data from msg.payload.___ to a variable inside the template and ignore any that have not changed.

Using this method, is there a way to check that the key exists and only define/change variables for items passed? I have tried using
msg.payloasd.myKey !== undefined
and had no luck. Maybe in using it in the wrong areas? I had it under the watch section, inside a function which draws/modifies the graph. I did not use it in a data() section. I also have since fine reference to a computed() section but have not figured out how I might use it.

You can condition the setting of local data variables using any property of the message you desire.

e.g.

<template>
    <div>
        <p>Current x: {{ x }}</p>
        <p>Current y: {{ y }}</p>
        <p>Current sum: {{ sum }}</p>
        <v-btn @click="send({payload:sum})">Send sum</v-btn>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                x: 0,
                y: 0
            }
        },
        watch: {
            msg: function () {
                if (this.msg.topic  === "update-x") {
                    this.x = msg.payload
                } else if (this.msg.topic  === "update-y") {
                    this.y = msg.payload
                } if (this.msg.topic  === "update-xy") {
                    this.x = msg.payload.x
                    this.y = msg.payload.y
                }
            }
        },
        computed: {
            sum: function () {
                return this.x + this.y
            }
        }
    }
</script>

I assume they typo msg.payloasd is just here in the post and not in your code.

There are many ways you can do this:
Colin's approach is perfectly good, yet requires additional pre-processing before the message comes into the node.
compute()/watch() ? I assume it works per the example in the documentation, but have never tried it.
If it was up to me (again, personal preference only), I would go to the <script> area and do something such as

export default {
  data () {
    return {
      x: null,
      y: null
    }
  },
  mounted() {
    const vThis = this; // save current 'this', for use inside callbacks & internal scopes

  // Set message listener
  this.$socket.on('msg-input:' + this.id, function(msg) {
    if (msg?.payload.x !== undefined)
      vThis.x = msg.payload.x;
    if (msg?.payload.y !== undefined)
      vThis.y = msg.payload.y;
    processXY(vThis.x,vThis.y);
  });
}

Thank you all for the information. I am going to use a combination of the information from both @Steve-Mcl and @omrid. I revisited the use of msg.payload.myKey !== undefined (yes, there was a typo as I was responding via my phone) and think I will use that under the watch section. While the use of topic would currently make things easier my messages are currently coming from MQTT) I do not think that will be the case for me long term as I intend to ultimately receive messages from a different source. I am going to keep it as a possibility, but I found that unless every message received had a topic the previous topic was apparently retained and used in the conditional checking.

What I have below is working for my testing as long as all messages have a topic with some not passing the test.

<template>
    <p>X = {{ myx }}</p>
    <p>Y = {{ myy }}</p>
    <p>X+Y = <div v-html="addValue"></div></p>
    <p>Sensor Angle = {{ msg.angle }}</p>
</template>

<script>
    export default {
        data() {
            return {
                myx: 0,
                myy: 0
            }
        },
        watch: {
            //Watch for any changes to msg and act on them.
            //This is run when the dashboard is loaded as well
            msg: function () {
                if (this.msg.topic  === "myx") {
                    this.myx = this.msg.payload.x + 10;
                } else if (this.msg.payload.x !== undefined){
                    this.myx = this.msg.payload.x;
                }

                if (this.msg.payload.y !== undefined){
                    this.myy = this.msg.payload.y;
                }              
                this.addValue = this.myx + this.myy;
            }
        },
    }
</script>