For those utilizing the Vue Router, there is a way to create a composable that can be connected to your element in order to maintain the scrollbar position. In this demonstration, a composable is used in a base wrapper component and in a more intricate scenario with multiple scrollbars that need to be persisted.
scrollPosition.ts
import { onActivated, ref, Ref, unref } from "vue";
import { onBeforeRouteLeave } from "vue-router";
export function useSaveScrollPosition(el: Ref<HTMLElement>) {
const scrollTop = ref(0);
const scrollLeft = ref(0);
onActivated(() => {
const $el = unref(el);
if ($el) {
$el.scrollTop = scrollTop.value;
$el.scrollLeft = scrollLeft.value;
}
});
onBeforeRouteLeave(() => {
const $el = unref(el);
if ($el) {
scrollTop.value = $el.scrollTop;
scrollLeft.value = $el.scrollLeft;
}
});
}
RouteWrapper.vue
<template>
<article ref="componentEl">
<slot />
</article>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useSaveScrollPosition } from "../scrollPosition";
const componentEl = ref<HTMLElement>();
useSaveScrollPosition(componentEl);
</script>
ViewHome.vue
<template>
<RouteWrapper style="height: 100px; overflow: auto">
<LongContent :total-lines="50" />
</RouteWrapper>
</template>
<script setup lang="ts">
import RouteWrapper from "./RouteWrapper.vue";
import LongContent from "./LongContent.vue";
</script>
ViewThing.vue
<template>
<article style="height: 200px; display: flex">
<button>A button</button>
<section ref="elementOne" style="overflow: auto; flex: 1 1 0%">
<LongContent :total-lines="200" />
</section>
<section ref="elementTwo" style="overflow: auto; width: 200px">
<LongContent :total-lines="100" content-style="width: 400px" />
</section>
</article>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useSaveScrollPosition } from "../scrollPosition";
import LongContent from "./LongContent.vue";
const elementOne = ref();
const elementTwo = ref();
useSaveScrollPosition(elementOne);
useSaveScrollPosition(elementTwo);
</script>
router.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import ViewHome from '@/components/ViewHome.vue';
import ViewThing from '@/components/ViewThing.vue';
export const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/home',
name: 'home',
component: ViewHome,
},
{
path: '/thing',
name: 'thing',
component: ViewThing,
},
],
});
App.vue
<template>
<main style="display: flex; flex-direction: column">
<nav style="margin-bottom: 1rem">
<a href="#/home" style="margin-right: 1rem">Home</a>
<a href="#/thing">Thing</a>
</nav>
<RouterView v-slot="{ Component }">
<KeepAlive>
<Component :is="Component" />
</KeepAlive>
</RouterView>
</main>
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
This approach allows for versatility in managing main components as well as individual elements, though it is specifically designed for use with the router due to the necessity of the onBeforeRouteLeave
hook (credits). Implementing onDeactivated
posed challenges as the element was inaccessible and scrollTop defaulted to 0.
While the desired directive functionality was considered, current limitations prevent access to activated/deactivated hooks (source). Additionally, it's important to note that the Vue Router's scrollBehavior hook primarily impacts navigation through browser back/forward actions, which may not align with all application scenarios.