Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
feat(nuxt): default router scroll behavior (#3851)
Browse files Browse the repository at this point in the history
Co-authored-by: joel <joel.wenzel@flexagon.com>
Co-authored-by: Pooya Parsa <pooya@pi0.io>
  • Loading branch information
3 people committed Oct 19, 2022
1 parent 66de87a commit ba3a118
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/content/3.api/4.advanced/1.hooks.md
Expand Up @@ -27,6 +27,7 @@ Hook | Arguments | Environment | Description
`link:prefetch` | `to` | Client | Called when a `<NuxtLink>` is observed to be prefetched.
`page:start` | `pageComponent?` | Client | Called on [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event.
`page:finish` | `pageComponent?` | Client | Called on [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event.
`page:transition:finish`| `pageComponent?` | Client | After page transition [onAfterLeave](https://vuejs.org/guide/built-ins/transition.html#javascript-hooks) event.

# Nuxt Hooks (build time)

Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/app/nuxt.ts
Expand Up @@ -34,6 +34,7 @@ export interface RuntimeNuxtHooks {
'link:prefetch': (link: string) => HookResult
'page:start': (Component?: VNode) => HookResult
'page:finish': (Component?: VNode) => HookResult
'page:transition:finish': (Component?: VNode) => HookResult
'vue:setup': () => void
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
}
Expand Down
5 changes: 4 additions & 1 deletion packages/nuxt/src/pages/module.ts
Expand Up @@ -139,11 +139,14 @@ export default defineNuxtModule({
addTemplate({
filename: 'router.options.mjs',
getContents: async () => {
// Check for router options
// Scan and register app/router.options files
const routerOptionsFiles = (await Promise.all(nuxt.options._layers.map(
async layer => await findPath(resolve(layer.config.srcDir, 'app/router.options'))
))).filter(Boolean) as string[]

// Add default options
routerOptionsFiles.unshift(resolve(runtimeDir, 'router.options'))

const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options)
.map(([key, value]) => [key, genString(value as string)]))

Expand Down
2 changes: 2 additions & 0 deletions packages/nuxt/src/pages/runtime/composables.ts
Expand Up @@ -29,6 +29,8 @@ export interface PageMeta {
layoutTransition?: boolean | TransitionProps
key?: false | string | ((route: RouteLocationNormalizedLoaded) => string)
keepalive?: boolean | KeepAliveProps
/** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean
}

declare module 'vue-router' {
Expand Down
31 changes: 25 additions & 6 deletions packages/nuxt/src/pages/runtime/page.ts
@@ -1,6 +1,7 @@
import { computed, defineComponent, h, provide, reactive, onMounted, nextTick, Suspense, Transition, KeepAliveProps, TransitionProps } from 'vue'
import type { DefineComponent, VNode } from 'vue'
import { RouterView } from 'vue-router'
import { defu } from 'defu'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocation } from 'vue-router'

import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
Expand Down Expand Up @@ -34,22 +35,27 @@ export default defineComponent({
},
setup (props, { attrs }) {
const nuxtApp = useNuxtApp()

return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => {
if (!routeProps.Component) { return }

const key = generateRouteKey(props.pageKey, routeProps)
const transitionProps = props.transition ?? routeProps.route.meta.pageTransition ?? (defaultPageTransition as TransitionProps)

const done = nuxtApp.deferHydration()

return _wrapIf(Transition, transitionProps,
const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition)
const transitionProps = hasTransition && _mergeTransitionProps([
props.transition,
routeProps.route.meta.pageTransition,
defaultPageTransition,
{ onAfterLeave: () => { nuxtApp.callHook('page:transition:finish', routeProps.Component) } }
].filter(Boolean))

return _wrapIf(Transition, hasTransition && transitionProps,
wrapInKeepAlive(props.keepalive ?? routeProps.route.meta.keepalive ?? (defaultKeepaliveConfig as KeepAliveProps), h(Suspense, {
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)
}, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition: !!transitionProps } as {}) })
onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)) }
}, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition } as {}) })
)).default()
}
})
Expand All @@ -62,6 +68,19 @@ export default defineComponent({
[key: string]: any
}>

function _toArray (val: any) {
return Array.isArray(val) ? val : (val ? [val] : [])
}

function _mergeTransitionProps (routeProps: TransitionProps[]): TransitionProps {
const _props: TransitionProps[] = routeProps.map(prop => ({
...prop,
onAfterLeave: _toArray(prop.onAfterLeave)
}))
// @ts-ignore
return defu(..._props)
}

const Component = defineComponent({
// TODO: Type props
// eslint-disable-next-line vue/require-prop-types
Expand Down
58 changes: 58 additions & 0 deletions packages/nuxt/src/pages/runtime/router.options.ts
@@ -0,0 +1,58 @@
import type { RouterConfig } from '@nuxt/schema'
import type { RouterScrollBehavior } from 'vue-router'
import { nextTick } from 'vue'
import { useNuxtApp } from '#app'

type ScrollPosition = Awaited<ReturnType<RouterScrollBehavior>>

// Default router options
// https://router.vuejs.org/api/#routeroptions
export default <RouterConfig> {
scrollBehavior (to, from, savedPosition) {
const nuxtApp = useNuxtApp()

// By default when the returned position is falsy or an empty object, vue-router will retain the current scroll position
// savedPosition is only available for popstate navigations (back button)
let position: ScrollPosition = savedPosition || undefined

// Scroll to top if route is changed by default
if (
!position &&
(from && to && from.matched[0] !== to.matched[0]) &&
to.meta.scrollToTop !== false
) {
position = { left: 0, top: 0 }
}

// Hash routes on the same page, no page hook is fired so resolve here
if (to.path !== from.path) {
if (from.hash && !to.hash) {
return { left: 0, top: 0 }
}
if (to.hash) {
return { el: to.hash, top: _getHashElementScrollMarginTop(to.hash) }
}
}

// Wait for `page:transition:finish` or `page:finish` depending on if transitions are enabled or not
const hasTransition = to.meta.pageTransition !== false && from.meta.pageTransition !== false
const hookToWait = hasTransition ? 'page:transition:finish' : 'page:finish'
return new Promise((resolve) => {
nuxtApp.hooks.hookOnce(hookToWait, async () => {
await nextTick()
if (to.hash) {
position = { el: to.hash, top: _getHashElementScrollMarginTop(to.hash) }
}
resolve(position)
})
})
}
}

function _getHashElementScrollMarginTop (selector: string): number {
const elem = document.querySelector(selector)
if (elem) {
return parseFloat(getComputedStyle(elem).scrollMarginTop)
}
return 0
}

0 comments on commit ba3a118

Please sign in to comment.