mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +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.
|
||||
`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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)]))
|
||||
|
||||
|
@ -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' {
|
||||
|
@ -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'
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
@ -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
|
||||
|
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