Randy Hall
Randy Hall

Reputation: 8177

VueJS 3 transition-group control parent size

I have a group of elements showing/hiding/rearranging with a transition group, nothing fancy.

.cross-fade-leave-active {
    transition: transform $fadeSpeed ease-in-out, opacity $fadeSpeed ease-in-out;
    position: absolute;
}
.cross-fade-enter-active {
    transition: opacity $fadeSpeed ease-in-out $fadeSpeed;
}
.cross-fade-enter-from,
.cross-fade-leave-to {
    opacity: 0;
}  
.cross-fade-move{
    transition: transform $fadeSpeed ease-in $fadeSpeed, opacity $fadeSpeed ease-in-out;
}

This works well except in situations where the dynamic height parent container is part of a flow of elements. The parent immediately snaps to the Last stage of the FLIP animation, while the contents of the transition-group smoothly do their thing.

While functionally it makes sense why it's happening, it's far from ideal.

Is there a straightforward way to hook into the Vue FLIP animation to grab the First and Last properties of the parent, so I can set the max height for transition?


My attempt

<div :ref="`container${i}`" :style="{'max-height': containers[`container${i}`]}">
    <transition-group name="cross-fade"
    @after-enter="clearMaxHeight(`container${i}`)" 
    @after-leave="clearMaxHeight(`container${i}`)" 
    @before-enter="maxHeight(`container${i}`)" 
    @enter="maxHeight(`container${i}`)" 
    @before-leave="maxHeight(`container${i}`)" 
    @leave="maxHeight(`container${i}`)"
    >
    //...conditional elements
    </transition-group>
</div>

maxHeight(ref){
    let container = this.$refs[ref];            
    this.containers[ref] = container.clientHeight + 'px';
},
clearMaxHeight(ref){
    this.containers[ref] = 'none';
},

I would think, in theory, that before-enter or before-leave would capture the height of the element before the transition, which would lock in the First height of the parent. Then enter or leave would capture the new immediately after the elements have been added/removed from the flow, giving the Last height. Finally, when the animations are done, just set max height back to none so it can behave normally.

But this doesn’t work like that. Even with the transition pumped up to 5s, I see max-height getting set to a number for a split second, then immediately back to 'none'. Maybe I misunderstand the lifecycle of the Vue animation hooks, but the docs seem a little sparse on the exact execution.

Upvotes: 1

Views: 1840

Answers (1)

Randy Hall
Randy Hall

Reputation: 8177

Disclaimer

It's a little buggy, but I think there may actually be an issue with transition-group (or undocumented feature that's trying and failing to do this very thing). Basically, some elements outside the transition-group container are most definitely getting transform matrices and -move classes added to them by something in Vue in reaction to this, which is annoying.

However, this does basically do what I set out to do originally, maybe you can improve it. Or maybe I'll dig into the Vue3 source code and force it to behave.


Setup:

    const firstAni = {};
    const aniContainer = {};
    
    const aniFirst = (i) => {
        let container = aniContainer[i];
        firstAni[i] = container.offsetHeight;
    };
    const aniLast = (i) => {
        let container = aniContainer[i];
        let lastAni = container.offsetHeight;

        const aniClear = (e) => {
            if(e.target !== container){
                return false;
            }
            container.removeEventListener('transitionend', aniClear);
            container.removeEventListener('transitioncancel', aniClear);
            container.style.height = 'auto';
        };

        container.style.height = `${firstAni[i]}px`;
        
        container.addEventListener('transitionend', aniClear);
        container.addEventListener('transitioncancel', aniClear);

        nextTick(() => {
            container.style.height = `${lastAni}px`;
        });
    };
    const setAniRef = (el, i) => {
        aniContainer[i] = el;
    };

The template:

<div class="cross-fade-wrapper" :ref="(el) => setAniRef(el, i)">
    <transition-group name="cross-fade"
        @before-enter="(el) => aniFirst(i, el)"
        @before-leave="(el) => aniFirst(i, el)"
        @enter="(el, done) => aniLast(i, el, done)"
        @leave="(el, done) => aniLast(i, el, done)"
    >
    <!-- all the conditional elements -->
    </transition-group>
</div>
        

Style it up:

$fadeSpeed: 0.1s;
.cross-fade-wrapper{
    position: relative;
    transition: height $fadeSpeed ease-out;
}
.cross-fade-leave-active {
    transition: opacity $fadeSpeed / 2 ease-out;
    position: absolute;
}
.cross-fade-enter-active {
    transition: opacity $fadeSpeed / 2 ease-in $fadeSpeed * 0.8;
}
.cross-fade-enter-from,
.cross-fade-leave-to {
    opacity: 0;
}  
.cross-fade-move{
    transition: transform $fadeSpeed ease-out;
}

Basically per FLIP, sorta:

  • Set a reference to the container.
  • Get the container's height before the enter/leave happens (First).
  • Get the container's height again immediately when enter/leave starts (Last), then set the height of the container to the First value.
  • Next tick, set the container's height to the Last value.
  • Let the animation do its thing.
  • When animation is finished (watched via listeners) set the container height back to auto.

Upvotes: 0

Related Questions