vue.jsvuejs3vuejs-slots

Update data in child component obtained by $slots


I'm using VueJS 3.4.25 and options API for this.

So I have these two components defined like this:

// ComponentA
<template>
...
<div ref="componentA">
  <slot></slot>
</div>
...
</template>
<script>
export default {
  ...
  methods: {
    fireComponentAMethod() {
      const slotNode = this.$slots.default()[0];
      slotNode.type.methods.fireComponentBMethod();
    },
  },
}
</script>
// ComponentB
<template>
...
isValueSet: {{ isValueSet }}
...
</template>
<script>
export default {
  ...
  data() {
    return {
      isValueSet: false,
    };
  },
  methods: {
    fireComponentBMethod() {
      console.log("I just fired fireComponentBMethod");
      this.isValueSet = true;
      console.log(this.isValueSet);
    },
  },
}
</script>

Ans somewhere else I set up the components in this manner:

// Some absolutely unrelated component
<ComponentA>
  <ComponentB />
</ComponentA>

The reasoning for this I'm aiming at greater reusability of the code, hence sometimes it's ComponentB, sometimes ComponentC and ComponentDoubleD inside ComponentA.

The problem is observed when fireComponentAMethod is fired. It indeed fires fireComponentBMethod and in the console I do see

I just fired fireComponentBMethod
true

But the change is not reflected in the ComponentB template, on the page it constantly stays isValueSet: false.

I tried using $refs to access the children, but I guess since they're in the slot, it doesn't work. For example, console.log(this.$refs) prints out this:

Proxy(Object) {componentA: div}
[[Target]]: Object
  componentA: div
    0: div

When calling this.$refs.componentA[0] it doesn't return a component instance. Instead, it returns an object representing a simple node... and I can't call any methods or change data on it.


Solution

  • I have been researching this some more, and thanks to @cantdocpp answer, cooked up the solution with provide/inject approached offered by Vue. Although to me this more looks like a workaround and a hack, it's the closest that I need.

    // ComponentA
    <template>
    ...
    <div ref="componentA">
      <slot></slot>
      <button @click="fireComponentAMethod" />
    </div>
    ...
    </template>
    <script>
    import { computed } from "vue";
    
    export default {
      ...
      data() {
        return {
          triggered: null,
        };
      },
      provide() {
        return {
          triggered: computed(() => this.triggered),
        };
      },
      methods: {
        fireComponentAMethod() {
          this.triggered = new Date();
        },
      },
    }
    </script>
    
    // ComponentB
    <template>
    ...
    isValueSet: {{ isValueSet }}
    ...
    </template>
    <script>
    export default {
      ...
      data() {
        return {
          isValueSet: false,
        };
      },
      methods: {
        fireComponentBMethod() {
          console.log("I just fired fireComponentBMethod");
          this.isValueSet = true;
          console.log(this.isValueSet);
        },
      },
      inject: ["triggered"],
      watch: {
        'triggered': function (n, o) {
          this.fireComponentBMethod();
        },
      },
    }
    </script>
    

    Now this approach will change triggered on the button click and will trigger the update in ComponentB. I use new Date() to make sure the triggered value is always unique. This changes the values in data, which was the main goal.