表格在分配自定义滚动位置后不断跳回顶部。

huangapple go评论60阅读模式
英文:

Table keeps jumping back to top after assigning custom scroll position

问题

I'm using Vue 3 with a Vuetify table and whenever I modify data I have to fetch everything again. If you modify a cell inside row 123 and column 456 reconstructing the whole grid is annoying because the table scrollbars jump back to the start. I think a good solution for this would be to

  • store the current scroll position
  • perform the write action
  • reassign the stored scroll position

( any better suggestions are highly appreciated )

As a sidenote: Since Vuetify requires a fixed table height for fixed table headers I'm, calculating the height dynamically ( table should fill the rest of the page ).

I created the following example ( Playground link )

英文:

I'm using Vue 3 with a Vuetify table and whenever I modify data I have to fetch everything again. If you modify a cell inside row 123 and column 456 reconstructing the whole grid is annoying because the table scrollbars jump back to the start. I think a good solution for this would be to

  • store the current scroll position
  • perform the write action
  • reassign the stored scroll position

( any better suggestions are highly appreciated )

As a sidenote: Since Vuetify requires a fixed table height for fixed table headers I'm, calculating the height dynamically ( table should fill the rest of the page ).

I created the following example ( Playground link )

<script setup lang="ts">
import { ref, nextTick, onMounted, watch } from "vue";  

const mainContainerComponent = ref<VMain>();
const tableComponent = ref<VTable>();
const tableHeight: Ref<number | undefined> = ref(undefined);
const tableMatrix = ref([[]]);

onMounted(async () => {
  // we only want to scroll inside the table
  document.documentElement.classList.add("overflow-y-hidden");

  await loadData();
});

watch(tableMatrix, async () => {
  // Unset table height and wait for it to rerender
  tableHeight.value = undefined;

  await nextTick();

  if (!tableComponent.value) {
    return;
  }

  const mainContainerComponentRectangle = mainContainerComponent.value.$el.getBoundingClientRect();
  const tableRectangle = tableComponent.value.$el.getBoundingClientRect();
  const topOffset = tableRectangle.top;
  const bottomOffset = mainContainerComponentRectangle.bottom - tableRectangle.bottom;

  tableHeight.value = window.innerHeight - bottomOffset - topOffset;
});

async function loadData() {
  // destroy table
  tableMatrix.value = [];

  await nextTick();
  
  // fetch data
  const fetchedData = new Array(Math.floor(Math.random() * 300) + 50).fill("data");

  // calculate table matrix
  tableMatrix.value = [...fetchedData.map(x => [x])];
}

async function performWriteAction() {
  // send modify request here

  const { scrollLeft, scrollTop } = getTableScrollPosition();

  await loadData();

  // wait for the DOM to finish
  await nextTick();

  // try to restore the previous scroll position
  setTableScrollPosition(scrollLeft, scrollTop);
}

function getTableDOMElement() {
  return tableComponent.value?.$el.querySelector(".v-table__wrapper");
}

function getTableScrollPosition() {
  const { scrollLeft, scrollTop } = getTableDOMElement();
  
  console.log(`current scroll position => x: ${scrollLeft} | y: ${scrollTop}`);

  return { scrollLeft, scrollTop };
}

function setTableScrollPosition(scrollLeft: number, scrollTop: number) {
  const tableElement = getTableDOMElement();
  
  console.log(`scroll to => x: ${scrollLeft} | y: ${scrollTop}`);

  tableElement.scrollLeft = scrollLeft;
  tableElement.scrollTop = scrollTop;
}
</script>

<template>
  <v-app>
    <v-main ref="mainContainerComponent">
      <v-container>
		<v-btn @click="performWriteAction">Modify data</v-btn>
      </v-container>
      
      <v-table
        ref="tableComponent"
        density="compact"
        fixed-header
        :height="tableHeight"
      >
        <thead>
          <tr>
            <th>Col</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, rowIndex) in tableMatrix" :key="rowIndex">
            <template v-for="(cell, columnIndex) in row" :key="columnIndex">
              <td>{{ rowIndex }}</td>
            </template>
          </tr>
        </tbody>
      </v-table>
    </v-main>
  </v-app>
</template>

The problem with this code is that the table always jumps back to the start. If you scroll down to the center of the table and modify some data the scroll position is still wrong.

Do you have any ideas what's wrong or missing?

答案1

得分: 2

Seems like it takes another rendering cycle, not sure why. Adding another nextTick() fixes the scroll.

Here is the updated playground

I think I found why it happens, first, let's look at the order of events (this is without a second nextTick), when inserting a number of log statements in the [playground](https://play.vuetifyjs.com/#eNqVV+1u2zYUfRVOKBB7laV23TDAddxmafcBNOiQBtuPKGhlibK1UqQqUXYMz+/eQ1KyKEtuUgSIpcvLc7/OvSJvd85FnnvrijpTZ1ZGRZpLUlJZ5YSFfHkeOLIMnHnA0ywXhSQ7UtDEJZzey5s0+uwSwa9ExSWNXbIJZbQie5IUIiOBA8zAeUlIwAMeCV5KkoUpvxRc4ocWlwKInHJJzhXm7J8riOej8ctGW4YLRntaN0raU/uTpsuVnJJrqPAqW9CC/E8qHtMEpuK52Tw6

英文:

Seems like it takes another rendering cycle, not sure why. Adding another nextTick() fixes the scroll:

  await nextTick();
  await nextTick();
  setTableScrollPosition(scrollLeft, scrollTop);

Here is the updated playground


I think I found why it happens, first, let's look at the order of events (this is without a second nextTick), when inserting a number of log statements in the playground, I get:

function action
performWriteAction awaiting loadData()
loadData clearing tableMatrix (triggers first watch), waiting
watch 1 unset tableHeight, waiting
render table is empty, no height
loadData continue, filling tableMatrix (triggers second watch)
watch 1 continuing, updating tableHeight (for empty table)
watch 2 unset tableHeight, waiting
render table has data, no height
performWriteAction waiting for update
watch 2 continuing, updating tableHeight (for filled table)
performWriteAction continue, setting scroll !!! starts before render !!!
render table has data, height is set
setTableScrollPosition scroll back

The important lines are the ones after the penultimate render:

function action
render table has data, no height
performWriteAction waiting for update
watch 2 continuing, updating tableHeight (for filled table)
performWriteAction continue, setting scroll !!! starts before render !!!

There you can see that performWriteAction() calls await nextTick() immediately after a render, when there are no pending state updates.

Looking at the implementation of nextTick(), we can see that if there are no pending updates (in the currentFlushPromise), a stored Promise.resolve() is used:

const resolvedPromise = Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

So basically, since there is nothing to wait for, nextTick() resolves immediately. Only the await forces to wait until the next computation cycle, allowing the watcher is run in between. But any changes made by the watcher do not affect the preceding nextTick(). You might as well write await Promise.resolve() directly.


Interestingly, it works when you pass a callback to nextTick():

async function performWriteAction() {
  const { scrollLeft, scrollTop } = getTableScrollPosition();
  await loadData();
  await nextTick(() => console.log('plz wait'));
  setTableScrollPosition(scrollLeft, scrollTop);
}

Note that the callback still runs before the render, but looking at the implementation above, it adds another .then(), which again forces to wait for another computation cycle, which is enough to get the render update in between (it is the same effect as the two await statements in my initial answer).


With all this, I am going to say that there is no real bug in your code, except that nextTick() behaves differently than expected (and maybe that it is not clear anymore what waits for what).

The clean solution would be to only run nextTick() when there are actually pending changes. You could just reset the tableHeight before waiting:

  await loadData();
  tableHeight.value = null
  await nextTick();
  setTableScrollPosition(scrollLeft, scrollTop);
}

Or, in general, it looks like you are using nextTick a lot because you want to do CSS changes with JS. If you instead find a way to set table height through CSS (which should be possible), you would get rid of all the problems and a lot of fragile code, where parallel tasks have to wait for each other.

The most feasible solution though is probably to eliminate the watcher. You know when tableHeight has to be updated, and that is when the tableMatrix data changes. So you could just set it along the call to the load method:

async function performWriteAction() {
  const { scrollLeft, scrollTop } = getTableScrollPosition();
  tableHeight.value = null;
  await loadData();
  updateTableHeight();
  setTableScrollPosition(scrollLeft, scrollTop);
}

Since it is particularly the clearing and then setting of tableMatrix and tableHeight, this would eliminate the issue and make your code much more clear and maintainable.

But that approach would not have led to your interesting question, which I hope this answers conclusively.

答案2

得分: 1

你可以通过添加几行CSS代码来取消固定表格高度设置。这样,你不仅可以解决滚动跳跃问题,还可以清理你的代码。

CSS 代码:

main {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.v-container {
  flex: none;
}
.v-table {
  flex: auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
英文:

You can opt out of the rigid table height set by adding a few lines of css. This way, you will not only get rid of the scroll jump problem, but also clean up your code.

Vuetify Play


CSS code:

main {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.v-container {
  flex: none;
}
.v-table {
  flex: auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

huangapple
  • 本文由 发表于 2023年5月22日 17:47:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76304921.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定