Bartosz Rosa
Bartosz Rosa

Reputation: 133

Accessing and modyfing slot's elements in Vue 3

I'm rewriting my MQTT based dashboard from Vue 2 to Vue 3 currently, and can't solve one problem.

The dashboard has many Vue components, which reacts to specific MQTT topics and values, to display current system state. One of them is the mqtt-multi-state component which is declared like below:

// status-page

<mqtt-multi-state subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
  <div value="OPEN"><font-awesome-icon icon="door-open"/></div>
  <div value="CLOSED"><font-awesome-icon icon="door-closed"/></div>
</mqtt-multi-state>

It contains dwo div elements, with two states that the system sensor (door) can has. These elements are passed to the default slot and are hidden by default via css.

What I want to achieve is to show one of them based on the equality of the value attr in each of them with the current MQTT value. So if the current value is OPEN then the first div show up, when value is CLOSED then the second one appears.

// mqtt-multi-state

<template>
    <div class="mqtt-multi-state">
        <div>
            <slot></slot>
        </div>
    </div> 
</template>

<script>
export default {
  methods: {
    messageArrived(value){
      
      let states = this.$slots.default() // here are the two divs
 
      for(let i = 0;i < states.length;i++ ){
        if(states[i].props.value === value )
        {
          //states[i].elm.className = "state-active" <- Vue 2 solution using DOM with elm
          //states[i].props.class = "state-active"; <- Vue 3, doesn't work, not reactive?
        }
        else 
        {
          //states[i].props.class = "";
        }
      }
   }
 }
}
</script>

I know, this approach is a little bit different but I really like to describe dashboard element in this way in HTML. In the messsageArrive() method I'm iterating over the default slot children elements and if the value match the current value prop I want to show this item, by add a state-active class. But this solution does not work. The VNode is changed (checked in the console).

In Vue 2 I've simply get to the DOM element directly and change it class, but on Vue 3 I can't figure it out, how to get from VNode to the DOM Node. Or there are maybe an other/better way to do that?

Upvotes: 2

Views: 4490

Answers (1)

Bartosz Rosa
Bartosz Rosa

Reputation: 133

Well, many evenings later I came to this solution. Instead of putting all the different states into a default slot, I've created dynamic named slots.

The different state elements (font-awesome icons in this case) go to each slot's template element. Unfortunately I can't pass the mqtt value to the template element itself, because it does not exists inside mqtt-multi-state parent component. Instead, I've passed the mqtt value into the template's first child element. It can be any element, div, p, font-awesome-icon.

// status-page

<mqtt-multi-state :state-count="2" subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
  <template #0><font-awesome-icon value="OPEN" icon="door-open"/></template>
  <template #1><font-awesome-icon value="CLOSED" icon="door-closed"/></template>
</mqtt-multi-state>

There is also a prop state-count that defines the number of different states. Note the colon before prop name, so the string "2" is a Number type.

The mqtt-multi-state component becomes like this:

// mqtt-multi-state

<template>
  <div class="mqtt-multi-state">
    <template v-for="(slot, index) in slots">
      <slot :name="index" v-if="slot" ></slot>
    </template>
  </div> 
</template>

<script>
export default {
  data: function(){
    return {
      slots: []
    }
  },
  props: {
    stateCount: {
      default: 2,
      type: Number
    }
  },
  methods: {
    messageArrived(value){
      for(let i = 0; i < this.stateCount; i++ ) {
        this.slots[i] = this.$slots[i]?.()[0]?.props?.value === value
      } 
    }
  }
}
</script>

In the messageArrived() function the code iterates through all the slots and compares the slot's first child element prop, named value with the current mqtt value received. If the values are equal then the this.slots[i] array value goes true, and the Vue's v-if directive makes corresponding slot visible, otherwise hides the slot.

Named slots names are created by the index value, so to get their content with this.$slot.slotNameRenderFunction() I call the function 0(), 1() etc using the this.$slot[index]() notation (accessing object member by name stored in the variable) with an optional chaining operator ?. - so the whole notation looks a little weird :).

Upvotes: 3

Related Questions