Vue 3 how to get information about $children

23.2k views Asked by At

This my old code with VUE 2 in Tabs component:

created() {
   this.tabs = this.$children;
}

Tabs:

<Tabs> 
  <Tab title="tab title">
    ....
  </Tab>
  <Tab title="tab title">
    ....
  </Tab> 
</Tabs>

VUE 3: How can I get some information about childrens in Tabs component, using composition API? Get length, iterate over them, and create tabs header, ...etc? Any ideas? (using composition API)

13

There are 13 answers

3
Ingrid Oberbüchler On BEST ANSWER

This is my Vue 3 component now. I used provide to get information in child Tab component.

<template>
  <div class="tabs">
    <div class="tabs-header">
      <div
        v-for="(tab, index) in tabs"
        :key="index"
        @click="selectTab(index)"
        :class="{'tab-selected': index === selectedIndex}"
        class="tab"
      >
        {{ tab.props.title }}
      </div>
    </div>
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {defineComponent, reactive, provide, onMounted, onBeforeMount, toRefs, VNode} from "vue";
    
interface TabProps {
  title: string;
}
    
export default defineComponent({
  name: "Tabs",
  setup(_, {slots}) {
    const state = reactive({
      selectedIndex: 0,
      tabs: [] as VNode<TabProps>[],
      count: 0
    });
    
    provide("TabsProvider", state);
    
    const selectTab = (i: number) => {
      state.selectedIndex = i;
    };
    
    onBeforeMount(() => {
      if (slots.default) {
        state.tabs = slots.default().filter((child) => child.type.name === "Tab");
      }
    });

    onMounted(() => {
      selectTab(0);
    });

    return {...toRefs(state), selectTab};
  }
});
</script>

Tab component:

<script lang="ts">
export default defineComponent({
  name: "Tab",
  setup() {
    const index = ref(0);
    const isActive = ref(false);

    const tabs = inject("TabsProvider");

    watch(
      () => tabs.selectedIndex,
      () => {
        isActive.value = index.value === tabs.selectedIndex;
      }
    );

    onBeforeMount(() => {
      index.value = tabs.count;
      tabs.count++;
      isActive.value = index.value === tabs.selectedIndex;
    });
    return {index, isActive};
  }
});
</script>

<template>
  <div class="tab" v-show="isActive">
      <slot></slot>
  </div>
</template>
5
Ingrid Oberbüchler On

Oh guys, I solved it:

this.$slots.default().filter(child => child.type.name === 'Tab')
1
user3224416 On

If you copy pasted same code as me

then just add to the "tab" component a created method which adds itself to the tabs array of its parent

created() {
    
        this.$parent.tabs.push(this); 

    },
2
alexwenzel On

Based on the answer of @Urkle:

/**
 * walks a node down
 * @param vnode
 * @param cb
 */
export function walk(vnode, cb) {
    if (!vnode) return;

    if (vnode.component) {
        const proxy = vnode.component.proxy;
        if (proxy) cb(vnode.component.proxy);
        walk(vnode.component.subTree, cb);
    } else if (vnode.shapeFlag & 16) {
        const vnodes = vnode.children;
        for (let i = 0; i < vnodes.length; i++) {
            walk(vnodes[i], cb);
        }
    }
}

In addition to the accepted answer:

Instead of

this.$root.$children.forEach(component => {})

write

walk(this.$root, component => {})

And this is how i got it working for me.

0
cajoue On

I found this updated Vue3 tutorial Building a Reusable Tabs Component with Vue Slots very helpful with explanations that connected with me.

It uses ref, provide and inject to replace this.tabs = this.$children; with which I was having the same problem.

I had been following the earlier version of the tutorial for building a tabs component (Vue2) that I originally found Creating Your Own Reusable Vue Tabs Component.

2
Ernesto José Jiménez Canquiz On

I had the same problem, and after doing so much research and asking myself why they had removed $children, I discovered that they created a better and more elegant alternative.

It's about Dynamic Components. (<component: is =" currentTabComponent "> </component>).

The information I found here:

https://v3.vuejs.org/guide/component-basics.html#dynamic-components

I hope this is useful for you, greetings to all !!

0
cmlima On

A per Vue documentation, supposing you have a default slot under Tabs component, you could have access to the slot´s children directly in the template like so:

// Tabs component

<template>
  <div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
    <button
      v-for="(tab, index) in getTabs($slots.default()[0].children)"
      :key="index"
      :class="{ active: modelValue === index }"
      @click="$emit('update:model-value', index)"
    >
      <span>
        {{ tab.props.title }}
      </span>
    </button>
  </div>
  <slot></slot>
</template>

<script setup>
  defineProps({ modelValue: Number })

  defineEmits(['update:model-value'])

  const getTabs = tabs => {
    if (Array.isArray(tabs)) {
      return tabs.filter(tab => tab.type.name === 'Tab')
    } else {
      return []
    }
</script>

<style>
...
</style>

And the Tab component could be something like:

// Tab component

<template>
  <div v-show="active">
    <slot></slot>
  </div>
</template>

<script>
  export default { name: 'Tab' }
</script>

<script setup>
  defineProps({
    active: Boolean,
    title: String
  })
</script>

The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):

...
<tabs v-model="active">
  <tab
    v-for="(section, index) in sections"
    :key="index"
    :title="section.title"
    :active="index === active"
  >
    <component
      :is="section.component"
    ></component>
  </app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'

const active = ref(0)
</script>

Another way is to make use of useSlots as explained in Vue´s documentation (link above).

0
soroush On

In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, they recommend using $refs. as a array

https://v3-migration.vuejs.org/breaking-changes/children.html#_2-x-syntax

3
agm1984 On

With script setup syntax, you can use useSlots: https://vuejs.org/api/sfc-script-setup.html#useslots-useattrs

<script setup>
import { useSlots, ref, computed } from 'vue';

const props = defineProps({
    perPage: {
        type: Number,
        required: true,
    },
});

const slots = useSlots();

const amountToShow = ref(props.perPage);
const totalChildrenCount = computed(() => slots.default()[0].children.length);
const childrenToShow = computed(() => slots.default()[0].children.slice(0, amountToShow.value));
</script>

<template>
    <component
        :is="child"
        v-for="(child, index) in childrenToShow"
        :key="`show-more-${child.key}-${index}`"
    ></component>
</template>
4
Urkle On

My solution for scanning children elements (after much sifting through vue code) is this.

export function findChildren(parent, matcher) {
  const found = [];
  const root = parent.$.subTree;
  walk(root, child => {
    if (!matcher || matcher.test(child.$options.name)) {
      found.push(child);
    }
  });
  return found;
}

function walk(vnode, cb) {
  if (!vnode) return;

  if (vnode.component) {
    const proxy = vnode.component.proxy;
    if (proxy) cb(vnode.component.proxy);
    walk(vnode.component.subTree, cb);
  } else if (vnode.shapeFlag & 16) {
    const vnodes = vnode.children;
    for (let i = 0; i < vnodes.length; i++) {
      walk(vnodes[i], cb);
    }
  }
}

This will return the child Components. My use for this is I have some generic dialog handling code that searches for child form element components to consult their validity state.

const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);
const invalid = found.filter(input => !input.checkHtml5Validity());
0
Minh Hùng Trần On

In 3.x version, the $children is removed and no longer supported. Using ref to access children instance.

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent .vue'

const child = ref(null)

onMounted(() => {
   console.log(child.value) // log an instance of <Child />
})
</script>

<template>
  <ChildComponent ref="child" />
</template>

Detail: https://vuejs.org/guide/essentials/template-refs.html#template-refs

1
Lizard Derad On

To someone wanting whole code:

Tabs.vue

<template>
    <div>
        <div class="tabs">
            <ul>
                <li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
                    <a :href="tab.href" @click="selectTab(tab)">{{ tab.name }}</a>
                </li>
            </ul>
        </div>

        <div class="tabs-details">
            <slot></slot>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Tabs",
        data() {
            return {tabs: [] };
        },
        created() {

        },
        methods: {
            selectTab(selectedTab) {
                this.tabs.forEach(tab => {
                    tab.isActive = (tab.name == selectedTab.name);
                });
            }
        }
    }
</script>

<style scoped>

</style>

Tab.vue

<template>
    <div v-show="isActive"><slot></slot></div>
</template>

<script>
    export default {
        name: "Tab",
        props: {
            name: { required: true },
            selected: { default: false}
        },

        data() {

            return {
                isActive: false
            };

        },

        computed: {

            href() {
                return '#' + this.name.toLowerCase().replace(/ /g, '-');
            }
        },

        mounted() {

            this.isActive = this.selected;

        },

        created() {

            this.$parent.tabs.push(this);

        },
    }
</script>

<style scoped>

</style>

App.js

<template>
    <Tabs>
                    <Tab :selected="true"
                         :name="'a'">
                        aa
                    </Tab>
                    <Tab :name="'b'">
                        bb
                    </Tab>
                    <Tab :name="'c'">
                        cc
                    </Tab>
                </Tabs>
<template/>
0
therealpaulgg On

I made a small improvement to Ingrid Oberbüchler's component as it was not working with hot-reload/dynamic tabs.

in Tab.vue:

onBeforeMount(() => {
  // ...
})
onBeforeUnmount(() => {
  tabs.count--
})

In Tabs.vue:

const selectTab = // ...
// ...
watch(
  () => state.count,
  () => {
    if (slots.default) {
      state.tabs = slots.default().filter((child) => child.type.name === "Tab")
    }
  }
)