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

feat(nuxt): default router scroll behavior #3851

Merged
merged 31 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
658cd35
added page:transition:finish hook
joel-wenzel Mar 23, 2022
354af7c
default scroll behavior
joel-wenzel Mar 23, 2022
dc02ea1
support no transition and no suspense between pages
joel-wenzel Mar 23, 2022
37ce115
better handling when no transitions are set
joel-wenzel Mar 23, 2022
5a5b895
fix (router): simpler default scroll behavior
joel-wenzel Mar 30, 2022
6af9fec
removed window on load scroll
joel-wenzel Mar 31, 2022
131dcaa
Merge remote-tracking branch 'upstream/main' into feature/scroll-beha…
joel-wenzel Mar 31, 2022
6c80f33
Merge branch 'main' into feature/scroll-behavior
joel-wenzel Apr 4, 2022
5dd0498
chore(nuxt3): white spacing fix
joelflexagon Apr 6, 2022
e434275
Merge remote-tracking branch 'upstream/main' into feature/scroll-beha…
joel-wenzel Apr 8, 2022
d613f2c
docs: re-added page:transition:finish
joel-wenzel Apr 8, 2022
e1658f5
Merge branch 'main' of https://github.com/nuxt/framework into feature…
joel-wenzel May 9, 2022
293e7fc
feat (routing): handle anchor tags on the same page
joel-wenzel May 9, 2022
c93ee6c
Merge remote-tracking branch 'upstream/main' into feature/scroll-beha…
joel-wenzel Jun 22, 2022
7273c98
Merge branch 'main' into feature/scroll-behavior
pi0 Jun 27, 2022
a99e413
Merge branch 'main' into feature/scroll-behavior
pi0 Jul 6, 2022
533ce5c
Merge branch 'main' into pr/joel-wenzel/3851
pi0 Aug 2, 2022
21149bc
Merge branch 'main' into feature/scroll-behavior
joel-wenzel Aug 17, 2022
f0fd7f5
Merge remote-tracking branch 'upstream/main' into feature/scroll-beha…
joel-wenzel Oct 13, 2022
a038a3e
fix (pages): keep user defined onAfterLeave transition if specified
joel-wenzel Oct 13, 2022
535b116
fix (pages): include both route meta and prop transition objects
joel-wenzel Oct 13, 2022
e4a89a0
Merge branch 'main' into feature/scroll-behavior
joel-wenzel Oct 14, 2022
1fb8a9e
Merge branch 'main' into pr/joel-wenzel/3851
pi0 Oct 19, 2022
6f94bc7
fix transition props merging
pi0 Oct 19, 2022
81568e6
lint docs
pi0 Oct 19, 2022
44418dd
refactor and improvements for default scroll behavior
pi0 Oct 19, 2022
9cceb19
more refactors and move default options to runtime
pi0 Oct 19, 2022
a0aaf3f
update module logic
pi0 Oct 19, 2022
9240ba8
restyle page.ts
pi0 Oct 19, 2022
3ec9051
fix: only scroll to top for non children
pi0 Oct 19, 2022
023936f
support `scrollToTop` page meta
pi0 Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/3.api/4.advanced/1.hooks.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}