Myra Guerrier Myra Guerrier - 26 days ago 9
Javascript Question

Accessing and filtering child components defined in a Vue.js component's <slot>

I'm in the process of implementing a combined select dropdown and searchbox component for a UI library I'm writing, using Vue.js to drive interactions, but I'm running into a problem with how Vue.js seems to handle communication between parent and child components.

Basically, what I'm aiming to achieve is to allow users of the library to define search-select elements in HTML using something like this:

<search-select>
<search-option value="foo">Foo</search-option>
<search-option value="bar">Bar</search-option>
<search-option value="baz">Baz</search-option>
<search-option value="foobar">Foobar</search-option>
</search-select>


My template for the
search-select
component looks like this:

<template>
<div class="search-select">
<div class="placeholder" ref="placeholder"></div>
<ul class="dropdown">
<input
type="text"
placeholder="Type to filter..."
v-on:input="updateFilter"
>
<slot></slot>
</ul>
</div>
</template>


The idea is that the
search-select
component will add a text input box above the specified inputs, which the user can type in to filter the options that can be selected. The finished component would look something like this.

However, from what I can tell in the documentation, Vue.js doesn't provide any direct way for me to access the properties of the child components from within the parent, nor any way to listen for
click
events or similar from children within a parent's
<slot>
.

This means that I don't have a way to filter visibility of the children based on the user's input, or to update the value of the
search-select
component when a user clicks on one of the children.

The Vue.js documentation mentions ways to pass events from child components to parents, but none seem applicable to elements defined within slots - it appears that parents can only listen for events from components explicitly defined within them.

How would one implement the type of two-way communication between parent and child components required for this use case, without violating any Vue.js best practices related to sharing information between components?

Answer Source

You can use event bus to solve this problem. Just keep every option listening to input event, then letting it decide whether hide itself or not depending on the argument passed.
See fiddle or demo below.

const bus = new Vue();

Vue.component('search-option', {
  template: `
    <option v-if="show" :value="this.$attrs.value"><slot></slot></option> 
  `,
  created() {
    bus.$on('filter', (input) => {
      this.show = this.$attrs.value.includes(input);
    });
  },
  beforeDestory() {
  	bus.$off('filter');
  },
  data() {
    return {
      show: true,
    };
  },
});

Vue.component('search-select', {
  template: `
<div class="search-select">
    <div class="placeholder" ref="placeholder"></div>
    <ul class="dropdown">
        <input
            type="text"
            placeholder="Type to filter..."
            v-on:input="updateFilter"
            v-model="myinput"
        >
        <slot></slot>
    </ul>
</div>
  `,
  methods: {
    updateFilter() {
      console.log('update filter');
      bus.$emit('filter', this.myinput);
    },
  },
  data() {
    return {
    	myinput: undefined,
    };
  },
});

Vue.component('parent', {
  template: `
<div class="parent">
<search-select>
    <search-option value="foo">Foo</search-option>
    <search-option value="bar">Bar</search-option>
    <search-option value="baz">Baz</search-option>
    <search-option value="foobar">Foobar</search-option>
</search-select>
</div>
  `,
});

new Vue({
  el: '#app',
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app">
  <parent></parent>
</div>