Skip to content
Advertisement

How do I communicate from a slot child definition to the containing component in vue 2.x?

I am writing a simple tooltip component. The contents of the tooltip are defined with a vue slot. I want the contents of the slot to be able to signal a close of the tooltip, the same way the tooltip can close itself from its own template.

Here’s example code:

Vue.component('vtip', {
  created: function() {
    this.$on('closeDialog', function(a,b,c) {
      console.log('[closeDialog in vtip]', a,b,c,this);
      this.closeDialog(); // idfk
    });
  },
  props: ['button_text'],
  template: '#vtip_template',
  methods: {
    closeDialog: function(event) {
      var $dialog = $(this.$refs.dialog);
      $dialog.fadeToggle('fast');
    },
    toggleDialog: function(event) {
      var $button = $(this.$refs.button);
      var $dialog = $(this.$refs.dialog);

      var button_left = $button.position().left;
      var button_top = $button.position().top;

      var extras = 2;
      var arrow_size = 10;
      var new_pos = {left:button_left+'px',top:button_top+$button.height()+arrow_size+extras+'px'};

      $dialog.css(new_pos);
      $dialog.fadeToggle('fast');
    },
  }
});

var v_app;
$(document).ready(function () {
  v_app = new Vue({
    el: '#v_app',
  });
});
body {
  background:#eee;
  padding:1em;
  font-family:Verdana;
  font-size:11pt;
}

.dialog {
  background:#222;
  color:#eee;
  border:2px solid white;
  display:none;

  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
  border-radius:4px;

  position:absolute;
}

.dialog .item-body .item-body {
  padding:1em 1em;
}

.dialog .item-body {
  min-width:10em;
  padding:0.5em 1em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

<script id="vtip_template" type="x-template">
    <span style="position:relative;">
        <button @click="toggleDialog" v-text="button_text" ref="button"></button>
        <div ref="dialog" class="dialog">
            <div class='item-body'>
                <a @click="closeDialog" style="float:right;color:white;padding:1em;" href="#">x</a>
                <!-- i hate slots -->
                <slot>...</slot>
            </div>
        </div>
    </span>
</script>

<DIV id="v_app">

    1. Click Hey.<br>
    2. How do I make the "Yes!" button close the component like the "x" link does?<br>
    
    <vtip button_text="hey">
        <!-- how do i make it so this slot child can close the parent component -->
        Are you sure?
        <button>Yes!</button> <!-- @click="closeDialog" does not work (and should not) -->
    </vtip>   

</DIV>

Advertisement

Answer

You could add a ref attribute to the vtip component tag, and then reference the component’s closeDialog method via the $refs object:

<vtip button_text="hey" ref="tip">
    Are you sure?
    <button @click="$refs.tip.closeDialog()">Yes!</button>
</vtip>   

Vue.component('vtip', {
  created: function() {
    this.$on('closeDialog', function(a,b,c) {
      console.log('[closeDialog in vtip]', a,b,c,this);
      this.closeDialog(); // idfk
    });
  },
  props: ['button_text'],
  template: '#vtip_template',
  methods: {
    closeDialog: function(event) {
      var $dialog = $(this.$refs.dialog);
      $dialog.fadeToggle('fast');
    },
    toggleDialog: function(event) {
      var $button = $(this.$refs.button);
      var $dialog = $(this.$refs.dialog);

      var button_left = $button.position().left;
      var button_top = $button.position().top;

      var extras = 2;
      var arrow_size = 10;
      var new_pos = {left:button_left+'px',top:button_top+$button.height()+arrow_size+extras+'px'};

      $dialog.css(new_pos);
      $dialog.fadeToggle('fast');
    },
  }
});

var v_app;
$(document).ready(function () {
  v_app = new Vue({
    el: '#v_app',
  });
});
body {
  background:#eee;
  padding:1em;
  font-family:Verdana;
  font-size:11pt;
}

.dialog {
  background:#222;
  color:#eee;
  border:2px solid white;
  display:none;

  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
  border-radius:4px;

  position:absolute;
}

.dialog .item-body .item-body {
  padding:1em 1em;
}

.dialog .item-body {
  min-width:10em;
  padding:0.5em 1em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

<script id="vtip_template" type="x-template">
    <span style="position:relative;">
        <button @click="toggleDialog" v-text="button_text" ref="button"></button>
        <div ref="dialog" class="dialog">
            <div class='item-body'>
                <a @click="closeDialog" style="float:right;color:white;padding:1em;" href="#">x</a>
                <!-- i hate slots -->
                <slot>...</slot>
            </div>
        </div>
    </span>
</script>

<DIV id="v_app">

    1. Click Hey.<br>
    2. How do I make the "Yes!" button close the component like the "x" link does?<br>
    
    <vtip button_text="hey" ref="tip">
        <!-- how do i make it so this slot child can close the parent component -->
        Are you sure?
        <button @click="$refs.tip.closeDialog()">Yes!</button> <!-- @click="closeDialog" does not work (and should not) -->
    </vtip>   

</DIV>

Alternatively, you could also use a scoped slot, passing the closeDialog method as a click handler property in the props.

In the slot definition, bind a clickHandler property:

<slot :clickHandler="closeDialog">...</slot>

Then, in your parent component, you can access the clickHandler via the scoped props like so:

<vtip button_text="hey">
  <template scope="props">
    Are you sure?
    <button @click="props.clickHandler">Yes!</button> 
  </template>
</vtip>   

Vue.component('vtip', {
  created: function() {
    this.$on('closeDialog', function(a,b,c) {
      console.log('[closeDialog in vtip]', a,b,c,this);
      this.closeDialog(); // idfk
    });
  },
  props: ['button_text'],
  template: '#vtip_template',
  methods: {
    closeDialog: function(event) {
      var $dialog = $(this.$refs.dialog);
      $dialog.fadeToggle('fast');
    },
    toggleDialog: function(event) {
      var $button = $(this.$refs.button);
      var $dialog = $(this.$refs.dialog);

      var button_left = $button.position().left;
      var button_top = $button.position().top;

      var extras = 2;
      var arrow_size = 10;
      var new_pos = {left:button_left+'px',top:button_top+$button.height()+arrow_size+extras+'px'};

      $dialog.css(new_pos);
      $dialog.fadeToggle('fast');
    },
  }
});

var v_app;
$(document).ready(function () {
  v_app = new Vue({
    el: '#v_app',
  });
});
body {
  background:#eee;
  padding:1em;
  font-family:Verdana;
  font-size:11pt;
}

.dialog {
  background:#222;
  color:#eee;
  border:2px solid white;
  display:none;

  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
  border-radius:4px;

  position:absolute;
}

.dialog .item-body .item-body {
  padding:1em 1em;
}

.dialog .item-body {
  min-width:10em;
  padding:0.5em 1em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

<script id="vtip_template" type="x-template">
    <span style="position:relative;">
        <button @click="toggleDialog" v-text="button_text" ref="button"></button>
        <div ref="dialog" class="dialog">
            <div class='item-body'>
                <a @click="closeDialog" style="float:right;color:white;padding:1em;" href="#">x</a>
                <!-- i hate slots -->
                <slot :clickHandler="closeDialog">...</slot>
            </div>
        </div>
    </span>
</script>

<DIV id="v_app">

    1. Click Hey.<br>
    2. How do I make the "Yes!" button close the component like the "x" link does?<br>
    
    <vtip button_text="hey">
      <template scope="props">
        Are you sure?
        <button @click="props.clickHandler">Yes!</button> 
      </template>
    </vtip>   

</DIV>
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement