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 18 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
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 17 additions & 16 deletions docs/content/3.api/4.advanced/1.hooks.md
Expand Up @@ -6,22 +6,23 @@

Check the [app source code](https://github.com/nuxt/framework/blob/main/packages/nuxt/src/app/nuxt.ts#L24) for all available hooks.

Hook | Arguments | Environment | Description
-----------------------|---------------------|-----------------|-------------
`app:created` | `vueApp` | Server & Client | Called when initial `vueApp` instance is created.
`app:error` | `err` | Server & Client | Called when a fatal error occurs.
`app:error:cleared` | `{ redirect? }` | Server & Client | Called when a fatal error occurs.
`app:data:refresh` | `keys?` | Server & Client | (internal)
`meta:register` | `metaRenderers` | Server & Client | (internal)
`vue:setup` | - | Server & Client | (internal)
`vue:error` | `err, target, info` | Server & Client | Called when a vue error propages to the root component. [Learn More](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
`app:rendered` | `renderContext` | Server | Called when SSR rendering is done.
`app:redirected` | - | Server | Called before SSR redirection.
`app:beforeMount` | `vueApp` | Client | Called before mounting the app, called only on client side.
`app:mounted` | `vueApp` | Client | Called when Vue app is initialized and mounted in browser.
`app:suspense:resolve` | `appComponent` | Client | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved 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.
Hook | Arguments | Environment | Description
------------------------|---------------------|-----------------|-------------
`app:created` | `vueApp` | Server & Client | Called when initial `vueApp` instance is created.
`app:error` | `err` | Server & Client | Called when a fatal error occurs.
`app:error:cleared` | `{ redirect? }` | Server & Client | Called when a fatal error occurs.
`app:data:refresh` | `keys?` | Server & Client | (internal)
`meta:register` | `metaRenderers` | Server & Client | (internal)
`vue:setup` | - | Server & Client | (internal)
`vue:error` | `err, target, info` | Server & Client | Called when a vue error propages to the root component. [Learn More](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
`app:rendered` | `renderContext` | Server | Called when SSR rendering is done.
`app:redirected` | - | Server | Called before SSR redirection.
`app:beforeMount` | `vueApp` | Client | Called before mounting the app, called only on client side.
`app:mounted` | `vueApp` | Client | Called when Vue app is initialized and mounted in browser.
`app:suspense:resolve` | `appComponent` | Client | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved 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: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 @@ -33,6 +33,7 @@ export interface RuntimeNuxtHooks {
'app:data:refresh': (keys?: string[]) => HookResult
'page:start': (Component?: VNode) => HookResult
'page:finish': (Component?: VNode) => HookResult
'page:transition:finish': (Component?: VNode) => HookResult
'meta:register': (metaRenderers: Array<(nuxt: NuxtApp) => NuxtMeta | Promise<NuxtMeta>>) => HookResult
'vue:setup': () => void
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
Expand Down
4 changes: 4 additions & 0 deletions packages/nuxt/src/pages/module.ts
Expand Up @@ -133,6 +133,7 @@ export default defineNuxtModule({
filename: 'router.options.mjs',
getContents: async () => {
// Check for router options
const defaultOptsFile = resolve(distDir, 'pages/routing/default-router.options')
const routerOptionsFiles = (await Promise.all(nuxt.options._layers.map(
async layer => await findPath(resolve(layer.config.srcDir, 'app/router.options'))
))).filter(Boolean)
Expand All @@ -142,8 +143,11 @@ export default defineNuxtModule({

return [
...routerOptionsFiles.map((file, index) => genImport(file, `routerOptions${index}`)),
defaultOptsFile ? genImport(defaultOptsFile, 'defaultOptions') : '',

`const configRouterOptions = ${configRouterOptions}`,
'export default {',
'...(defaultOptions || {}),',
'...configRouterOptions,',
// We need to reverse spreading order to respect layers priority
...routerOptionsFiles.map((_, index) => `...routerOptions${index},`).reverse(),
Expand Down
60 changes: 60 additions & 0 deletions packages/nuxt/src/pages/routing/default-router.options.ts
@@ -0,0 +1,60 @@
import type { RouterConfig } from '@nuxt/schema'
import { nextTick } from 'vue'
import { useNuxtApp } from '#app'

// https://router.vuejs.org/api/#routeroptions
export default <RouterConfig>{
scrollBehavior (to, from, savedPosition) {
const nuxtApp = useNuxtApp()
// If the returned position is falsy or an empty object, will retain current scroll position
let position
const isRouteChanged = to !== from

// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
} else if (isRouteChanged) {
position = { left: 0, top: 0 }
pi0 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 }
} else if (to.hash) {
return {
el: to.hash,
top: getHashElementScrollMarginTop()
}
}
}

// if either to or from has no transition then wait for page:finish
const hasTransition = to.meta.pageTransition !== false && from.meta.pageTransition !== false
const hookAwait = hasTransition ? 'page:transition:finish' : 'page:finish'

function getHashElementScrollMarginTop () {
// vue-router does not incorporate scroll-margin-top on its own.
const elem = document.querySelector(to.hash)

if (elem) {
return parseFloat(getComputedStyle(elem).scrollMarginTop)
}
return 0
}

return new Promise((resolve) => {
nuxtApp.hooks.hookOnce(hookAwait, async () => {
await nextTick()

if (to.hash) {
position = {
el: to.hash,
top: getHashElementScrollMarginTop()
}
}
resolve(position)
})
})
}
}
29 changes: 25 additions & 4 deletions packages/nuxt/src/pages/runtime/page.ts
@@ -1,6 +1,5 @@
import { computed, DefineComponent, defineComponent, h, inject, provide, reactive, Suspense, Transition } from 'vue'
import { computed, DefineComponent, defineComponent, h, inject, nextTick, provide, reactive, Suspense, Transition } from 'vue'
import { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouterView } from 'vue-router'

import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
import { useNuxtApp } from '#app'
import { _wrapIf } from '#app/components/utils'
Expand Down Expand Up @@ -28,20 +27,42 @@ export default defineComponent({
const isNested = inject(isNestedKey, false)
provide(isNestedKey, true)

function getTransitionProps (routeProps: RouterViewSlotProps) {
const metaTransition = routeProps.route.meta.pageTransition
const onAfterLeave = () => {
nuxtApp.callHook('page:transition:finish', routeProps.Component)
}

if (typeof metaTransition === 'boolean') {
return metaTransition && {
...defaultPageTransition,
onAfterLeave
joel-wenzel marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
return {
...defaultPageTransition,
...(metaTransition || {}),
onAfterLeave
}
}
}

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

const key = generateRouteKey(props.pageKey, routeProps)

return _wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
return _wrapIf(Transition, getTransitionProps(routeProps),
wrapInKeepAlive(routeProps.route.meta.keepalive, isNested && nuxtApp.isHydrating
// Include route children in parent suspense
? h(Component, { key, routeProps, pageKey: key } as {})
: h(Suspense, {
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
onResolve: () => {
nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component))
}
}, { default: () => h(Component, { key, routeProps, pageKey: key } as {}) })
)).default()
}
Expand Down