DEV Community

Nick Mousavi
Nick Mousavi

Posted on • Originally published at biomousavi.com

How to Access a Child Component’s Ref with multi-root node (Fragment) in Vue 3

Introduction

If you're used to Vue 2, you might remember that every component's template needed a single root element. In Vue 3, that's no longer necessary because of fragments. This means your components can now have multiple root elements without needing a wrapper.


<!-- Vue 2 -->
<template>
  <div> <!-- wrapper 😫 -->
    <h1>My Blog Post</h1>
    <ArticleComponent>{{ content }}</ArticleComponent>
  </div>
</template>

<!-- Vue 3 -->
<template>
  <h1>My Blog Post</h1>
  <ArticleComponent>{{ content }}</ArticleComponent>
</template>

Enter fullscreen mode Exit fullscreen mode

That's very similar to Fragment in React. However, Vue handles fragments behind the scenes. In fact, in Vue 3, you can think of the <template> tag as a fragment.

The ref() Problem

In Vue 2, we could easily set a ref on a child component, and it would refer to both the wrapper element and the component instance.

But in Vue 3, when there’s no wrapper element, what does the ref refer to? 🤔

If the child component uses the Options API or doesn't use <script setup>, the ref will point to the child component's this, giving the parent full access to its properties and methods.

What if we use <script setup> ?

Components using <script setup> are private by default. To expose properties, we need to use the defineExpose macro.

Access To Children's Element

  • This is what happen when you have wrapper (single root) element:
<!-- Child -->
<template>
  <div class="wrapper"> <!-- Root -->
    <h1>My Blog Post</h1>
    <ArticleComponent>{{ content }}</ArticleComponent>
  </div>
</template>



<!-- Parent -->
<script setup lang="ts">
const childRef = ref()

onMounted(()=>{
  console.log(childRef.value.$el); // <div class="wrapper"></div>  // [!code highlight]
})
</script>

<template>
  <Child ref="childRef" />
</template>
Enter fullscreen mode Exit fullscreen mode
  • And when you have more than one root:
<!-- Child -->
<template>
  <h1>My Blog Post</h1> <!-- Root 1 -->
  <ArticleComponent>{{ content }}</ArticleComponent> <!-- Root 2 -->
</template>



<!-- Parent -->
<script setup lang="ts">
const childRef = ref()

onMounted(()=>{
  console.log(childRef.value.$el); // #text  
})
</script>

<template>
  <Child ref="childRef" />
</template>
Enter fullscreen mode Exit fullscreen mode

Wait, what, what happened?

When we using Fragment(multiple nodes), Vue creates a text node that wraps our child component root nodes.

When using Fragments in Vue 3, Vue inserts an empty text node at the beginning of the component as a marker, which is why $el returns a #text node.

#text is like a reference point that Vue uses internally.

Also I should mention that you have still access to component instance (if you don't use <script setup> in child).

Solution

1) Use Single Root Like this
2) Use Template Refs + defineExpose

Using Template Refs + defineExpose

<!-- Child -->
<script setup lang="ts">
import { ref } from 'vue';

const h1Ref = ref()
const articleRef = ref()


defineExpose({
  h1Ref,
  articleRef
})
</script>

<template>
  <h1 ref="h1Ref">My Blog Post</h1> 
  <ArticleComponent ref="articleRef">{{ content }}</ArticleComponent> 
</template>



<!-- Parent -->
<script setup lang="ts">
const childRef = ref()

onMounted(()=>{
  console.log(childRef.value); 
  //  {h1Ref: RefImpl, articleRef: RefImpl}
})
</script>

<template>
  <Child ref="childRef" />
</template>
Enter fullscreen mode Exit fullscreen mode

Now you have access to your refs and all the things that you exposed by using defineExpose.

Top comments (0)