Help us understand the problem. What is going on with this article?

Vuetify 2.0 で datatable を使う

Vuetify 2.0 で datatable を使って一覧画面を作る

Vuetify 2.0の v-datatable を用いて以下の方針で一覧画面を作ってみます。

  • 並べ変えに対応させる (第2ソート、第3ソートにも対応)
  • 絞り込みに対応させる
  • 極力日本語化
  • ページ切り替えの度にAjaxでレコードを取得

画面イメージ

PCで見た場合

PC.png

スマホで見た場合 (表形式ではなく縦長のレイアウトになる!)

SP.png

まずは全体のソース

<template>
  <div id="form">
    <v-container fluid>
      <v-row justify="center">
        <v-col>
          <v-card>
            <v-app-bar color="primary" dark>
              <v-toolbar-title><v-icon>list_alt</v-icon> ○○一覧</v-toolbar-title>
              <v-tooltip bottom>
                <template v-slot:activator="{ on }">
                  <v-btn color="pink" dark small absolute bottom right fab to="/create" v-on="on"><v-icon>add</v-icon></v-btn>
                </template>
                <span>新しい○○を作成する</span>
              </v-tooltip>
            </v-app-bar>
            <v-form v-model="valid" ref="listForm" lazy-validation>
              <v-container fluid class="pa-1">
                <v-row class="px-2">
                  <v-col cols="6" class="pa-1">
                    <v-text-field
                      prepend-inner-icon="search"
                      clearable
                      label="タイトル"
                      name="title"
                      maxlength="64"
                      v-model="model.title"
                      @change="loadList"
                    ></v-text-field>
                  </v-col>
                  <v-col cols="3" class="pa-1">
                    <v-select
                      label="種別"
                      name="type"
                      item-text="label"
                      item-value="value"
                      :items="[
                        { label: '-', value: null },
                        { label: '銀行', value: 0 },
                        { label: '郵便局', value: 1 },
                      ]"
                      v-model="model.type"
                      @change="loadList"
                    ></v-select>
                  </v-col>
                  <v-col cols="3" class="pa-1">
                    <v-select
                      label="公開"
                      name="is_open"
                      item-text="label"
                      item-value="value"
                      :items="[
                        { label: '-', value: null },
                        { label: '公開中', value: true },
                        { label: '未公開', value: false },
                      ]"
                      v-model="model.is_open"
                      @change="loadList"
                    ></v-select>
                  </v-col>
                </v-row>
              </v-container>
              <v-data-table
                :headers="headers"
                :items="items"
                :options.sync="options"
                :server-items-length="total"
                :footer-props="{
                  'items-per-page-options': [10, 20, 50, 100, 200, 300, 400, 500],
                  showFirstLastPage: true,
                }"
                :loading="loading"
                multi-sort
                locale="ja-jp"
                loading-text="読込中"
                no-data-text="データがありません。"
                class="elevation-1"
              >
                <template v-slot:item.uri="{ item }">
                  <a
                    :href="'https://hoge.jp/' + item.uri"
                    target="_blank"
                    >hoge.jp/{{ item.uri }}</a
                  >
                </template>
                <template v-slot:item.type="{ item }">
                  {{ selectionItems.type[item.type] }}
                </template>
                <template v-slot:item.is_open="{ item }">
                  <v-icon v-if="item.is_open">check</v-icon>
                </template>
                <template v-slot:item.created_at="{ item }">
                  {{ item.created_at.replace('T', ' ').replace(/-/g, '/') }}
                </template>
                <template v-slot:item.updated_at="{ item }">
                  {{ item.updated_at.replace('T', ' ').replace(/-/g, '/') }}
                </template>
                <template v-slot:item.code="{ item }">
                  {{ item.name }}
                </template>
                <template v-slot:item.action="{ item }">
                  <v-btn small class="mx-1" color="orange accent-4" :to="'/detail/' + item.uri">
                    <v-icon>pageview</v-icon>詳細
                  </v-btn>
                  <v-btn small class="mx-1" color="orange accent-4" :to="'/stats/' + item.uri">
                    <v-icon>bar_chart</v-icon>集計
                  </v-btn>
                </template>
              </v-data-table>
            </v-form>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

<script>
import Axios from 'axios'
export default {
  data: () => ({
    loading: false,
    headers: [
      { text: 'ID', align: 'center', sortable: true, value: 'form_id' },
      { text: 'タイトル', align: 'center', sortable: true, value: 'title' },
      { text: 'URL', align: 'center', sortable: false, value: 'uri' },
      { text: '種別', align: 'center', sortable: false, value: 'form_type' },
      { text: '公開', align: 'center', sortable: false, value: 'is_open' },
      { text: '作成日時', align: 'center', sortable: true, value: 'created_at' },
      { text: '更新日時', align: 'center', sortable: true, value: 'updated_at' },
      { text: '組織', align: 'center', sortable: false, value: 'code' },
      { text: '操作', align: 'center', sortable: false, value: 'action' }
    ],
    options: {
      page: 1,
      itemsPerPage: 20,
      sortBy: ['form_id'],
      sortDesc: [true],
    },
    items: [],
    total: 0,
    selectionItems: {
      form_type: ['銀行', '郵便局'],
    },
    model: {
      'title': '',
      type: null,
      is_open: null,
    },
  }),
  watch: {
    options: {
      handler() {
        this.loadList()
      },
      deep: true,
    },
  },
  methods: {
    async loadList() {
      this.loading = true
      try {
        let sorts = []
        if (this.options.sortBy !== null) {
          this.options.sortBy.forEach((value, index) => {
            sorts.push((this.options.sortDesc[index] ? '-' : '+') + value)
          })
        }
        const res = await Axios.post(
          '/api/list',
          Object.assign(this.model, {
            offset: (this.options.page - 1) * this.options.itemsPerPage,
            limit: this.options.itemsPerPage,
            sort: sorts.join(' ')
          })
        )
        if (res.data) {
          this.items = res.data.items
          this.total = res.data.total
        }
      } catch (error) {
        alert('情報を取得できませんでした。時間をおいてやり直してください。')
      }
      this.loading = false
    },
  },
  created: function() {
    this.loadList()
  },
}
</script>

Vuetify 1.5との違い

結構色々変わってて苦戦しました。

  • v-datatable の項目名が色々変わっている
  • tdタグを書いていく方式から、各カラムの template タグを実装していく方式に変わった
  • ソートカラムが文字列ではなく配列で返ってくる (multi-sortでない場合でも)

Ajaxでのデータ取得

datatableは、

  • 全レコードを取得して並べ替えや絞り込みをJS内で行う方式
  • ページ切り替えやソート方法変更の度に該当ページのレコードを取得する方式

があります。レコードが数万レコードレベルで大量に存在する場合は前者の方法は使えません。
そこで後者の方式で実装します。

<v-data-table
  :headers="headers"
  :items="items"
  :server-items-length="total"
>

:server-item-length でレコードの総数を渡してあげると後者のモードになる模様。あとは、

const res = await Axios.post(
  '/api/list',
  {
    offset: (this.options.page - 1) * this.options.itemsPerPage,
    limit: this.options.itemsPerPage
  }
)
if (res.data) {
  this.items = res.data.items
  this.total = res.data.total
}

で、現在のページ番号に応じたレコードを取得して変数に格納してあげるだけでいけました。

ソート

<v-data-table
  multi-sort
>

第二ソート以上に対応させる場合は、multi-sortを指定し、対応させない場合は multi-sort を外して、必要に応じて must-sort を入れます。
multi-sortmust-sort を共存させてしまうと、ソートが外せなくなり、第一ソートと第二ソートの入れ替えなどができなくなってしまいます。

あとは、options.sortByにソート方法の配列 (例えば、['title', 'id'])、options.sortDescに昇順・降順の配列 (例えば、[true, false])が格納されるので、APIにあった方法に変換してAjaxで渡します。
今回は、 +title -id といった形式で渡すようにしてみました。

let sorts = []
if (this.options.sortBy !== null) {
  this.options.sortBy.forEach((value, index) => {
    sorts.push((this.options.sortDesc[index] ? '-' : '+') + value)
  })
}
sorts = sorts.join(' ')

テーブルの描画

Ajaxから取得したデータをそのまま出力する場合は問題無いのですが、リンクやボタンをつけたり、複数の情報を組み合わせてデータを出力する場合などは、セルの描画内容をカスタマイズします。

<!-- リンクを出力する例 -->
<template v-slot:item.uri="{ item }">
  <a
    :href="'https://hoge.jp/' + item.uri"
    target="_blank"
    >hoge.jp/{{ item.uri }}</a
  >
</template>
<!-- カラム値を文字列に変換して出力する例 -->
<template v-slot:item.type="{ item }">
  {{ selectionItems.type[item.type] }}
</template>
<!-- チェックマークをつける例 -->
<template v-slot:item.is_open="{ item }">
  <v-icon v-if="item.is_open">check</v-icon>
</template>
<!-- ボタンを設置する例 -->
<template v-slot:item.action="{ item }">
  <v-btn small class="mx-1" color="orange accent-4" :to="'/detail/' + item.uri">
    <v-icon>pageview</v-icon>詳細
  </v-btn>
</template>

絞り込み

各絞り込みフィールドに @change 属性を追加し、発火したらAjaxでレコードを取得し直せば問題無いでしょう。

日本語化

Vuetifyの言語設定を ja に変えれば日本語になってくれました。 Vuetify を初期化している処理で、

import ja from 'vuetify/es5/locale/ja.js'
new Vuetify({
  lang: {
    locales: {ja},
    current: 'ja'
  }
})

とすることで概ね日本語化されました。ただ、それでも日本語化されない箇所があったので別途 datatable側でも直に日本語設定を入れました。

<v-data-table
  loading-text="読込中"
  no-data-text="データがありません。"
>
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away