feat: useScrollRestoration and onRouteRendered (#2805) · vuejs/router@e3dde5e · GitHub
Skip to content

Commit e3dde5e

Browse files
authored
feat: useScrollRestoration and onRouteRendered (#2805)
* feat: wip experimental new scroll * feat: wip useScrollRestoration * chore: playground navigation api * refactor: wip navigator * feat: expose experimental scroll restoration APIs * refactor: merge scroll into scroll-restoration * test: cover experimental scroll restoration * feat: add onRouteRendered + ScrollRestoration plugin * chore: fix types * docs: scroll restoration * refactor: edge cases for onRouteRendered * refactor: minor simplification * chore: delete old file * chore: minor renames * fix(router): diagnose missing scroll restoration plugin * chore: reword
1 parent 287507b commit e3dde5e

30 files changed

Lines changed: 3786 additions & 12 deletions

‎packages/docs/.vitepress/config/en.ts‎

Lines changed: 5 additions & 1 deletion

‎packages/docs/.vitepress/config/index.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from 'vitepress'
2-
import { enConfig } from './en'
3-
import { sharedConfig } from './shared'
4-
import { zhConfig } from './zh'
2+
import { enConfig } from './en.ts'
3+
import { sharedConfig } from './shared.ts'
4+
import { zhConfig } from './zh.ts'
55

66
export default defineConfig({
77
...sharedConfig,

‎packages/docs/.vitepress/config/shared.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type { HeadConfig } from 'vitepress'
22
import { defineConfig } from 'vitepress'
3-
import { zhSearch } from './zh'
3+
import { zhSearch } from './zh.ts'
44
import {
55
groupIconMdPlugin,
66
groupIconVitePlugin,
77
} from 'vitepress-plugin-group-icons'
88
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'
99
import { ModuleResolutionKind } from 'typescript'
1010
// TODO: simplify and only importwm from '../twoslash/files'
11-
import { typedRouterFileAsModule } from '../twoslash-files'
12-
import { extraFiles } from '../twoslash/files'
11+
import { typedRouterFileAsModule } from '../twoslash-files.ts'
12+
import { extraFiles } from '../twoslash/files.ts'
1313
import llmstxt from 'vitepress-plugin-llms'
1414

1515
// TODO:

‎packages/docs/.vitepress/config/zh.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
2-
import typedocSidebar from '../../api/typedoc-sidebar.json'
2+
import typedocSidebar from '../../api/typedoc-sidebar.json' with { type: 'json' }
33
// TODO: rework the typedoc sidebar to include the /zh/ prefix
44

55
export const META_URL = 'https://router.vuejs.org'
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Scroll Restoration
2+
3+
::: warning Experimental
4+
This API is experimental and might have breaking changes.
5+
:::
6+
7+
Scroll restoration lets components save and restore their own scroll positions. It is the **upcoming replacement** for the `scrollBehavior` router option.
8+
9+
With it, you can:
10+
11+
- Save multiple positions for one page
12+
- Use custom capture and restore functions, for example with `scrollIntoView()`
13+
- Control how pages share scroll restoration rather than relying on history entries
14+
15+
[[toc]]
16+
17+
## Setup
18+
19+
Install `ScrollRestoration` before the router. Give it the router, and the default capture and restore functions:
20+
21+
```ts [main.ts]
22+
import { createApp } from 'vue'
23+
import {
24+
ScrollRestoration,
25+
SCROLL_RESTORATION_CAPTURE_DEFAULT,
26+
SCROLL_RESTORATION_RESTORE_DEFAULT,
27+
} from 'vue-router/experimental'
28+
import App from './App.vue'
29+
import { router } from './router'
30+
31+
const app = createApp(App)
32+
33+
app.use(ScrollRestoration, {
34+
router,
35+
capture: SCROLL_RESTORATION_CAPTURE_DEFAULT,
36+
restore: SCROLL_RESTORATION_RESTORE_DEFAULT,
37+
})
38+
app.use(router)
39+
40+
app.mount('#app')
41+
```
42+
43+
The default functions save and restore the window scroll position and imitate the current mechanism combined with `scrollBehavior()` and some smart defaults. They are automatically inherited by nested components and can be overridden anywhere. Call `useScrollRestoration()` in a component to use them.
44+
45+
Installing the plugin sets `history.scrollRestoration` to `manual`. Setting it to auto can with scroll restoration, especially in the context of _anchor links_. Set it back to `auto` if this is not an issue for you.
46+
47+
## Restore a page
48+
49+
Call `useScrollRestoration()` in any page component that needs to restore its scroll position:
50+
51+
```vue [pages/Articles.vue]
52+
<script setup lang="ts">
53+
import { useScrollRestoration } from 'vue-router/experimental'
54+
55+
useScrollRestoration()
56+
</script>
57+
```
58+
59+
The router _captures_ the position **when you leave the page** and _restores_ it after a navigation, when the component is mounted or updated using `onRouteRendered()`.
60+
61+
## Multiple positions
62+
63+
`capture()` returns an object. Each property is one saved position. Use `default` for the main position and add other names for other scroll containers:
64+
65+
```vue [pages/Docs.vue]
66+
<script setup lang="ts">
67+
import { useTemplateRef } from 'vue'
68+
import { useScrollRestoration } from 'vue-router/experimental'
69+
70+
const sidebar = useTemplateRef('sidebar')
71+
72+
useScrollRestoration({
73+
capture: () => ({
74+
default: { top: window.scrollY },
75+
sidebar: { top: sidebar.value?.scrollTop },
76+
}),
77+
restore: entry => {
78+
// scroll to top if no entry is found (new visit)
79+
window.scrollTo({ top: entry?.default?.top ?? 0 })
80+
sidebar.value!.scrollTop = entry?.sidebar?.top ?? 0
81+
},
82+
})
83+
</script>
84+
85+
<template>
86+
<aside ref="sidebar">...</aside>
87+
<main>...</main>
88+
</template>
89+
```
90+
91+
`capture()` and `restore()` must be synchronous.
92+
93+
## Custom restore
94+
95+
You can save an element selector and scroll to it with a saved offset:
96+
97+
```vue [pages/Products.vue]
98+
<script setup lang="ts">
99+
import { ref } from 'vue'
100+
import { useScrollRestoration } from 'vue-router/experimental'
101+
102+
const selectedId = ref<string>()
103+
104+
useScrollRestoration({
105+
capture: () =>
106+
selectedId.value
107+
? {
108+
default: {
109+
el: `#${CSS.escape(selectedId.value)}`,
110+
// here you can add an offset to scroll a bit above the element
111+
top: 100,
112+
},
113+
}
114+
: null,
115+
restore: entry => {
116+
if (!entry?.default?.el) return
117+
118+
const element = document.querySelector(entry.default.el)
119+
if (!element) return
120+
121+
window.scrollTo({
122+
top:
123+
element.getBoundingClientRect().top +
124+
window.scrollY -
125+
(entry.default.top ?? 0),
126+
})
127+
},
128+
})
129+
</script>
130+
```
131+
132+
Return `null` from `capture()` to remove the saved entry.
133+
134+
## Keys
135+
136+
By default, the key is `to.path + to.hash`. The same path and hash always use the same saved entry, including a new visit from a link. For example, if the users navigates to `/articles/42#comments`, scrolls down, and then navigates to `/articles/43`, navigating back **or** navigating directly again to `/articles/42#comments` will restore the previous position.
137+
138+
Use a different key when pages must share or split entries:
139+
140+
```ts
141+
// same position for all hashes on a page
142+
useScrollRestoration({
143+
key: to => to.path,
144+
})
145+
146+
// different positions for different queries
147+
useScrollRestoration({
148+
key: to => to.fullPath,
149+
})
150+
151+
// same position for different pages
152+
useScrollRestoration({
153+
key: 'products',
154+
})
155+
```
156+
157+
Components that are active at the same time must use different keys.
158+
159+
## Manual restore
160+
161+
Set `manual: true` when the content is not ready after navigation, like animations or virtualized list. Then call `scroll()` when the content is displayed:
162+
163+
```vue [pages/Feed.vue]
164+
<script setup lang="ts">
165+
import { nextTick, onMounted, ref } from 'vue'
166+
import { useScrollRestoration } from 'vue-router/experimental'
167+
168+
const posts = ref<Post[]>([])
169+
const { scroll } = useScrollRestoration({ manual: true })
170+
171+
onMounted(async () => {
172+
posts.value = await fetchPosts()
173+
await nextTick()
174+
scroll()
175+
})
176+
</script>
177+
```
178+
179+
The router still captures the position automatically.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7+
<title>Navigation API - instances in guards</title>
8+
<link rel="stylesheet" href="/global.css" />
9+
<style>
10+
.fade-enter-active,
11+
.fade-leave-active {
12+
transition: opacity 2s ease;
13+
}
14+
15+
.fade-enter-from,
16+
.fade-leave-active {
17+
opacity: 0;
18+
}
19+
</style>
20+
</head>
21+
22+
<body>
23+
<a href="/navigation-api/">&lt;&lt; Back to Navigation API</a>
24+
<hr />
25+
26+
<main id="app"></main>
27+
<script type="module" src="/navigation-api/guards-instances.ts"></script>
28+
</body>
29+
</html>
Lines changed: 64 additions & 0 deletions

0 commit comments

Comments
 (0)