Subscribe on changes!

TransitionGroup getBoundingClientRect is not a function

avatar
Sep 24th 2022

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

avatar
Sep 26th 2022

There are two issues at play here:

  1. 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>:

    image

    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!

  2. 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 why isElementRoot() 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.
avatar
Oct 28th 2022

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.

avatar
Jan 28th 2023

` ` error:Uncaught (in promise) TypeError: child.el.getBoundingClientRect is not a function !

Why this not working?

avatar
Jan 28th 2023

@locki13freja What does the template of PostItem look like?

avatar
Mar 17th 2023

I fixed this error by removing a comment in a child component just above the </template>

avatar
Jul 29th 2023

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

avatar
Aug 30th 2023

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>
avatar
Sep 13th 2023

It seems I am no longer encountering this issue in Vue 3.3.4

avatar
Jan 7th 2024

There are two issues at play here:

  1. 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>: image 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!

  2. 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 why isElementRoot() 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

avatar
Feb 28th 2024

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>