Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for KeepAlive and Transition in Vue Router 4 #160

Merged
merged 7 commits into from
Jul 18, 2020

Conversation

posva
Copy link
Member

@posva posva commented Apr 21, 2020

@posva posva added router 3.x This RFC only targets 3.0 and above labels Apr 21, 2020
@dobromir-hristov
Copy link
Contributor

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
Copy link

aztalbot commented Apr 24, 2020

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
Copy link
Member Author

posva commented Apr 24, 2020

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 (#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
Copy link

aztalbot commented Apr 24, 2020

@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
Copy link
Member Author

posva commented Apr 24, 2020

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
Copy link
Member Author

posva commented Jul 10, 2020

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.

@posva posva added the final comments This RFC is in final comments period label Jul 10, 2020
@posva posva merged commit ad5111b into vuejs:master Jul 18, 2020
@posva posva deleted the router/router-view-slot branch July 18, 2020 14:38
@taoorange
Copy link

taoorange commented Jul 31, 2020

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
Copy link

madmoizo commented Oct 1, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.x This RFC only targets 3.0 and above breaking change This RFC contains breaking changes or deprecations of old API. final comments This RFC is in final comments period router
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants