Rollie
Rollie

Reputation: 4752

Is there a way to pass an instance of a component to another component?

For example, if I have

<question-dialog />  /* hidden */
<show-dialog-button />
<answer-dialog />  /* hidden */
<show-dialog-button />

I now want each of the 2 buttons to affect their preceding dialog - the first button changes a property of the question-dialog, and the second a property of answer-dialog. One way I think I can achieve this is to bind all 4 to the model for my current page, and in mount(), wire them up appropriately. Is there a better way? For example, can I do something like:

<question-dialog v-id="questionDialog"/>  /* hidden */
<show-dialog-button v-prop:dialog="questionDialog"/>
<answer-dialog />  /* hidden */
<show-dialog-button />

Or maybe there's a completely different way that I'm not aware of?

(If it matters, I'm using typescript for all my components)

Upvotes: 1

Views: 102

Answers (1)

acdcjunior
acdcjunior

Reputation: 135782

You can pass refs to those components:

<question-dialog ref="questionDialog"/>  /* hidden */
<show-dialog-button v-prop:dialog="$refs.questionDialog"/>

And call their methods directly. But there is a caveat: since refs aren't reactive and are only available after mount, you have to force an update of the template after mount. See demo below.

Vue.component('answer-dialog', {
  template: "#answer-dialog",
  data() {
    return {isShown: false}
  },
  methods: {
    show() {
      this.isShown = true;
    }
  }
});
Vue.component('show-dialog-button', {
  template: "#show-dialog-button",
  props: ['dialog'],
  methods: {
    showDialog() {
      this.dialog.show();
    }
  }
})

new Vue({
  el: '#app',
  mounted() {
    // force a re-render after mount, so the $refs are updated in the template
    this.$forceUpdate();
  }
})
<script src="https://unpkg.com/vue"></script>

<template id="answer-dialog">
    <div>answer-dialog - open? {{ isShown }}</div>
</template>

<template id="show-dialog-button">
    <div><button @click="showDialog">Click to toggle dialog</button></div>
</template>

<div id="app">
  <answer-dialog ref="dialogOne"></answer-dialog>
  <show-dialog-button :dialog="$refs.dialogOne"></show-dialog-button>
  
  <answer-dialog ref="dialogTwo"></answer-dialog>
  <show-dialog-button :dialog="$refs.dialogTwo"></show-dialog-button>
</div>

But a, maybe more idiomatic solution is to use an event hub and emit events that the sibling component can listen to:

var eventHub = new Vue(); // use a Vue instance as event hub

Vue.component('answer-dialog', {
  template: "#answer-dialog",
  props: ['dialogId'],
  data() {
    return {
      open: false
    }
  },
  created() {
    eventHub.$on('open-dialog', (e) => {
      if (e === this.dialogId) {
        this.open = true;
      }
    });
  }
});
Vue.component('show-dialog-button', {
  template: "#show-dialog-button",
  props: ['dialogId'],
  methods: {
    showDialog() {
      eventHub.$emit('open-dialog', this.dialogId);
    }
  }
})

new Vue({
  el: '#app'
})
<script src="https://unpkg.com/vue"></script>

<template id="answer-dialog">
    <div>answer-dialog {{ dialogId }} - open? {{ open }}</div>
</template>

<template id="show-dialog-button">
    <div><button @click="showDialog">Click to show dialog {{ dialogId }}</button></div>
</template>

<div id="app">
  <answer-dialog :dialog-id="1"></answer-dialog>
  <show-dialog-button :dialog-id="1"></show-dialog-button>
  
  <answer-dialog :dialog-id="2"></answer-dialog>
  <show-dialog-button :dialog-id="2"></show-dialog-button>
</div>

Or, even better, if you can, just use v-model:

Vue.component('answer-dialog', {
  template: "#answer-dialog",
  props: ['show']
});
Vue.component('show-dialog-button', {
  template: "#show-dialog-button",
  props: ['value']
})

new Vue({
  el: '#app',
  data: {
    dialogOneShow: false,
    dialogTwoShow: false
  }
})
<script src="https://unpkg.com/vue"></script>

<template id="answer-dialog">
    <div>answer-dialog - open? {{ show }}</div>
</template>

<template id="show-dialog-button">
    <div><button @click="$emit('input', !value)">Click to toggle dialog</button></div>
</template>

<div id="app">
  <answer-dialog :show="dialogOneShow"></answer-dialog>
  <show-dialog-button v-model="dialogOneShow"></show-dialog-button>
  
  <answer-dialog :show="dialogTwoShow"></answer-dialog>
  <show-dialog-button v-model="dialogTwoShow"></show-dialog-button>
</div>

Upvotes: 1

Related Questions