vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.97k stars 33.68k forks source link

Components transition `enter` function not called when using Vue version 2.6.10 #10227

Closed katerlouis closed 5 years ago

katerlouis commented 5 years ago

Version

2.6.10

Reproduction link

https://github.com/katerlouis/vue-2610-bug

Steps to reproduce

serve or build the app and press the toggle button

What is expected?

HelloWorld component should call enter function

What is actually happening?

HelloWorld component doesn't call enter function


enter of the transition directly inside App.vue (the logo img) gets called though, which is odd. With vue v2.5.17 it works as expected (haven't used any other vue version since)

posva commented 5 years ago

You need to use the appear prop for the animation to play initially

katerlouis commented 5 years ago

I didn't say anything about initial render is an issue- if you press the toggle button multiple times, the enter function of HelloWorld still isn't called with Vue 2.6.10, as you can see in the provided demo. Please reopen the issue @posva

kanbekotori commented 5 years ago

I have the same problem, and i down to 2.6.8 to get work.

posva commented 5 years ago

the transition inside HelloWorld needs the prop appear to play at the same time as the parent appears:

  <transition :css="false" @enter="enter" @leave="leave" appear>
    <div class="hello">

For the leave transition it's currently not possible, you can track the feature at https://github.com/vuejs/vue/issues/9328

I hope this helps :)

katerlouis commented 5 years ago

Wait? Doesn't this also mean the HelloWorld component would animate / call the enter function on first load? I understand that this is the purpose of the appear-prop. What if I don't want that?

And the leave transition isn't the issue here. It works as expected. Before the component dismounts, its leave-function gets called.

I'm wondering why this change was introduced. We clearly lose functionality here.

katerlouis commented 5 years ago

@posva could you please look into this again? I'm still under the impression there is a misunderstanding here.

Again: leave is not an issue. And initial animation is not desired.

I still do not understand why the enter function should be called in this situation

// App.vue
<transition @enter="enter" @leave="leave" :css="false">
    <img alt="Vue logo" src="./assets/logo.png" v-if="show">
</transition>

<HelloWorld msg="Helloooo" />

but not here

// HelloWorld.vue
<transition :css="false" @enter="enter" @leave="leave"> 
    <div class="hello">
        <h1>{{ msg }}</h1>
                ....
    </div>
</transition>

It's hard to believe that this is intended. Could you please help me understand?

posva commented 5 years ago

It is intended because the first one is wrapping an element with a v-if while the second isn't and it's directly there when the parent renders. So if you want it to apply the enter transition, you need the appear prop

katerlouis commented 5 years ago

Okay, if I add appear to the transition inside HelloWorld.vue, its enter-function rightfully/expectedly gets called when toggling. Cool.

But it also gets called on initial load of the app, which is not what I want and what I understand is exactly what appear is there for. How do you suggest working around that? Because with this change, as is, I cannot update to the newer vue version in my app.

I read what you are saying, but still not fully understand what you are trying to solve with this new implementation. What was wrong with the way it worked before? Maybe an example would help illustrating the problem the old implementation caused.

LinusBorg commented 5 years ago

The way it works now it the way it was always intended to work.

Bug Report: #9628 PR that fixed it: #9668

But it also gets called on initial load of the app which is not what I want [...]

That could only be the case if the v-if="show" in App.vue were also true when loading the app, which it isn't in your example.

So I assume it is (or can be) true at the initial load of your real app?

How do you suggest working around that?

I would handle this in a global fashion, i.e. set a (non-reactive) flag in the main component to trueafter initial render.

Then each component can check that flag on re-render and determine wether or not appear should be applied.

in App.vue

provide() {
  return {
    isFirstRender: () => { return this.isFirstRender === false }
  }
},
updated() {
  this.isFirstRender = false // do *not* add this prop to `data`!
}

in HelloWorld.vue

<template>
  <transition
    :appear="!isFirstRender()"
    :css="false"
    @enter="enter"
    @leave="leave"
  >
    <div class="hello">
      xxx
    </div>
  </transition>
</template>

<script>
import Vue from 'vue'
import { TweenMax } from 'gsap'
export default Vue.extend({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  inject: ['isFirstRender'],
  methods: {
    enter(el, done) {
      console.log('enter hello world called', el)
      TweenMax.from(el, 2, { opacity: 0, xPercent: -100, onComplete: done })
    },
    leave(el, done) {
      console.log('leave hello world called', el)
      TweenMax.to(el, 2, { opacity: 0, xPercent: -100, onComplete: done })
    },
  },
})
</script>

I used provide/inject so this can be re-used in all of the app in different places that need this.

Other patterns, i.e. with HelloWorld accepting a prop, are also possible to imagine.

katerlouis commented 5 years ago

@LinusBorg I think I found a slight thought error in your code. The pattern works really great, but on initial load your provided function, which asks "Am I the first render?!" returns false, since the flag is still undefined and not false yet;

So if I'm correct the pattern should look like this:

provide() {
  return {
    isFirstRender: () => { return this.isFirstRender === undefined }
  }
},
updated() {
  this.isFirstRender = false // do *not* add this prop to `data`!
}

Atleast that's how it worked for me; now !isFirstRender() is false on first render, as expected;

[EDIT] There's another issue I found with this pattern; you set the initalRender status only if some updated fires, and that only that only happens when data changes. That's not always the case. So I had to do this.$forceUpdate() in App.vues mounted() .. or you could just set this.isFirstRender = false in mounted() ..

Here's what I don't understand: After doing this.isFirstRender = false in App.vues mounted()-hook, the slideOver component still doesn't slide on initial render (which is what I want ..) – but the App.vue mounts earlier than the slide component does, so why is this.isfirstRender still undefined by that time?

[EDIT 2] image Why on earth is mounted() of the SlideOver fired before Apps mounted()?!