core icon indicating copy to clipboard operation
core copied to clipboard

TransitionGroup getBoundingClientRect is not a function

Open yurirnascimento opened this issue 3 years ago • 10 comments

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

yurirnascimento avatar Sep 24 '22 23:09 yurirnascimento

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.

jonaskuske avatar Sep 26 '22 02:09 jonaskuske

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.

ssche avatar Oct 28 '22 00:10 ssche

`

locki13freja avatar Jan 28 '23 08:01 locki13freja

@locki13freja What does the template of PostItem look like?

jonaskuske avatar Jan 28 '23 15:01 jonaskuske

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

kudp02 avatar Mar 17 '23 19:03 kudp02

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

tsboh avatar Jul 29 '23 16:07 tsboh

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>

yassernasc avatar Aug 30 '23 20:08 yassernasc

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

Tobiaqs avatar Sep 13 '23 12:09 Tobiaqs

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

VladBrok avatar Jan 07 '24 14:01 VladBrok

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>

Tobiaqs avatar Feb 28 '24 15:02 Tobiaqs

Closing as it had been fixed via https://github.com/vuejs/core/pull/9421

edison1105 avatar Sep 03 '24 07:09 edison1105