From 658cd35120b6d4f49d99e9b10d55a5847268623e Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Tue, 22 Mar 2022 20:02:59 -0500 Subject: [PATCH 01/19] added page:transition:finish hook --- docs/content/3.docs/4.advanced/4.hooks.md | 19 +++++------ packages/nuxt3/src/app/nuxt.ts | 1 + packages/nuxt3/src/pages/runtime/page.ts | 39 ++++++++++++++++++++--- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/docs/content/3.docs/4.advanced/4.hooks.md b/docs/content/3.docs/4.advanced/4.hooks.md index 93a8199937c..05011c380d9 100644 --- a/docs/content/3.docs/4.advanced/4.hooks.md +++ b/docs/content/3.docs/4.advanced/4.hooks.md @@ -50,14 +50,15 @@ Check the [source code](https://github.com/nuxt/framework/blob/main/packages/nux **Note:** Please note -Hook | Arguments | Description ------------------------|-------------------|--------------- -`app:created` | `vueApp` | When initial `vueApp` instance is created -`app:beforeMount` | `vueApp` | Same as `app:created` -`app:mounted` | `vueApp` | When Vue app is initialized and mounted in browser -`app:rendered` | - | When SSR rendering is done -`app:suspense:resolve` | `appComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event -`page:start` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event -`page:finish` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event +Hook | Arguments | Description +------------------------|-------------------|--------------- +`app:created` | `vueApp` | When initial `vueApp` instance is created +`app:beforeMount` | `vueApp` | Same as `app:created` +`app:mounted` | `vueApp` | When Vue app is initialized and mounted in browser +`app:rendered` | - | When SSR rendering is done +`app:suspense:resolve` | `appComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event +`page:start` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event +`page:finish` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event +`page:transition:finish`| `pageComponent` | After page transition onAfterLeave event `meta:register` | `metaRenderers` | (internal) `vue:setup` | - | (internal) diff --git a/packages/nuxt3/src/app/nuxt.ts b/packages/nuxt3/src/app/nuxt.ts index d3af122c2cd..8fd246a2f36 100644 --- a/packages/nuxt3/src/app/nuxt.ts +++ b/packages/nuxt3/src/app/nuxt.ts @@ -25,6 +25,7 @@ export interface RuntimeNuxtHooks { 'app:error:cleared': (options: { redirect?: 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>) => HookResult 'vue:setup': () => void 'vue:error': (...args: Parameters[0]>) => HookResult diff --git a/packages/nuxt3/src/pages/runtime/page.ts b/packages/nuxt3/src/pages/runtime/page.ts index c51c58f9853..745ec871f4d 100644 --- a/packages/nuxt3/src/pages/runtime/page.ts +++ b/packages/nuxt3/src/pages/runtime/page.ts @@ -16,14 +16,43 @@ export default defineComponent({ setup (props) { const nuxtApp = useNuxtApp() + function hasTransition (routeProps: RouterViewSlotProps) { + return routeProps.route.meta.pageTransition !== false + } + + 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 + } + } else { + return { + ...defaultPageTransition, + ...(metaTransition || {}), + onAfterLeave + } + } + } + return () => { return h(RouterView, {}, { default: (routeProps: RouterViewSlotProps) => routeProps.Component && - _wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition, - wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, { - onPending: () => nuxtApp.callHook('page:start', routeProps.Component), - onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component) - }, { default: () => h(routeProps.Component, { key: generateRouteKey(props.pageKey, routeProps) } as {}) }))).default() + _wrapIf(Transition, getTransitionProps(routeProps), + wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, { + onPending: () => nuxtApp.callHook('page:start', routeProps.Component), + onResolve: () => { + nuxtApp.callHook('page:finish', routeProps.Component) + if (!hasTransition(routeProps)) { + nuxtApp.callHook('page:transition:finish', routeProps.Component) + } + } + }, { default: () => h(routeProps.Component, { key: generateRouteKey(props.pageKey, routeProps) } as {}) }))).default() }) } } From 354af7c9e0e6f6a1cd5d34877288f249cf28952c Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Tue, 22 Mar 2022 20:03:19 -0500 Subject: [PATCH 02/19] default scroll behavior --- packages/nuxt3/src/pages/module.ts | 4 + .../pages/routing/default-router.options.ts | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 packages/nuxt3/src/pages/routing/default-router.options.ts diff --git a/packages/nuxt3/src/pages/module.ts b/packages/nuxt3/src/pages/module.ts index fb8301413eb..ccadde8f00f 100644 --- a/packages/nuxt3/src/pages/module.ts +++ b/packages/nuxt3/src/pages/module.ts @@ -84,12 +84,16 @@ export default defineNuxtModule({ getContents: async () => { // Check for router options const routerOptionsFile = await findPath('~/app/router.options') + const defaultOptsFile = resolve(distDir, 'pages/routing/default-router.options') + const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options) .map(([key, value]) => [key, genString(value as string)])) return [ routerOptionsFile ? genImport(routerOptionsFile, 'routerOptions') : '', + defaultOptsFile ? genImport(defaultOptsFile, 'defaultOptions') : '', `const configRouterOptions = ${configRouterOptions}`, 'export default {', + '...(defaultOptions || {}),', '...configRouterOptions,', routerOptionsFile ? '...routerOptions' : '', '}' diff --git a/packages/nuxt3/src/pages/routing/default-router.options.ts b/packages/nuxt3/src/pages/routing/default-router.options.ts new file mode 100644 index 00000000000..a8136f34375 --- /dev/null +++ b/packages/nuxt3/src/pages/routing/default-router.options.ts @@ -0,0 +1,82 @@ +import type { RouterOptions } from '@nuxt/schema' +import { nextTick } from 'vue' +import { useNuxtApp } from '#app' + +function setScrollRestoration (newVal) { + try { + window.history.scrollRestoration = newVal + } catch (e) { } +} + +if (process.client) { + if ('scrollRestoration' in window.history) { + setScrollRestoration('manual') + + // reset scrollRestoration to auto when leaving page, allowing page reload + // and back-navigation from other pages to use the browser to restore the + // scrolling position. + window.addEventListener('beforeunload', () => { + setScrollRestoration('auto') + }) + + // Setting scrollRestoration to manual again when returning to this page. + window.addEventListener('load', () => { + setScrollRestoration('manual') + }) + } +} + +// https://router.vuejs.org/api/#routeroptions +export default { + 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 } + } + + return new Promise((resolve) => { + nuxtApp.hooks.hookOnce('page:transition:finish', async () => { + await nextTick() + + // coords will be used if no selector is provided, + // or if the selector didn't match any element. + if (to.hash) { + let hash = to.hash + // CSS.escape() is not supported with IE and Edge. + if ( + typeof window.CSS !== 'undefined' && + typeof window.CSS.escape !== 'undefined' + ) { + hash = '#' + window.CSS.escape(hash.substr(1)) + } + try { + const elem = document.querySelector(hash) + + // vue-router does not incorporate scroll-margin-top on its own. + if (elem) { + const offset = parseFloat(getComputedStyle(elem).scrollMarginTop) + position = { + selector: hash, + offset: { y: offset } + } + } else { + position = { selector: hash } + } + } catch (e) { + console.warn( + 'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).' + ) + } + } + resolve(position) + }) + }) + } +} From dc02ea12ae0322059b343ac119ba4347cf35ad50 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Tue, 22 Mar 2022 21:43:39 -0500 Subject: [PATCH 03/19] support no transition and no suspense between pages --- .../nuxt3/src/pages/routing/default-router.options.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/nuxt3/src/pages/routing/default-router.options.ts b/packages/nuxt3/src/pages/routing/default-router.options.ts index a8136f34375..177144d592b 100644 --- a/packages/nuxt3/src/pages/routing/default-router.options.ts +++ b/packages/nuxt3/src/pages/routing/default-router.options.ts @@ -41,6 +41,17 @@ export default { position = { left: 0, top: 0 } } + // if either to or from has no transition + // and to is not async setup, then we can scroll right away + const hasTransition = to.meta.pageTransition !== false && from.meta.pageTransition !== false + if (!hasTransition) { + const proto = Object.getPrototypeOf(to.matched[0].components.default.setup || '') + + if (proto.constructor.name !== 'AsyncFunction') { + return position + } + } + return new Promise((resolve) => { nuxtApp.hooks.hookOnce('page:transition:finish', async () => { await nextTick() From 37ce11536138c181b0e56871f878546b66aa5561 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Tue, 22 Mar 2022 22:34:24 -0500 Subject: [PATCH 04/19] better handling when no transitions are set --- .../src/pages/routing/default-router.options.ts | 13 +++---------- packages/nuxt3/src/pages/runtime/page.ts | 11 ++--------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/nuxt3/src/pages/routing/default-router.options.ts b/packages/nuxt3/src/pages/routing/default-router.options.ts index 177144d592b..b66d7da5600 100644 --- a/packages/nuxt3/src/pages/routing/default-router.options.ts +++ b/packages/nuxt3/src/pages/routing/default-router.options.ts @@ -41,19 +41,12 @@ export default { position = { left: 0, top: 0 } } - // if either to or from has no transition - // and to is not async setup, then we can scroll right away + // if either to or from has no transition then wait for page:finish const hasTransition = to.meta.pageTransition !== false && from.meta.pageTransition !== false - if (!hasTransition) { - const proto = Object.getPrototypeOf(to.matched[0].components.default.setup || '') - - if (proto.constructor.name !== 'AsyncFunction') { - return position - } - } + const hookAwait = hasTransition ? 'page:transition:finish' : 'page:finish' return new Promise((resolve) => { - nuxtApp.hooks.hookOnce('page:transition:finish', async () => { + nuxtApp.hooks.hookOnce(hookAwait, async () => { await nextTick() // coords will be used if no selector is provided, diff --git a/packages/nuxt3/src/pages/runtime/page.ts b/packages/nuxt3/src/pages/runtime/page.ts index 745ec871f4d..c85f24ee41b 100644 --- a/packages/nuxt3/src/pages/runtime/page.ts +++ b/packages/nuxt3/src/pages/runtime/page.ts @@ -1,4 +1,4 @@ -import { defineComponent, h, Suspense, Transition } from 'vue' +import { defineComponent, h, Suspense, Transition, nextTick } from 'vue' import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router' import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils' @@ -16,10 +16,6 @@ export default defineComponent({ setup (props) { const nuxtApp = useNuxtApp() - function hasTransition (routeProps: RouterViewSlotProps) { - return routeProps.route.meta.pageTransition !== false - } - function getTransitionProps (routeProps: RouterViewSlotProps) { const metaTransition = routeProps.route.meta.pageTransition const onAfterLeave = () => { @@ -47,10 +43,7 @@ export default defineComponent({ wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, { onPending: () => nuxtApp.callHook('page:start', routeProps.Component), onResolve: () => { - nuxtApp.callHook('page:finish', routeProps.Component) - if (!hasTransition(routeProps)) { - nuxtApp.callHook('page:transition:finish', routeProps.Component) - } + nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component)) } }, { default: () => h(routeProps.Component, { key: generateRouteKey(props.pageKey, routeProps) } as {}) }))).default() }) From 5a5b8955f613db0834bec3e17ca0812375db72da Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Wed, 30 Mar 2022 18:58:38 -0500 Subject: [PATCH 05/19] fix (router): simpler default scroll behavior --- .../pages/routing/default-router.options.ts | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/nuxt3/src/pages/routing/default-router.options.ts b/packages/nuxt3/src/pages/routing/default-router.options.ts index b66d7da5600..3469437d8b9 100644 --- a/packages/nuxt3/src/pages/routing/default-router.options.ts +++ b/packages/nuxt3/src/pages/routing/default-router.options.ts @@ -49,34 +49,19 @@ export default { nuxtApp.hooks.hookOnce(hookAwait, async () => { await nextTick() - // coords will be used if no selector is provided, - // or if the selector didn't match any element. if (to.hash) { - let hash = to.hash - // CSS.escape() is not supported with IE and Edge. - if ( - typeof window.CSS !== 'undefined' && - typeof window.CSS.escape !== 'undefined' - ) { - hash = '#' + window.CSS.escape(hash.substr(1)) + let top = 0 + + // vue-router does not incorporate scroll-margin-top on its own. + const elem = document.querySelector(to.hash) + + if (elem) { + top = parseFloat(getComputedStyle(elem).scrollMarginTop) } - try { - const elem = document.querySelector(hash) - // vue-router does not incorporate scroll-margin-top on its own. - if (elem) { - const offset = parseFloat(getComputedStyle(elem).scrollMarginTop) - position = { - selector: hash, - offset: { y: offset } - } - } else { - position = { selector: hash } - } - } catch (e) { - console.warn( - 'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).' - ) + position = { + el: to.hash, + top } } resolve(position) From 6af9fecbec10c452e0ec7c3d93f5c241f01a28a7 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Wed, 30 Mar 2022 19:05:04 -0500 Subject: [PATCH 06/19] removed window on load scroll --- .../pages/routing/default-router.options.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/nuxt3/src/pages/routing/default-router.options.ts b/packages/nuxt3/src/pages/routing/default-router.options.ts index 3469437d8b9..417e94cc27b 100644 --- a/packages/nuxt3/src/pages/routing/default-router.options.ts +++ b/packages/nuxt3/src/pages/routing/default-router.options.ts @@ -2,30 +2,6 @@ import type { RouterOptions } from '@nuxt/schema' import { nextTick } from 'vue' import { useNuxtApp } from '#app' -function setScrollRestoration (newVal) { - try { - window.history.scrollRestoration = newVal - } catch (e) { } -} - -if (process.client) { - if ('scrollRestoration' in window.history) { - setScrollRestoration('manual') - - // reset scrollRestoration to auto when leaving page, allowing page reload - // and back-navigation from other pages to use the browser to restore the - // scrolling position. - window.addEventListener('beforeunload', () => { - setScrollRestoration('auto') - }) - - // Setting scrollRestoration to manual again when returning to this page. - window.addEventListener('load', () => { - setScrollRestoration('manual') - }) - } -} - // https://router.vuejs.org/api/#routeroptions export default { scrollBehavior (to, from, savedPosition) { From 5dd0498c549e0cd0b1c61fd7250674f13ab4a8cb Mon Sep 17 00:00:00 2001 From: joel Date: Wed, 6 Apr 2022 08:36:44 -0500 Subject: [PATCH 07/19] chore(nuxt3): white spacing fix --- packages/nuxt3/src/pages/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt3/src/pages/module.ts b/packages/nuxt3/src/pages/module.ts index 0c314d6233d..3769f54af6a 100644 --- a/packages/nuxt3/src/pages/module.ts +++ b/packages/nuxt3/src/pages/module.ts @@ -94,7 +94,7 @@ export default defineNuxtModule({ return [ ...routerOptionsFiles.map((file, index) => genImport(file, `routerOptions${index}`)), defaultOptsFile ? genImport(defaultOptsFile, 'defaultOptions') : '', - + `const configRouterOptions = ${configRouterOptions}`, 'export default {', '...(defaultOptions || {}),', From d613f2c2120f1360333076ac93ee5689edc45b65 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Thu, 7 Apr 2022 21:02:54 -0500 Subject: [PATCH 08/19] docs: re-added page:transition:finish --- docs/content/3.api/4.advanced/1.hooks.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/content/3.api/4.advanced/1.hooks.md b/docs/content/3.api/4.advanced/1.hooks.md index 83f8dd48411..2f0356df73e 100644 --- a/docs/content/3.api/4.advanced/1.hooks.md +++ b/docs/content/3.api/4.advanced/1.hooks.md @@ -7,17 +7,18 @@ Check the [app source code](https://github.com/nuxt/framework/blob/main/packages/nuxt3/src/app/nuxt.ts#L18) for all available hooks. -Hook | Arguments | Description ------------------------|-------------------|--------------- -`app:created` | `vueApp` | When initial `vueApp` instance is created -`app:beforeMount` | `vueApp` | Same as `app:created` -`app:mounted` | `vueApp` | When Vue app is initialized and mounted in browser -`app:rendered` | - | When SSR rendering is done -`app:suspense:resolve` | `appComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event -`page:start` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event -`page:finish` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event -`meta:register` | `metaRenderers` | (internal) -`vue:setup` | - | (internal) +Hook | Arguments | Description +------------------------|-------------------|--------------- +`app:created` | `vueApp` | When initial `vueApp` instance is created +`app:beforeMount` | `vueApp` | Same as `app:created` +`app:mounted` | `vueApp` | When Vue app is initialized and mounted in browser +`app:rendered` | - | When SSR rendering is done +`app:suspense:resolve` | `appComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event +`page:start` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event +`page:finish` | `pageComponent` | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event +`page:transition:finish`| `pageComponent` | After page transition [onAfterLeave](https://vuejs.org/guide/built-ins/transition.html#javascript-hooks) event +`meta:register` | `metaRenderers` | (internal) +`vue:setup` | - | (internal) # Nuxt Hooks (build time) From 293e7fcc41bca9efdf784e148a69304523affebd Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Mon, 9 May 2022 18:27:35 -0500 Subject: [PATCH 09/19] feat (routing): handle anchor tags on the same page --- .../pages/routing/default-router.options.ts | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/pages/routing/default-router.options.ts b/packages/nuxt/src/pages/routing/default-router.options.ts index 9065dfbd63a..6564ec524f8 100644 --- a/packages/nuxt/src/pages/routing/default-router.options.ts +++ b/packages/nuxt/src/pages/routing/default-router.options.ts @@ -17,27 +17,40 @@ export default { 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 } + } 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) { - let top = 0 - - // vue-router does not incorporate scroll-margin-top on its own. - const elem = document.querySelector(to.hash) - - if (elem) { - top = parseFloat(getComputedStyle(elem).scrollMarginTop) - } - position = { el: to.hash, - top + top: getHashElementScrollMarginTop() } } resolve(position) From a038a3ec842528a35eed43dff9c1dcb0828f27a4 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Thu, 13 Oct 2022 05:46:07 -0500 Subject: [PATCH 10/19] fix (pages): keep user defined onAfterLeave transition if specified --- packages/nuxt/src/pages/runtime/page.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index 3c9b4399eea..e5802a55243 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -39,6 +39,9 @@ export default defineComponent({ const metaTransition = routeProps.route.meta.pageTransition const onAfterLeave = () => { nuxtApp.callHook('page:transition:finish', routeProps.Component) + if (metaTransition?.onAfterLeave) { + metaTransition.onAfterLeave() + } } if (typeof metaTransition === 'boolean') { From 535b116ef8b5e7bb585fd8300e6b3b09f0ea1cf7 Mon Sep 17 00:00:00 2001 From: joel-wenzel Date: Thu, 13 Oct 2022 05:59:31 -0500 Subject: [PATCH 11/19] fix (pages): include both route meta and prop transition objects --- packages/nuxt/src/pages/runtime/page.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index e5802a55243..4999c999281 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -37,24 +37,26 @@ export default defineComponent({ function getTransitionProps (routeProps: RouterViewSlotProps) { const metaTransition = routeProps.route.meta.pageTransition + const propTransition = props.transition + const onAfterLeave = () => { nuxtApp.callHook('page:transition:finish', routeProps.Component) if (metaTransition?.onAfterLeave) { metaTransition.onAfterLeave() } + if (propTransition?.onAfterLeave) { + propTransition.onAfterLeave() + } } - if (typeof metaTransition === 'boolean') { - return metaTransition && { - ...defaultPageTransition, - onAfterLeave - } - } else { - return { - ...defaultPageTransition, - ...(metaTransition || {}), - onAfterLeave - } + if (metaTransition === false || propTransition === false) { + return false + } + return { + ...defaultPageTransition, + ...(typeof propTransition !== 'object' ? {} : propTransition), + ...(typeof metaTransition !== 'object' ? {} : metaTransition), + onAfterLeave } } From 6f94bc7e1fc1d4513ff419b1654fcf612402c118 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 13:34:20 +0200 Subject: [PATCH 12/19] fix transition props merging --- packages/nuxt/src/pages/runtime/page.ts | 55 +++++++++++-------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index 0059313d620..f43b680ae43 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/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' @@ -34,49 +35,26 @@ export default defineComponent({ }, setup (props, { attrs }) { const nuxtApp = useNuxtApp() - - function getTransitionProps (routeProps: RouterViewSlotProps) { - const metaTransition = routeProps.route.meta.pageTransition - const propTransition = props.transition - - const onAfterLeave = () => { - nuxtApp.callHook('page:transition:finish', routeProps.Component) - if (metaTransition?.onAfterLeave) { - metaTransition.onAfterLeave() - } - if (propTransition?.onAfterLeave) { - propTransition.onAfterLeave() - } - } - - if (metaTransition === false || propTransition === false) { - return false - } - return { - ...defaultPageTransition, - ...(typeof propTransition !== 'object' ? {} : propTransition), - ...(typeof metaTransition !== 'object' ? {} : 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) - const done = nuxtApp.deferHydration() - const hasExplicitTransitionProps = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition) - - return _wrapIf(Transition, getTransitionProps(routeProps), + 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: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)) } - }, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition: hasExplicitTransitionProps } as {}) }) + }, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition } as {}) }) )).default() } }) @@ -89,6 +67,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 From 81568e6e9454a6abe86f613854b18898631d3fa8 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 13:40:54 +0200 Subject: [PATCH 13/19] lint docs --- docs/content/3.api/4.advanced/1.hooks.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/content/3.api/4.advanced/1.hooks.md b/docs/content/3.api/4.advanced/1.hooks.md index 2cde58b27a1..04829cdb81c 100644 --- a/docs/content/3.api/4.advanced/1.hooks.md +++ b/docs/content/3.api/4.advanced/1.hooks.md @@ -29,7 +29,6 @@ Hook | Arguments | Environment | Description `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) Check the [schema source code](https://github.com/nuxt/framework/blob/main/packages/schema/src/types/hooks.ts#L69) for all available hooks. From 44418ddb9a3a5fe2df0f8bf308aa414c6d4f5aaf Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:06:48 +0200 Subject: [PATCH 14/19] refactor and improvements for default scroll behavior --- .../pages/routing/default-router.options.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/nuxt/src/pages/routing/default-router.options.ts b/packages/nuxt/src/pages/routing/default-router.options.ts index 6564ec524f8..e59f3542a0c 100644 --- a/packages/nuxt/src/pages/routing/default-router.options.ts +++ b/packages/nuxt/src/pages/routing/default-router.options.ts @@ -1,56 +1,48 @@ import type { RouterConfig } from '@nuxt/schema' +import type { RouterScrollBehavior } from 'vue-router' import { nextTick } from 'vue' import { useNuxtApp } from '#app' +type ScrollPosition = Exclude>, false|void> + // https://router.vuejs.org/api/#routeroptions export default { 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) + // By default when the returned position is falsy or an empty object, vue-router will retain the current scroll position + let position: ScrollPosition + + // SavedPosition is only available for popstate navigations (back button) if (savedPosition) { position = savedPosition - } else if (isRouteChanged) { + } else if (to !== from /* Route changed */) { position = { left: 0, top: 0 } } - // hash routes on the same page, no page hook is fired so resolve here - if (to.path === from.path) { + // 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) { + } + if (to.hash) { return { el: to.hash, - top: getHashElementScrollMarginTop() + top: _getHashElementScrollMarginTop(to.hash) } } } - // if either to or from has no transition then wait for page:finish + // Wait for page:transition:finish or page:finish depending on transitions enabled or not 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 - } - + const hookToWait = hasTransition ? 'page:transition:finish' : 'page:finish' return new Promise((resolve) => { - nuxtApp.hooks.hookOnce(hookAwait, async () => { + nuxtApp.hooks.hookOnce(hookToWait, async () => { await nextTick() - if (to.hash) { position = { el: to.hash, - top: getHashElementScrollMarginTop() + top: _getHashElementScrollMarginTop(to.hash) } } resolve(position) @@ -58,3 +50,11 @@ export default { }) } } + +function _getHashElementScrollMarginTop (selector: string): number { + const elem = document.querySelector(selector) + if (elem) { + return parseFloat(getComputedStyle(elem).scrollMarginTop) + } + return 0 +} From 9cceb19cb2b4221a7c5cff1d08df30c6a09a770d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:20:42 +0200 Subject: [PATCH 15/19] more refactors and move default options to runtime --- packages/nuxt/src/pages/module.ts | 2 +- .../router.options.ts} | 26 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) rename packages/nuxt/src/pages/{routing/default-router.options.ts => runtime/router.options.ts} (67%) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 6b3e3c3840f..c1ffdebf15b 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -140,7 +140,7 @@ export default defineNuxtModule({ filename: 'router.options.mjs', getContents: async () => { // Check for router options - const defaultOptsFile = resolve(distDir, 'pages/routing/default-router.options') + const defaultOptsFile = resolve(distDir, 'pages/runtime/router.options') const routerOptionsFiles = (await Promise.all(nuxt.options._layers.map( async layer => await findPath(resolve(layer.config.srcDir, 'app/router.options')) ))).filter(Boolean) as string[] diff --git a/packages/nuxt/src/pages/routing/default-router.options.ts b/packages/nuxt/src/pages/runtime/router.options.ts similarity index 67% rename from packages/nuxt/src/pages/routing/default-router.options.ts rename to packages/nuxt/src/pages/runtime/router.options.ts index e59f3542a0c..bf9fa08420f 100644 --- a/packages/nuxt/src/pages/routing/default-router.options.ts +++ b/packages/nuxt/src/pages/runtime/router.options.ts @@ -3,20 +3,20 @@ import type { RouterScrollBehavior } from 'vue-router' import { nextTick } from 'vue' import { useNuxtApp } from '#app' -type ScrollPosition = Exclude>, false|void> +type ScrollPosition = Awaited> +// Default router options // https://router.vuejs.org/api/#routeroptions -export default { +export default { 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 - let position: ScrollPosition + // savedPosition is only available for popstate navigations (back button) + let position: ScrollPosition = savedPosition || undefined - // SavedPosition is only available for popstate navigations (back button) - if (savedPosition) { - position = savedPosition - } else if (to !== from /* Route changed */) { + // Scroll to top if route is changed by default + if (!position && to !== from) { position = { left: 0, top: 0 } } @@ -26,24 +26,18 @@ export default { return { left: 0, top: 0 } } if (to.hash) { - return { - el: to.hash, - top: _getHashElementScrollMarginTop(to.hash) - } + return { el: to.hash, top: _getHashElementScrollMarginTop(to.hash) } } } - // Wait for page:transition:finish or page:finish depending on transitions enabled or not + // 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) - } + position = { el: to.hash, top: _getHashElementScrollMarginTop(to.hash) } } resolve(position) }) From a0aaf3f6e9c3206da604229b80703ca08bac9b48 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:25:46 +0200 Subject: [PATCH 16/19] update module logic --- packages/nuxt/src/pages/module.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index c1ffdebf15b..26f7fc447de 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -139,22 +139,21 @@ export default defineNuxtModule({ addTemplate({ filename: 'router.options.mjs', getContents: async () => { - // Check for router options - const defaultOptsFile = resolve(distDir, 'pages/runtime/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)])) 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(), From 9240ba856a85b194a66c7d44316c29856938dba8 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:29:42 +0200 Subject: [PATCH 17/19] restyle page.ts --- packages/nuxt/src/pages/runtime/page.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index f43b680ae43..22311d3f3fc 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -39,8 +39,10 @@ export default defineComponent({ return h(RouterView, { name: props.name, route: props.route, ...attrs }, { default: (routeProps: RouterViewSlotProps) => { if (!routeProps.Component) { return } + const key = generateRouteKey(props.pageKey, routeProps) const done = nuxtApp.deferHydration() + const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition) const transitionProps = hasTransition && _mergeTransitionProps([ props.transition, @@ -48,12 +50,11 @@ export default defineComponent({ 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: () => { - nextTick(() => 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 } as {}) }) )).default() } From 3ec9051b077e0ad8ae2ff8bb03bd5c3c428a65a9 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:35:57 +0200 Subject: [PATCH 18/19] fix: only scroll to top for non children --- packages/nuxt/src/pages/runtime/router.options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/pages/runtime/router.options.ts b/packages/nuxt/src/pages/runtime/router.options.ts index bf9fa08420f..7d2f6ed2876 100644 --- a/packages/nuxt/src/pages/runtime/router.options.ts +++ b/packages/nuxt/src/pages/runtime/router.options.ts @@ -16,7 +16,7 @@ export default { let position: ScrollPosition = savedPosition || undefined // Scroll to top if route is changed by default - if (!position && to !== from) { + if (!position && from.matched[0] !== to.matched[0]) { position = { left: 0, top: 0 } } From 023936fbb14316edc268affc78662b86c53f1542 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Oct 2022 14:39:52 +0200 Subject: [PATCH 19/19] support `scrollToTop` page meta --- packages/nuxt/src/pages/runtime/composables.ts | 2 ++ packages/nuxt/src/pages/runtime/router.options.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/pages/runtime/composables.ts b/packages/nuxt/src/pages/runtime/composables.ts index ca977dd23ed..6c517be0443 100644 --- a/packages/nuxt/src/pages/runtime/composables.ts +++ b/packages/nuxt/src/pages/runtime/composables.ts @@ -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' { diff --git a/packages/nuxt/src/pages/runtime/router.options.ts b/packages/nuxt/src/pages/runtime/router.options.ts index 7d2f6ed2876..1137b346dde 100644 --- a/packages/nuxt/src/pages/runtime/router.options.ts +++ b/packages/nuxt/src/pages/runtime/router.options.ts @@ -16,7 +16,11 @@ export default { let position: ScrollPosition = savedPosition || undefined // Scroll to top if route is changed by default - if (!position && from.matched[0] !== to.matched[0]) { + if ( + !position && + (from && to && from.matched[0] !== to.matched[0]) && + to.meta.scrollToTop !== false + ) { position = { left: 0, top: 0 } }