【Vuetify】VDataTable を拡張して縦スクロール可能な YScrollDataTale コンポーネントを作成する

【Vuetify】VDataTable を拡張して縦スクロール可能な YScrollDataTale コンポーネントを作成する

ゴール

使用イメージ

<y-scroll-data-table
  :headers="headers"
  :items="items"
  :items-per-display="5"
>
  <template v-slot:item.color="{ item }">
    <v-icon :color="item.color" class="mr-3">mdi-circle</v-icon>
    {{ item.color }}
  </template>
</y-scroll-data-table>

作業の流れ

  1. 新しく作成したコンポーネント内で v-data-table を使用し、一部のプロパティの値を固定する
  2. 6行目以降をスクロール可能にする
  3. items-per-display プロパティを用意し、受け取った数値以降の行をスクロール可能にする

1. 新しく作成したコンポーネント内で v-data-table を使用し、一部のプロパティの値を固定する

下部のフッターを非表示にするためにhide-default-footer を true に、
アイテムを全てを表示するために items-per-page を items の数と同じにする必要があります。

v-data-table API

template タグを使用し下記のようにシンプルに記述したいところですが、slot が機能しません。

<!-- YScrollDataTable.vue -->

<template>
  <v-data-table
    v-bind="$attrs"
    :items-per-page="$attrs.items.length"
    hide-default-footer
    v-on="$listeners"
  />
</template>

template タグは削除し、以下のように描画関数を使用することで解決できます。
描画関数とJSX — Vue.js

<!-- YScrollDataTable.vue -->

<script>
export default {
  render(createElement) {
    return createElement('v-data-table', {
      props: {
        ...this.$attrs,
        itemsPerPage: this.$attrs.items.length,
        hideDefaultFooter: true
      },
      on: this.$listeners,
      scopedSlots: this.$scopedSlots
    })
  }
}
</script>

2. 6行目以降をスクロール可能にする

上記のように全てのアイテムを表示することができたため、次は5行分のみを表示し、6行目以降はスクロールする形に修正します。
デザインを調節するために、まずはクラス名を追加します。

<script>
export default {
  render(createElement) {
    return createElement('v-data-table', {

      // ↓追加↓
      class: {
        'yScrollDataTable': true,
        'yScrollDataTable--scrollable': this.$attrs.items.length > 5
      },
      // ↑追加↑

      props: {
        ...this.$attrs,
        itemsPerPage: this.$attrs.items.length,
        hideDefaultFooter: true
      },
      on: this.$listeners,
      scopedSlots: this.$scopedSlots
    })
  }
}
</script>

次にデザインを調節します。
scoped 付きの style タグ内で子コンポーネントのスタイルを調節するために deep selector を使用しています。

Deep Selectors | Vue Loader

<style lang="scss" scoped>
@import '~vuetify/src/styles/styles.sass';
@import '~vuetify/src/components/VDataTable/_variables.scss';

.yScrollDataTable {
  $row-height: $data-table-regular-row-height;

  ::v-deep {
    table, thead, tbody, th, td {
      display: block;
      width: 100% !important;
    }

    tr {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    th, td {
      display: flex;
      align-items: center;
      height: $row-height !important;
    }
  }

  &--scrollable ::v-deep tbody {
    overflow-y: scroll;
    max-height: $row-height * 5;

    // スクロールバー全体
    &::-webkit-scrollbar {
      width: 8px;
    }

    // スクロールバーの軌道
    &::-webkit-scrollbar-track {
      border-radius: 8px;
      box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
    }

    // スクロールバーの動く部分
    &::-webkit-scrollbar-thumb {
      background-color: map-get($grey, 'base');
      border-radius: 8px;
    }
  }
}
</style>

3. items-per-display プロパティを用意し、受け取った数値以降の行をスクロール可能にする

6行目以降をスクロールで表示する形に修正できたため、次はコンポーネントを使用する側が表示する行数を指定できるようにします。
まずは itemsPerDisplay プロパティを定義します。

props: {
  itemsPerDisplay: {
    type: Number,
    required: false,
    default: 5
  }
}

次に、用意した itemsPerDisplay プロパティを元に computed 内でCSS変数を作成します。
CSS カスタムプロパティ (変数) の使用 – CSS: カスケーディングスタイルシート | MDN

computed: {
  cssVars() {
    return {
      '--itemsPerDisplay': this.itemsPerDisplay
    }
  }
}

作成したCSS変数を、描画関数内で style にバインドします。
<v-data-table :style="cssVars"> と同じことをしています。

render(createElement) {
  return createElement('v-data-table', {
    class: {
      'yScrollDataTable': true,
      'yScrollDataTable--scrollable': this.$attrs.items.length > 5
    },

    // ↓↓追加↓↓
    style: this.cssVars,
    // ↑↑追加↑↑

    props: {
      ...this.$attrs,
      itemsPerPage: this.$attrs.items.length,
      hideDefaultFooter: true
    },
    on: this.$listeners,
    scopedSlots: this.$scopedSlots
  })
},

受け取ったCSS変数を利用して tbody に高さを与えます。

&--scrollable ::v-deep tbody {
  overflow-y: scroll;

  // From
  max-height: $row-height * 5;

  // To
  max-height: calc(#{$row-height} * var(--itemsPerDisplay));
}

最後にyScrollDataTable–scrollable クラスの設定条件を修正します。

class: {
  'yScrollDataTable': true,

  // From
  'yScrollDataTable--scrollable': this.$attrs.items.length > 5

  // To
  'yScrollDataTable--scrollable': this.$attrs.items.length > this.itemsPerDisplay
}

これで、以下のようにコンポーネントを使用する側が好きな行数を指定できるようになりました。

<y-scroll-data-table
  :headers="..."
  :items="..."
  :items-per-display="5"
>
  ...
</y-scroll-data-table>

以上で完了です。
最後にソースコード全体を記載します。

index.vue (コンポーネントを使用する側)

<template>
  <v-container>
    <y-scroll-data-table
      :headers="headers"
      :items="items"
      :items-per-display="5"
    >
      <template v-slot:item.color="{ item }">
        <v-icon :color="item.color" class="mr-3">mdi-circle</v-icon>
        {{ item.color }}
      </template>
    </y-scroll-data-table>
  </v-container>
</template>

<script>
import YScrollDataTable from '@/components/YScrollDataTable'

export default {
  components: {
    YScrollDataTable
  },
  data: () => ({
    headers: [{ text: "Colors", value: "color", align: "start" }],
    items: [
      { color: "red" },
      { color: "pink" },
      { color: "purple" },
      { color: "deep-purple" },
      { color: "indigo" },
      { color: "blue" },
      { color: "light-blue" },
      { color: "cyan" },
      { color: "teal" },
      { color: "green" },
    ]
  })
}
</script>

YScrollDataTable.vue (この記事で作成したコンポーネント)

<script>
export default {
  props: {
    itemsPerDisplay: {
      type: Number,
      required: false,
      default: 5
    }
  },
  render(createElement) {
    return createElement('v-data-table', {
      class: {
        'yScrollDataTable': true,
        'yScrollDataTable--scrollable': this.$attrs.items.length > this.itemsPerDisplay
      },
      style: this.cssVars,
      props: {
        ...this.$attrs,
        itemsPerPage: this.$attrs.items.length,
        hideDefaultFooter: true
      },
      on: this.$listeners,
      scopedSlots: this.$scopedSlots
    })
  },
  computed: {
    cssVars() {
      return {
        '--itemsPerDisplay': this.itemsPerDisplay
      }
    }
  }
}
</script>

<style lang="scss" scoped>
@import '~vuetify/src/styles/styles.sass';
@import '~vuetify/src/components/VDataTable/_variables.scss';

.yScrollDataTable {
  $this: &;
  $row-height: $data-table-regular-row-height;

  ::v-deep {
    table, thead, tbody, th, td {
      display: block;
      width: 100% !important;
    }

    tr {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    th, td {
      display: flex;
      align-items: center;
      height: $row-height !important;
    }
  }

  &--scrollable ::v-deep tbody {
    overflow-y: scroll;
    max-height: calc(#{$row-height} * var(--itemsPerDisplay));

    // スクロールバー全体
    &::-webkit-scrollbar {
      width: 8px;
    }

    // スクロールバーの軌道
    &::-webkit-scrollbar-track {
      border-radius: 8px;
      box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
    }

    // スクロールバーの動く部分
    &::-webkit-scrollbar-thumb {
      background-color: map-get($grey, 'base');
      border-radius: 8px;
    }
  }
}
</style>

ありがとうございました。