TransitionGroup getBoundingClientRect is not a function
Vue version
3.2.31
Link to minimal reproduction
https://stackblitz.com/edit/vitejs-vite-srxkag?file=src%2Fcomponents%2FHelloWorld.vue
Steps to reproduce
In parent component I have
<TransitionGroup appear>
<template v-for="item in list" :key="item.id">
<Notification
v-if="item.visible"
:notification="item"
@close="handleClose"
/>
</template>
</TransitionGroup>
And if child component init with html comment
<template>
<!-- COMMENT -->
<div>
{{ notification.title }} -
<button type="button" @click="handleClose">Clickme</button>
</div>
</template>
I get the "TypeError: child.el.getBoundingClientRect is not a function"
Work if I remove the HTML comment from first line in template.
What is expected?
Execute the transition correctly, with or without comment on the child element
What is actually happening?
TypeError: child.el.getBoundingClientRect is not a function
System Info
No response
Any additional comments?
No response
There are two issues at play here:
Components used in
<TransitionGroup>
break if they don't have a single-element root but render a Fragment (multiple root nodes). This is currently not documented for<TransitionGroup>
, only for<Transition>
:if (__DEV__ && !isElementRoot(root)) { warn( `Component inside <Transition> renders non-element root node ` + `that cannot be animated.` ) }
In Vue 2,
<TransitionGroup>
did support multi-root (functional) components as children, so this is either a bug or a non-documented breaking change!The behavior of components that have multiple root nodes (includes comments) but only one root element is inconsistent between dev & prod:
- In production, comments are usually stripped during compilation, so components render a single root element instead of a fragment. This is why your example does actually work in production mode (
vite build
/vite preview
), it only breaks in development mode. - In development, comments are not stripped, so components render fragments, breaking
<TransitionGroup>
. However, Vue pretends that the component is single-root during checks, because it knows that this will be the case once you build for production. That's whyisElementRoot()
returns true in the above warning check and the warning is not logged, despite the component actually not having an element root but rendering a fragment and breaking the transition.
- In production, comments are usually stripped during compilation, so components render a single root element instead of a fragment. This is why your example does actually work in production mode (
Another use case arises when using custom components within a <transition-group>
as a grid layout (display: grid
). The elements need to be rendered as individual elements without a common root, so the grid layout works as expected.
`
Why this not working?Post list
`
error:Uncaught (in promise) TypeError: child.el.getBoundingClientRect is not a function !
<TransitionGroup name="post-list">
<PostItem
v-for="post in posts"
:post="post"
:key="post.id"
@remove="$emit('remove', post)"
/>
</TransitionGroup>
</div>
<h2 v-else style="color: red">Post list empty</h2>
I fixed this error by changing the place of the v-if
The transition worked when 'isOpen' became true. The error occurred when 'isOpen' became false.
After changing the place of the v-if from:
<TransitionGroup name="pos-items-list"
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<template v-if="isOpen" v-for="(posItem,key) in posItems" :key="Math.random()">
<ShowItemCard v-if="posItem.products.length > 1" :key="posItem.id + 'levelflex'" />
<ShowItem v-else :key="posItem.id + 'level'" />
</template>
</TransitionGroup>
to so:
<template v-if="isOpen" v-for="(posItem,key) in posItems" :key="Math.random()">
<TransitionGroup name="pos-items-list"
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<ShowItemCard v-if="posItem.products.length > 1" :key="posItem.id + 'levelflex'" />
<ShowItem v-else :key="posItem.id + 'level'" />
</TransitionGroup>
</template>
It all worked. Seems logical: It was trying to transition an emtpy/non-existing 'element'.
@yurirnascimento [v-if="item.visible"] and @locki13freja: could that have been the problem in your case too? Anyway just to find common ground. Happy coding
In my case, I solved this by changing from:
<TransitionGroup name="list" tag="div">
<CustomComponent v-for="item in items" :key="item.id" :componentProp="prop" />
</CustomComponent>
To:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
<CustomComponent :componentProp="prop" />
</li>
</TransitionGroup>
There are two issues at play here:
Components used in
<TransitionGroup>
break if they don't have a single-element root but render a Fragment (multiple root nodes). This is currently not documented for<TransitionGroup>
, only for<Transition>
: Also, the warning that's printed if you use a multi-root component in a<TransitionGroup>
only mentions<Transition>
:if (__DEV__ && !isElementRoot(root)) { warn( `Component inside <Transition> renders non-element root node ` + `that cannot be animated.` ) }
In Vue 2,
<TransitionGroup>
did support multi-root (functional) components as children, so this is either a bug or a non-documented breaking change!The behavior of components that have multiple root nodes (includes comments) but only one root element is inconsistent between dev & prod:
- In production, comments are usually stripped during compilation, so components render a single root element instead of a fragment. This is why your example does actually work in production mode (
vite build
/vite preview
), it only breaks in development mode.- In development, comments are not stripped, so components render fragments, breaking
<TransitionGroup>
. However, Vue pretends that the component is single-root during checks, because it knows that this will be the case once you build for production. That's whyisElementRoot()
returns true in the above warning check and the warning is not logged, despite the component actually not having an element root but rendering a fragment and breaking the transition.
@Tobiaqs thank you, I had this bug because I had multiple root notes, but one of them was just a comment and I didn't thought that it may cause the issue, but it did
It seems I'm still encountering this issue, even in Vue 3.4.x
This is my workaround (works with Vite). It disables transitions in dev. In prod everything works...
// FixedTransitionGroup.vue
//
// Usage:
// <FixedTransitionGroup
// enter-active-class="transform ease-out duration-300 transition"
// enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
// enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
// leave-active-class="transition ease-in duration-100"
// leave-from-class="opacity-100"
// leave-to-class="opacity-0">
// <!-- your content -->
// </FixedTransitionGroup>
<template>
<TransitionGroup v-if="PROD" v-bind="$attrs">
<slot></slot>
</TransitionGroup>
<template v-else>
<slot></slot>
</template>
</template>
<script lang="ts" setup>
defineOptions({
inheritAttrs: false,
})
const PROD = import.meta.env.PROD
</script>