🧷

Vuetify3 VDataTableで複数列を固定する方法

に公開

Vuetify3にあるVDataTableの列をExcelの列固定機能のように複数の列を固定しようとした際、手間取ったので備忘録としてまとめます。

やりたかったこと

解決策

  • headers のパラメータのうち、固定したい列に対して以下の設定を行う
    • fixed の値を trueに設定する
    • width の値を設定する

何に躓いたのか

当初、VDataTableドキュメントを見て「 fixedパラメータを適用することで固定化できるな」と理解していた。
実際適用してみると、1列目だけ固定したい場合はうまくいったが、左から2列分を固定しようとすると、2列目が1列目を覆い隠してしまうという動作となってしまった

設定 期待する動作 実際の動作
fixed: true(1列目のみ) 1列目が固定される ✅ 期待通り
fixed: true(1列目と2列目) 1, 2列目が固定される ❌ 2列目が1列目を覆う
  • やりたかった動作
  • 実際の動作
つまずいた際のコード
<script lang="ts" setup>
import { ref } from "vue";

const headers = ref([
  { title: "Name", key: "name", fixed: true },
  { title: "Manufacturer", key: "manufacturer", fixed: true },
  { title: "Year", key: "year" },
  { title: "Sales", key: "sales" },
  { title: "Exclusive", key: "exclusive" },
]);
const consoles = ref([
  {
    name: "PlayStation 5",
    manufacturer: "Sony",
    year: 2020,
    sales: "10M",
    exclusive: true,
  },
  {
    name: "Xbox Series X",
    manufacturer: "Microsoft",
    year: 2020,
    sales: "6.5M",
  },
  {
    name: "Nintendo Switch",
    manufacturer: "Nintendo",
    year: 2017,
    sales: "89M",
    exclusive: true,
  },
  {
    name: "PlayStation 4",
    manufacturer: "Sony",
    year: 2013,
    sales: "116M",
    exclusive: true,
  },
  {
    name: "Xbox One",
    manufacturer: "Microsoft",
    year: 2013,
    sales: "50M",
    exclusive: false,
  },
  {
    name: "Nintendo Wii",
    manufacturer: "Nintendo",
    year: 2006,
    sales: "101M",
    exclusive: true,
  },
]);
</script>

<template>
  <v-data-table :headers="headers" :items="consoles" hide-default-footer>
    <template #item.exclusive="{ item }">
      <v-checkbox-btn v-model="item.exclusive" :ripple="false" />
    </template>
  </v-data-table>
</template>

どのように解決したか

VDataTableHeaderの実装を確認した

こちらを確認すると fixed: true の場合には以下のCSSが指定されていることがわかる

function getFixedStyles (column: InternalDataTableHeader, y: number): CSSProperties | undefined {
  if (!(props.sticky || props.fixedHeader) && !column.fixed) return undefined

  return {
    position: 'sticky',
    left: column.fixed ? convertToUnit(column.fixedOffset) : undefined,
    top: (props.sticky || props.fixedHeader) ? `calc(var(--v-table-header-height) * ${y})` : undefined,
  }
}

ここから、以下のことがわかった

  • position: 'sticky'が指定されているため、一番右の列はスクロールにて端に移動しても常に表示される
  • 2列目以降を固定するために、 column.fixedOffsetの値を使用している

現状の動作を見ると、sticky は意図した通りに動作しているように見えるが、 left の値が適切に設定されていないようにみえる

では fixedOffsetはどのように設定されているのか

column の値は useHeadersから返されていたため、そちらのコードを確認した

https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VDataTable/composables/headers.ts#L140

  function setFixedOffset (item: InternalDataTableHeader, fixedOffset = 0) {
    if (!item) return fixedOffset

    if (item.children) {
      item.fixedOffset = fixedOffset
      for (const child of item.children) {
        fixedOffset = setFixedOffset(child, fixedOffset)
      }
    } else if (item.fixed) {
      item.fixedOffset = fixedOffset
      fixedOffset += parseFloat(item.width || '0') || 0
    }

    return fixedOffset
  }

  let fixedOffset = 0
  for (const item of items) {
    fixedOffset = setFixedOffset(item, fixedOffset)
  }
}

確認すると、上記のコードにて指定されていることがわかった。

こちらでは、左から順に width を足していき、fixedOffsetとしていた

そのため、各列に widthの値が必要であることがわかる

fixedとwidthを指定して解決

fixed:true を指定した列には widthを指定することで、2列目以降もExcelのように列を固定することができた

最後に

  • 今回はVuetifyの機能でVDataTableで複数の列を固定化する方法を調査した
  • 本来であればCSSの仕様からleftを指定しないと実現できないよねということを解説したかったが、うまく説明できなかったので今回は割愛

Discussion