Igid
Igid

Reputation: 515

How to use Vue Router scroll behavior with page transition

I'm using Vue Router 4 on an application where the top level RouterView is wrapped in a transition, like so:

<router-view v-slot="{ Component }">
  <transition mode="out-in">
    <component :is="Component" />
  </PageTransition>
</router-view>

When I try to add scroll behavior to the Router, to scroll to a specific element when users navigate back to the index page, the scroll behavior fires during the out phase of the transition, when the index page isn't mounted yet, so the element isn't found.

eg.

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  scrollBehavior (to, from) {
    if (to.name === 'index' && isContentPage(from)) {
      return { el: '#menu' }
    }

    return undefined
  }
})

I would get a warning in the console: Couldn't find element using selector "#menu" returned by scrollBehavior.

The Vue Router docs on scroll behavior mention that it's possible to work around this issue by hooking up to transition events:

It's possible to hook this up with events from a page-level transition component to make the scroll behavior play nicely with your page transitions, but due to the possible variance and complexity in use cases, we simply provide this primitive to enable specific userland implementations.

But I couldn't figure out what sort of approach it was suggesting, nor could I find any "userland implementations".

Upvotes: 0

Views: 1837

Answers (1)

Igid
Igid

Reputation: 515

Finally I found a solution to this, which was to use a module holding some state—in this case a Promise—to act as the link between the transition and the router.

The module:

let transitionState: Promise<void> = Promise.resolve()
let resolveTransition: (() => void)|null = null

/**
 * Call this before the leave phase of the transition
 */
export function transitionOut (): void {
  transitionState = new Promise(resolve => {
    resolveTransition = resolve
  })
}

/**
 * Call this in the enter phase of the transition
 */
export function transitionIn (): void {
  if (resolveTransition != null) {
    resolveTransition()
    resolveTransition = null
  }
}

/**
 * Await this in scrollBehavior
 */
export function pageTransition (): Promise<void> {
  return transitionState
}

I hooked up the transition events:

<router-view v-slot="{ Component }">
  <transition mode="out-in" @before-leave="transitionOut" @enter="transitionIn">
    <component :is="Component" />
  </PageTransition>
</router-view>

...and in the Router:

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  async scrollBehavior (to, from) {
    if (to.name === 'index' && isContentPage(from)) {
      await pageTransition()
      return { el: '#menu' }
    }

    return undefined
  }
})

What's more, I actually have a nested RouterView also wrapped in a transition. With that transition extracted to a component, both instances could call transitionOut and transitionIn and it seems to work (though I haven't tested it much for race conditions).

If anyone has found simpler solutions though, I'd be interested to see them.

Upvotes: 1

Related Questions