mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 21:55:11 +00:00
feat(nuxt): default router scroll behavior (#3851)
Co-authored-by: joel <joel.wenzel@flexagon.com> Co-authored-by: Pooya Parsa <pooya@pi0.io>
This commit is contained in:
parent
66de87af91
commit
ba3a11800c
@ -27,6 +27,7 @@ Hook | Arguments | Environment | Description
|
|||||||
`link:prefetch` | `to` | Client | Called when a `<NuxtLink>` is observed to be prefetched.
|
`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: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: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)
|
# Nuxt Hooks (build time)
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ export interface RuntimeNuxtHooks {
|
|||||||
'link:prefetch': (link: string) => HookResult
|
'link:prefetch': (link: string) => HookResult
|
||||||
'page:start': (Component?: VNode) => HookResult
|
'page:start': (Component?: VNode) => HookResult
|
||||||
'page:finish': (Component?: VNode) => HookResult
|
'page:finish': (Component?: VNode) => HookResult
|
||||||
|
'page:transition:finish': (Component?: VNode) => HookResult
|
||||||
'vue:setup': () => void
|
'vue:setup': () => void
|
||||||
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
|
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
|
||||||
}
|
}
|
||||||
|
@ -139,11 +139,14 @@ export default defineNuxtModule({
|
|||||||
addTemplate({
|
addTemplate({
|
||||||
filename: 'router.options.mjs',
|
filename: 'router.options.mjs',
|
||||||
getContents: async () => {
|
getContents: async () => {
|
||||||
// Check for router options
|
// Scan and register app/router.options files
|
||||||
const routerOptionsFiles = (await Promise.all(nuxt.options._layers.map(
|
const routerOptionsFiles = (await Promise.all(nuxt.options._layers.map(
|
||||||
async layer => await findPath(resolve(layer.config.srcDir, 'app/router.options'))
|
async layer => await findPath(resolve(layer.config.srcDir, 'app/router.options'))
|
||||||
))).filter(Boolean) as string[]
|
))).filter(Boolean) as string[]
|
||||||
|
|
||||||
|
// Add default options
|
||||||
|
routerOptionsFiles.unshift(resolve(runtimeDir, 'router.options'))
|
||||||
|
|
||||||
const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options)
|
const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options)
|
||||||
.map(([key, value]) => [key, genString(value as string)]))
|
.map(([key, value]) => [key, genString(value as string)]))
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ export interface PageMeta {
|
|||||||
layoutTransition?: boolean | TransitionProps
|
layoutTransition?: boolean | TransitionProps
|
||||||
key?: false | string | ((route: RouteLocationNormalizedLoaded) => string)
|
key?: false | string | ((route: RouteLocationNormalizedLoaded) => string)
|
||||||
keepalive?: boolean | KeepAliveProps
|
keepalive?: boolean | KeepAliveProps
|
||||||
|
/** Set to `false` to avoid scrolling to top on page navigations */
|
||||||
|
scrollToTop?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { computed, defineComponent, h, provide, reactive, onMounted, nextTick, Suspense, Transition, KeepAliveProps, TransitionProps } from 'vue'
|
import { computed, defineComponent, h, provide, reactive, onMounted, nextTick, Suspense, Transition, KeepAliveProps, TransitionProps } from 'vue'
|
||||||
import type { DefineComponent, VNode } from 'vue'
|
import type { DefineComponent, VNode } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
import { defu } from 'defu'
|
||||||
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocation } from 'vue-router'
|
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocation } from 'vue-router'
|
||||||
|
|
||||||
import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
|
import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
|
||||||
@ -34,22 +35,27 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
setup (props, { attrs }) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
|
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
|
||||||
default: (routeProps: RouterViewSlotProps) => {
|
default: (routeProps: RouterViewSlotProps) => {
|
||||||
if (!routeProps.Component) { return }
|
if (!routeProps.Component) { return }
|
||||||
|
|
||||||
const key = generateRouteKey(props.pageKey, routeProps)
|
const key = generateRouteKey(props.pageKey, routeProps)
|
||||||
const transitionProps = props.transition ?? routeProps.route.meta.pageTransition ?? (defaultPageTransition as TransitionProps)
|
|
||||||
|
|
||||||
const done = nuxtApp.deferHydration()
|
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, {
|
wrapInKeepAlive(props.keepalive ?? routeProps.route.meta.keepalive ?? (defaultKeepaliveConfig as KeepAliveProps), h(Suspense, {
|
||||||
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
||||||
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)
|
onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)) }
|
||||||
}, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition: !!transitionProps } as {}) })
|
}, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition } as {}) })
|
||||||
)).default()
|
)).default()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -62,6 +68,19 @@ export default defineComponent({
|
|||||||
[key: string]: any
|
[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({
|
const Component = defineComponent({
|
||||||
// TODO: Type props
|
// TODO: Type props
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
|
58
packages/nuxt/src/pages/runtime/router.options.ts
Normal file
58
packages/nuxt/src/pages/runtime/router.options.ts
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user