Search code examples
vue.jsvue-component

VueJS: Tracking the order of child components in a parent slot at run time


I have a custom component where the parent and an array of child components are tightly coupled and there is a "backdoor" communication between the parent and child components using the provide-inject feature provided by vue.

In the parent component, I would like to keep track of the child components in the order they are defined in the slot.

I know that on mounted, the child components are instantiated in the order they are defined. However, at runtime, a child component might be inserted in between first child component and second child component for example. The question is how do I find out the updated order of the child components in the parent slot?

I saw in the vue devtools, it can recognise the order of child and nested components in the Components tab and show it in a tree layout. How can I achieve something like that?

Here is the example code https://stackblitz.com/edit/vitejs-vite-pkqhkv?file=src%2FApp.vue

In the above example, if you click prev and next, it should show Slide 1 and Slide 3 as expected because Slide 2 is toggled 'off', but when you toggle on Slide 2, the order goes into 1, 3, 2 which is not what I want. The order should be 1, 2 and 3.

Obviously, the implementation of providing each child with a numbered index is not reliable as they can be registered at different times, therefore I want to implement differently where I can know the current order of the Child components at any given time.


Solution

  • Even your parent/child components are strongly coupled you better don't propagate the slide visibility state to the children. That's not a good design, too much leaking of the slide state. Make all slide logic inside the parent (a good design and encapsulation).

    VUE SFC PLAYGROUND

    Parent.vue

    <script setup>
    
    import { ref, useSlots, watch, computed } from 'vue';
    
    // the current component index
    const current = ref(0);
    
    const slots = useSlots();
    // collect all slot child components
    const $children = computed(() => slots.default().filter(vnode => vnode.type.render)); // remove all non-component vnodes
    // watch for deletion of the current component and move to the next one
    watch(() => $children.value.length, length => current.value < length || next(current.value - 1));
    
    
    function prev(idx = current.value) {
      if(--idx < 0) idx = $children.value.length - 1;
      current.value = idx;
    }
    
    function next(idx = current.value) {
      if(++idx >= $children.value.length) idx = 0;
      current.value = idx;
    }
    
    defineExpose({
      prev,
      next,
    });
    </script>
    
    <template>
      <section>
        <div>$children.length: {{ $children.length }}, current: {{ current }}</div>
        <component :is="() => $children.filter((_, idx)=> idx === current)" />
      </section>
    </template>
    
    

    App.vue

    <script setup>
    import Parent from './Parent.vue';
    import Child from './Child.vue';
    import { ref } from 'vue';
    
    const toggleSlide2 = ref(false);
    const toggleSlide3 = ref(true);
    const parent = ref();
    
    </script>
    
    <template>
      <div>
        <section>
          <button @click="parent.prev()">Prev</button>
          <button @click="parent.next()">Next</button>
          <button @click="toggleSlide2 = !toggleSlide2">Toggle Slide 2</button>
           <button @click="toggleSlide3 = !toggleSlide3">Toggle Slide 3</button>
        </section>
    
        <Parent ref="parent">
          <Child>
            <h1>Slide 1</h1>
          </Child>
          <Child v-if="toggleSlide2">
            <h1>Slide 2</h1>
          </Child>
          <Child v-if="toggleSlide3">
            <h1>Slide 3</h1>
          </Child>
        </Parent>
      </div>
    </template>