vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.87k stars 546 forks source link

Support for KeepAlive and Transition in Vue Router 4 #160

Closed posva closed 4 years ago

posva commented 4 years ago

Rendered

dobromir-hristov commented 4 years ago

Looks good. I think that if you want to use a transition or keep-alive with vue router, you are already going into more advanced territory. 😆 This solution makes pretty good sense, and having it well explained in the docs is good enough.

If it was possible to detect if router-view is a direct child of a transition, and warn users that it wont work as they expect, maybe even point to docs, that would be even better, but yeah...

aztalbot commented 4 years ago

I think that if you want to use a transition or keep-alive with vue router, you are already going into more advanced territory.

@dobromir-hristov I'm not so sure about that. I remember starting out with Vue and thinking how incredibly simple it was to do things like this. Slots and dynamic components are not things you try to use; but making everything look "cool" by adding transitions definitely is. The CSS part of the transition is already enough of a barrier to beginners that adding slots and dynamic components makes this, potentially, a bit of a daunting task for beginners. In my mind it could be the difference between having fun while learning Vue and getting maybe a little too frustrated or feeling a little too overwhelmed.

@posva I think this RFC is great in terms of the details for how to support Transition and Keep-Alive. Are there any plans or ideas, though, for abstracting this away from the user a little via transition props on router-view or, to avoid coupling with transition, using a special transition HOC? I know Vue 3 has leaned toward less magic (more explicitness, which is great), but I do think it is important to abstract away some of these things for beginners.

To simplify things for the end-user, I'm thinking of introducing a transition component deep-transition that takes it's default slot and clones it via cloneVNode, and it passes in a special vnode render prop (rather than a slot), and router-view then falls back to this special render prop if it does not have it's own default slot. That way we can achieve the below.

<deep-transition>
    <router-view></router-view>
</deep-transition>

I think the user can still use the approach in this RFC, but this would be a simplified, more accessible version when using Vue templates. Also, the deep-transition HOC can be used by other libraries/components, like a Tabs components that might need similar logic. In my experimenting with the idea I got the below to work (very rough/experimental code):

  <DeepTransition name="fade" mode="out-in">
    <Transitionable :activeSlot="slot">
      <template #Another>
        <h2>Another</h2>
      </template>
      <template #About>
        <h1>About</h1>
      </template>
    </Transitionable>
  </DeepTransition>
…
const vTransition = "vTransition"

function DeepTransition(props, { slots }) {
  return slots.default().map(vNode => cloneVNode(vNode, {
    [vTransition]: ({ Component, attrs }) => h(Transition, props, {
        default: () => [
            Component
              ? (openBlock(), createBlock(Component, mergeProps({ key: 0 }, attrs), null, 16 /* FULL_PROPS */))
              : createCommentVNode("v-if", true)
          ],
        _: 1
      }, 1024)
  }))
}

function Transitionable(props, { slots }) {
  const Component = () => slots[props.activeSlot]()
  const attrs = { activeSlot: props.activeSlot } // router would go here
  const defaultSlot = slots.default || props[vTransition]
  const vnode = typeof defaultSlot === 'function'
    ? defaultSlot({ Component, attrs })
    : h(defaultSlot)
  return vnode
}

I'm sure there are drawbacks, I just figured I would try to push toward simplifying this use case since it was one I clearly remember being a very positive experience when learning Vue.


Admittedly, I have only been able to get this to work with functional components (both with and without props bound to the Transitionable component, using provide/inject). With a stateful setup component, it doesn't quite work; what's weird is it errors on the first change with TypeError: "leavingVNode.el is null" and then it works just fine 🤔 .


realizing now this only works cause I have slots in there 🤦 ... so is there really no way to make this work? maybe via provide/inject, where DeepTransition provides a render function to descendants which inject the render function and pass in any children to wrap them in transition?

posva commented 4 years ago

I wished I could keep the same simplicity as in Vue 2 but the best I could come up with is explicitly passing the route prop (https://github.com/vuejs/rfcs/pull/153):

    <transition name="fade">
      <router-view :route="$route" />
    </transition>

If I could automatically make that when no route prop is based, that would be perfect. @yyx990803 is there a way to do so? If not, with a warning in dev mode detecting the transition/keep-alive (getCurrentInstance().vnode.transition but I don't know for Keep Alive) and the absence of the route prop, I already think this is easier than the slot version but not as flexible

Adding DeepTransition, an alternative to Transition doesn't solve the problem of it being complicated and I think it's as confusing as having to use a slot.

I also thought about adding specific props like transition + transition-props but I don't find it very intuitive and only makes RouterView compatible with these specific components but not with others added by the user.

aztalbot commented 4 years ago

@posva I think I agree with you now after trying to figure out several workarounds. It's just an unfortunate loss of simplicity.

Although, I'm not sure an additional DeepTransition component is as confusing. I clearly remember struggling for a while to understand scoped slots and I still see relatively experienced devs confused when they encounter scoped slots in templates (it is definitely much more natural in render functions and obviously once you get it it's super powerful).

For the sake of putting one last proposal forward on this, I was able to get this to work:

  <DeepTransition name="fade" mode="out-in">
    <Tabs />
  </DeepTransition>
...

const vAdopt = 'vAdopt'

const DeepTransition = ((props, { slots }) => {
  const instance = getCurrentInstance()
  const defaultNodes = slots.default()
  const willBeAdopted = defaultNodes.some(vNode => vAdopt in vNode.type.props || vNode.type.props.includes(vAdopt))
  if (willBeAdopted) {
    instance.provides[vAdopt] = (defaultSlot) => h(Transition, props, defaultSlot)
    return defaultNodes
  }
  return h(Transition, () => defaultNodes)
})

const Tabs = defineComponent((props) => {
  const { activeView } = inject('viewData')
  const wrapper = inject(vAdopt, fn => { console.log('no fun'); return fn() })
  return () => {
    return wrapper(() => activeView.value === 'Another' ? h('h1', "Another") : h('h2', "About"))
  }
})

Tabs.props = [vAdopt]

The idea here is that the wrapper component (DeepTransition) looks to see if a child wants to adopt it, and if so, provides itself, then the adopting child can place it wherever it needs to. This prevents the unnecessary v-slot boiler plate for otherwise black-box components. However, it's a bit of a hack, and obviously the transition bleeds into the provides chain for the parent (a little additional logic could provide scoping perhaps). If having an additional DeepTransition is a concern, maybe this could be rolled into the built-in Transition and KeepAlive components?

posva commented 4 years ago

That's a cool concept but I wish it was inside Transition directly, maybe we can introduce a similar concept for advanced integrations? @yyx990803

posva commented 4 years ago

This RFC is now in final comments stage. An RFC in final comments stage means that:

The core team has reviewed the feedback and reached consensus about the general direction of the RFC and believes that this RFC is a worthwhile addition to the framework. Final comments stage does not mean the RFC's design details are final - we may still tweak the details as we implement it and discover new technical insights or constraints. It may even be further adjusted based on user feedback after it lands in an alpha/beta release. If no major objections with solid supporting arguments have been presented after a week, the RFC will be merged and become an active RFC.

taoorange commented 4 years ago

why i use vue-router-next like this router.isReady().then(() => app.mount('#app')),is useful。but when i use it like this app.mount('#app'),It doesn't work my edition is "vue": "^3.0.0-beta.15", "vue-router": "4.0.0-alpha.12",

madmoizo commented 4 years ago

If you can't hardcode the transition name because it depends on the Component, you can create a custom component:

<script>

export default {
  data () {
    return {
      transitionName: ''
    }
  },
  methods: {
    getTransitionName (vnode) {
      if (vnode) {
        this.transitionName = vnode.type.transitionName
      }

      return this.transitionName
    }
  }
}

</script>

<template>

  <RouterView v-slot="{ Component }">
    <transition :name="getTransitionName(Component)">
      <component :is="Component"></component>
    </transition>
  </RouterView>

</template>

Note: you must add a transitionName option to every component rendered by RouterView