Vue 3 how to get information about $children
Asked Answered
B

14

20

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)

Benempt answered 1/10, 2020 at 11:0 Comment(0)
B
19

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>
Benempt answered 28/10, 2020 at 7:14 Comment(3)
It's unclear, how do you make tab active. With $children I had component instance and could write "tab.active = true". But now tab is VNode. You store selectedIndex, but how do you use it in child tab?Selfsacrifice
Its kind of painful to see this much code to get children elements. Are we heading at right direction in using frameworks that are so stupidConners
This is unreadable madness for what should be simple code. I can't believe this is acceptable frontend code design to people who are supposed to be professionals. I've seen more readable shaders.Heterotrophic
B
15

Oh guys, I solved it:

this.$slots.default().filter(child => child.type.name === 'Tab')
Benempt answered 1/10, 2020 at 11:26 Comment(5)
where did you get that 'slots' from?Richia
Use this.$slots.default() insteadAdelia
@Katinka -> In composition API setup method => setup(_, {slots})Rento
Not sure why they deprecated the $children property :( This method works but requires passing any data to the slot like you do in the template this.$slots.default({....})Respect
Uncaught TypeError: this.$root.$slots.default is not a functionPastiche
H
10

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());
Handcraft answered 16/2, 2022 at 17:46 Comment(4)
thank you so much. your walk() method solves my issueJudson
this works, I wish there was just a getter like this built-into Vue. I don't know whether Vue changed implementation or what, but you have to replace .name with .__name for this to work as expected. I also added a depth param to restrict it.Trow
Thank you!! This answer actually works almost the same as the $children. I tried accessing the components through the slot, but i didnt get all the props so it was kind of hard, this actually works great!!Uncanny
I also used your function, and updated it a bit with an extra to find in what slot the child component is mounted in, i posted it on my gist. gist.github.com/erdesigns-eu/efb8d472684cabaa3573da65b97cf9d9Uncanny
P
9

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/>
Potboiler answered 25/10, 2020 at 23:9 Comment(1)
Thanks, but this is is not solution for Vue 3 + composition API. I resolve it in setup method 'setup(_, {slots})'Rento
E
6

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); 

    },
Erhart answered 2/10, 2020 at 20:33 Comment(1)
when having multiple components like this, this will end up in something like: "Maximum recursive updates exceeded in component ...."Metamorphosis
C
6

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>
Calamite answered 15/7, 2022 at 13:34 Comment(3)
Your's is the best answer here. I don't have to write any weird looking JS, and it's simple as hell. Thanks! And by the way, how did you know about slots.default()? I checked the docs but didn't find any mention of it.Tabret
@PraiseDare thanks, I knew about slots default because its fundamental to how the slots work. there is always a default slot that you get from <slot></slot> and any other slot must be named. Besides that I think my example assumes the default slot is a list of children, like <slot v-for="child in children">. You are likely correct to observe it may not work for named slots.Calamite
Actually that sounds a bit weird. I'm not sure. It's worth experimenting to see how the children can be read from the parent. You can console.log the value of useSlots() and see what's in there.Calamite
A
2

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")
    }
  }
)
Albertinealbertite answered 10/11, 2020 at 22:33 Comment(0)
S
2

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.

Skepticism answered 8/3, 2022 at 10:2 Comment(0)
B
1

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 !!

Bayou answered 24/8, 2021 at 17:58 Comment(2)
Dynamic components were already available years ago in Vue 2. Not sure what they have to do with accessing childrenBolduc
True, what happened to me was that I did it before as it appears in the link that I am going to share and it gave me the same problem as everyone else here: learnvue.co/2019/12/building-reusable-components -in-vuejs-tabs/Testee
H
0

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

Hyperthyroidism answered 20/1, 2021 at 8:43 Comment(0)
J
0

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).

Jorgenson answered 8/1, 2022 at 19:54 Comment(0)
M
0

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

Madoc answered 9/1 at 14:5 Comment(0)
U
0

My minimalistic solution for Vue 3 Composition API (also TS + Tailwind CSS).

Demo

App.vue

<template>
  <VTabs>
    <VTab title="Tab 1">Content 1</VTab>
    <VTab title="Tab 2">Content 2</VTab>
    <VTab title="Tab 3">Content 3</VTab>
  </VTabs>
</template>

VTabs.vue

<script setup lang="ts">
import { ref, provide, useSlots } from 'vue'

const slots = useSlots()
const activeTab = ref(slots!.default!()[0].props!.title)

provide('activeTab', activeTab)
</script>

<template>
  <div class="my-8">
    <div class="flex gap-4 mb-4">
      <div
        v-for="slot of $slots.default!()"
        :key="slot.props!.title"
        class="cursor-pointer"
        :class="{
        'font-bold border-b-4 border-black': activeTab === slot.props!.title,
      }"
        @click="activeTab = slot.props!.title"
      >
        {{ slot.props!.title }}
      </div>
    </div>
    <slot></slot>
  </div>
</template>

VTab.vue

<script setup lang="ts">
import { inject } from 'vue'

defineProps<{
  title: string
}>()

const activeTab = inject('activeTab')
</script>

<template>
  <div v-show="title === activeTab">
    <slot></slot>
  </div>
</template>
Unprofessional answered 8/4 at 13:56 Comment(0)
J
-1

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.

Judson answered 4/5, 2022 at 17:54 Comment(2)
What's the point of copy/pasting Urkle's answer?Aun
@TristanCHARBONNIER i added "Instead of .." and "write" I improved my writings, after your complain. thanks.Judson

© 2022 - 2024 — McMap. All rights reserved.