ブログ構築 7. タグ機能の構築 #04
2019年11月01日に公開

Vuetify2のdata-tableの使い方を学んで、タグ一覧ページをレイアウト

今回達成すること

タグ一覧ページにVuetify2のdata-tableを導入して、レイアウトを完成させます。

下の3つの機能をdata-tableで実現していきます。

  • デフォルトで投稿数の多い順に並べてタグを表示する
  • タグ名で検索できる
  • ページべネーションでタグ一覧を切り分ける

最終的に↓このようになります。

タグ一覧ページの完成イメージ

最終的なtags/index.vueのコードまですっ飛ばす

2019-11-01 18-15-47

Contentfulでタグ作成を行う

何がなくとも、基となるタグを作成しましょう。

Countentfulで、投稿に関連付くタグを20個くらい作成してください。

Countentfulの操作を忘れた方は、以下の記事にタグの作成方法を書いています。

目次)タグを作ってみよう からご覧ください。

Contentfulにタグモデルを作成し関連付けを行う

トップページのレイアウト崩れを修正する

ある程度タグが関連付くと、トップページの記事カードのレイアウトが崩れます。

これはv-card-textに設定したheightが原因です。

2019-11-01 18-07-06

pages/index.vueを修正しておきましょう。

pages/index.vue
<template>
 <v-container fluid>
   ...
   <v-list-item three-line style="min-height: unset;">
     <v-list-item-subtitle>
       {{ post.fields.body }}
     </v-list-item-subtitle>
   </v-list-item>

   <!-- 削除する -->
   <!-- <v-card-text
     style="height: 64px;"
   > -->

   <v-card-text>		<!-- 書き換え -->

     <template v-if="post.fields.tags">
       <v-chip
         v-for="(tag) in post.fields.tags"
         :key="tag.sys.id"
         :to="linkTo('tags', tag)"
         small
         label
         outlined
         class="ma-1"
       >
   ...

それではタグ一覧ページの編集を開始します。

【step1】Vuetify2のdata-tableを表示する

最初はdata-tableを表示して、骨組みを作ります。

Vuetify2のdata-tableのサンプル「search」を基本に作成します。

Data table components - Vuetify

pages/tags/index.vueを以下のように編集します。

pages/tags/index.vue
<template>
  <div>
    <breadcrumbs :add-items="addBreads" />

    <v-container>
      <v-row
        justify="center"
      >
        <v-col
          cols="12"
          sm="10"
          md="8"
        >
          <v-card>
            <v-card-title>
              <v-text-field
                v-model="search"
                append-icon="mdi-magnify"
                label="Search"
                single-line
                hide-details
              />
            </v-card-title>

            <v-data-table
              :headers="headers"
              :items="tableItems"
              :search="search"
            ></v-data-table>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    headers: [
      { text: 'タグ' },
      { text: '投稿数' }
    ]
  }),
  computed: {
    ...mapState(['tags']),
    ...mapGetters(['linkTo']),
    // 削除する
    // postCount() {
    //   return (currentTag) => {
    //     return this.$store.getters.associatePosts(currentTag).length
    //   }
    // },
    addBreads() {
      return [{ icon: 'mdi-tag-outline', text: 'タグ一覧', to: '/tags', disabled: true, iconColor: 'grey' }]
    },
    tableItems() {
      return []
    }
  }
}
</script>

"http://localhost:3000/tags"にアクセスすると。。。

data-tableの形ができました!

2019-10-31 23-34-08

【step2】タグアイテムを作成する

次は、表示するタグアイテムを作成します。

え?そのままthis.tagsを使えばええやんって思いました?

筆者も最初はそう思って実装しましたが、、、失敗しました。

そう、投稿数の多い順に並び替えができないのです。

この機能を実現するには、this.tagsと同じ配列内に、投稿数のデータを入れ込まなければなりません。

算出プロパティで実装しましょう。

pages/tags/index.vue
export default {
  ...
  computed: {
    ...
    tableItems() {
      const tags = []
      for (let i = 0; i < this.tags.length; i++) {
        const tag = this.tags[i]

        tag.fields.postcount = this.$store.getters.associatePosts(tag).length
        tags.push(tag)
      }
      return tags
    }
  }
}
  • tag.fields.postcount = this.$store.getters.associatePosts(tag).length

    tag.fieldsに新しいプロパティpostcountを追加しています。

    associatePostsは、引数に渡したタグの関連する記事を返します。

associatePostsは、こちらの記事で作成しています。

Contentfulのincludesを使って関連モデルを取得しタグ一覧ページを作成する

console.logで覗くとpostcountが追加されていますね。

2019-10-31 23-48-32

それではブラウザを確認してみま…。

おやおや、まだタグは表示されていません。

【step3】data-tableのheadersを追加する

タグを表示するには、headersの配列にvalueのプロパティを追加しなければなりません。

valueには、表示したいオブジェクトのkeyを指定ます。

pages/tags/index.vue
export default {
  data: () => ({
    search: '',
    headers: [
      {
        text: 'タグ',
        align: 'left',
        value: 'fields.name'
      },
      {
        text: '投稿数',
        align: 'center',
        width: 150,
        value: 'fields.postcount'
      }
    ]
  }),
  ...
}
  • align:

    table内の表示位置を指定できます。

    指定できる値は「left」「center」「right」の3つです。

  • width:

    tableセルの幅を指定できます。

    数字の場合、ピクセル値を渡します。

    文字列でwidth: '150px'と指定することもできます。

  • value:

    表示したいオブジェクトのkeyを文字列で指定します。

おおっ!タグが表示されました。

2019-11-01 00-11-56

【step4】タグの名前にリンクをつける

続いてタグの名前にリンクをつけます。

Vuetify2のdata-tableは非常に柔軟な拡張性を持っています。

タグの名前にリンクをつけるには、v-data-tableのタグの中でslotを定義します。

pages/tags/index.vue
<template>
	...
    <v-data-table
      :headers="headers"
      :items="tableItems"
      :search="search"
    >
      
      <!-- v-data-tableのタグ内に追記 -->
      <template v-slot:item.fields.name="{ item }">
        <v-icon size="18">
          mdi-tag-outline
        </v-icon>

        <nuxt-link
          :to="linkTo('tags', item)"
        >
          {{ item.fields.name }}
        </nuxt-link>
      </template>
      <!-- 終わり -->
      
    </v-data-table>
 ...
</template>
  • <template v-slot:item.fields.name="{ item }">

    v-slotには、拡張したいセルのvalueを指定します。

    valueはheadersの配列で指定しましたね。

headers: [
  {
    text: 'タグ',
    align: 'left',
    value: 'fields.name'	// この値を指定する
  },
  {
    text: '投稿数',
    align: 'center',
    width: 150,
    value: 'fields.postcount' // この値を指定する
  }
]

タグの名前にリンクがつきました。

クリックしたらちゃんとタグの個別ページに遷移しますね。OK!

2019-11-01 09-02-37

【step5】投稿の多い順に並び変える

さて、次は投稿の多い順にタグを並び替えます。

とは言っても簡単で、sort-bysort-descのプロパティを追加するだけで実現できます。

pages/tasg/index.vue
<template>
	...
    <v-data-table
      :headers="headers"
      :items="tableItems"
      :search="search"
      :sort-by="sortBy"   <!-- 追記 -->
      sort-desc           <!-- 追記 -->
    >
	...
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    sortBy: 'fields.postcount',   // 追記
    ...
  })
}
</script>
  • :sort-by="sortBy"

    sort-byプロパティに追加するのは、headersで指定したvalueの値です。

    ここでは’fields.postcount’を指定しています。

  • sort-desc

    true or false のブーリアン型を渡します。

    プロパティを指定しなければ小さい順に並びます。

    ちなみにtrueを渡す場合は、:sort-desc="true"としなくても、sort-descだけで渡すことができます。

投稿の多い順に並びましたね。

2019-11-01 15-43-08

【step6】1ページに表示するタグ数を変更する

続いてタグの表示数を変更しましょう。

items-per-pageに表示したい数字を渡します。

pages/tags/index.vue
<template>
  ...
    <v-data-table
      :headers="headers"
      :items="tableItems"
      :search="search"
      :sort-by="sortBy"
      :items-per-page="itemsPerPage"  <!-- 追記 -->
      sort-desc
    >
	...
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    sortBy: 'fields.postcount',
    itemsPerPage: 20,    // 追記

</script>
  • :items-per-page="itemsPerPage"

    今回は20を渡しています。

これで1ページに表示されるタグの数が20個になりました。

【step7】ページべネーションを追加する

さあ、最後のstep、ページべネーションを追加します。

data-tableのfooterを非表示に

まず、hide-default-footerプロパティでfooterを非表示にしましょう。

pages/tags/index.vue
<template>
  ...
    <v-data-table
      :headers="headers"
      :items="tableItems"
      :search="search"
      :sort-by="sortBy"
      :items-per-page="itemsPerPage"
      sort-desc
      hide-default-footer           <!-- 追記 -->
    >
  ...
  
</template>

tableのfooterが非表示になりました。

2019-11-01 16-03-49

べネーションを追加する

v-data-tableのとじタグの下にべネーションを追加します。

pages/tags/index.vue
<template>
  ...
    </v-data-table>

    <!-- paginationの追記 -->
		<div class="text-center py-2">
      <v-pagination
        v-model="page"
        :length="pageCount"
      />
    </div>
	...
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    sortBy: 'fields.postcount',
    itemsPerPage: 20,
    page: 1,         // 追記
    pageCount: 0,    // 追記

  ...
</script>

耳のようなものが出てきました。

2019-11-01 16-11-45

data-tableと連動させる

次は、べネーションとテーブルデータを連動させます。

data-table側page.sync@page-countを追記します。

pages/tags/index.vue
<template>
  ...
    <v-data-table
      :headers="headers"
      :items="tableItems"
      :search="search"
      :sort-by="sortBy"
      :items-per-page="itemsPerPage"
      :page.sync="page"                 <!-- 追記 -->
      sort-desc
      hide-default-footer
      @page-count="pageCount = $event"  <!-- 追記 -->
    >
  ...
</template>

ページべネーションが出てきました。

クリックすると次ページに移動していますね。よし!

2019-11-01 16-17-31

べネーションのボタンの数を制御する

べネーションをカスタマイズしていきましょう。

今のままだと問題があります。

先ほど指定したitemsPerPage: 20を、試しに1に変更してみてください。

itemsPerPageを20から1に変更したべネーション

2019-11-01 16-26-50

流石にボタンが多すぎますね。今後タグが増えてきたらこうなるということです。

そこでボタンの数を制御するtotal-visibleプロパティを設定します。

pages/tags/index.vue
<template>
  ...
    </v-data-table>

    <div class="text-center py-2">
      <v-pagination
        v-model="page"
        :length="pageCount"
        :total-visible="totalVisible"    <!-- 追記 -->
      />
    </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    sortBy: 'fields.postcount',
    itemsPerPage: 1,
    page: 1,
    pageCount: 0,
    totalVisible: 7,    // 追記
  ...
</script>
  • :total-visible="totalVisible"

    一番見栄えの良い、7を設定しています。

ボタンの数が減りましたね。確認が取れたらitemsPerPage: 1を、20に戻してください。

2019-11-01 16-34-01

ベーネションをオサレに

最後です。

ここから先は好みの問題ですのでご自由にカスタマイズしてください。

べネーションのボタンを丸く、矢印を別のiconに変更します。

pages/tags/index.vue
<template>
  ...
    </v-data-table>

    <div class="text-center py-2">
      <v-pagination
        v-model="page"
        :length="pageCount"
        :total-visible="totalVisible"
        circle						<!-- 追記 -->
        prev-icon="mdi-menu-left"	 <!-- 追記 -->
        next-icon="mdi-menu-right"	<!-- 追記 -->
      />
    </div>
   ...
</template>

ほお!オサレです \ o /

2019-11-01 16-49-45

以上でタグ一覧ページは完成です!

最終的なtags/index.vue

最終的なタグ一覧ページのコードです。

エラーが発生して動けない場合は、ここをコピペしてください。

pages/tags/index.vue
<template>
  <div>
    <breadcrumbs :add-items="addBreads" />

    <v-container>
      <v-row
        justify="center"
      >
        <v-col
          cols="12"
          sm="10"
          md="8"
        >
          <v-card>
            <v-card-title>
              <v-text-field
                v-model="search"
                append-icon="mdi-magnify"
                label="Search"
                single-line
                hide-details
              />
            </v-card-title>

            <v-data-table
              :headers="headers"
              :items="tableItems"
              :search="search"
              :sort-by="sortBy"
              :items-per-page="itemsPerPage"
              :page.sync="page"
              sort-desc
              hide-default-footer
              @page-count="pageCount = $event"
            >
              <template v-slot:item.fields.name="{ item }">
                <v-icon size="18">
                  mdi-tag-outline
                </v-icon>
                <nuxt-link
                  :to="linkTo('tags', item)"
                >
                  {{ item.fields.name }}
                </nuxt-link>
              </template>
            </v-data-table>

            <div class="text-center py-2">
              <v-pagination
                v-model="page"
                :length="pageCount"
                :total-visible="totalVisible"
                circle
                prev-icon="mdi-menu-left"
                next-icon="mdi-menu-right"
              />
            </div>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    search: '',
    sortBy: 'fields.postcount',
    itemsPerPage: 20,
    page: 1,
    pageCount: 0,
    totalVisible: 7,
    headers: [
      {
        text: 'タグ',
        align: 'left',
        value: 'fields.name'
      },
      {
        text: '投稿数',
        align: 'center',
        width: 150,
        value: 'fields.postcount'
      }
    ]
  }),
  computed: {
    ...mapState(['tags']),
    ...mapGetters(['linkTo']),
    addBreads() {
      return [{ icon: 'mdi-tag-outline', text: 'タグ一覧', to: '/tags', disabled: true, iconColor: 'grey' }]
    },
    tableItems() {
      const tags = []
      for (let i = 0; i < this.tags.length; i++) {
        const tag = this.tags[i]

        tag.fields.postcount = this.$store.getters.associatePosts(tag).length
        tags.push(tag)
      }
      return tags
    }
  }
}
</script>

本番環境にpushする

ここまでの変更を本番環境にpushしておきましょう。

$ git commit -am "add_datatable_for_tags/index.vue"
$ git push

本番環境のURLでも確認してみてくださいね。

お疲れ様でした!

さて、次回は?

さてさて、次回からは各ページのレイアウトを完成させていきます。

モックアップの作成やscssの導入を行いますよ。

それではまたお会いしましょう。

あとがき

いやぁ、流石に長かったですね。

最後まで読んでいただいてありがとうございます。

最後に、data-tableコンポーネントの拡張方法を3つ紹介しています。

お時間があるときにゆっくりご覧ください。↓

【コラム】Vuetify2 data-table 拡張機能いろいろ

data-tableのいろいろな拡張機能をご紹介します。

1. 行全体にリンクをつけたい【v-slot:body】

tableの行(trタグ)にリンクをつける方法です。

スマホの場合に、ユーザーが誤ってタップしてしまうのでおすすめはしません。

完成イメージ

2019-11-01 10-35-17

pages/tags/index.vue
<template>

  <v-data-table
    :headers="headers"
    :items="tableItems"
    :search="search"
  >
    <template v-slot:body="{ items }">
      <tbody>
        <tr
          v-for="(item, i) in items"
          :key="i"
          class="pointer"
          @click="goLink(item)"
        >
          <td>
            {{ item.fields.name }}
          </td>
          <td class="text-center">
            {{ item.fields.postcount }}
          </td>
        </tr>
      </tbody>
    </template>

</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {

  methods: {
    goLink(item) {
      this.$router.push(this.linkTo('tags', item))
    }
  }
}
</script>
<style lang="css">
  .pointer:hover {
    cursor: pointer;
  }
</style>
  • @click="goLink(item)"

    nuxt-linkでtrタグを囲んでしまうと、レイアウトが崩れてしまいます。

    そこでクリックイベントでページ遷移する方法をとっています。

  • cursor: pointer;

    table bodyをカスタマイズする場合、スタイリングを自分で行わなければなりません。

    マウスホーバーしたときに、ポインターになるようCSSを設定しています。

2. チェックボックスのカラーを変えたい【v-slot:item.data-table-select】

tableの横に表示するチェックボックスの色を変える拡張方法です。

header用とbody用、2パターンあります。

  • header = v-slot:header.data-table-select
  • body = v-slot:item.data-table-select

headerチェックボックスの色をパープルに、bodyチェックボックスをグリーンに変更しています。

完成イメージ

2019-11-01 12-46-57

pages/tags/index.vue
<template>
  ...
    <v-data-table
      v-model="selected"     <!-- 追記 -->
      :headers="headers"
      :items="tableItems"
      :search="search"
      show-select           <!-- 追記 -->
      item-key="sys.id"     <!-- 追記 -->
    >
      <!-- headerチェックボックス -->
      <template v-slot:header.data-table-select="{ on, props }">
        <v-simple-checkbox
          color="purple"
          v-bind="props"
          v-on="on"
        />
      </template>
      
      <!-- bodyチェックボックス -->
      <template v-slot:item.data-table-select="{ isSelected, select }">
        <v-simple-checkbox
          color="green"
          :value="isSelected"
          @input="select($event)"
        />
      </template>

      <template v-slot:item.fields.name="{ item }">
        <v-icon size="18">
          mdi-tag-outline
        </v-icon>

        <nuxt-link
          :to="linkTo('tags', item)"
        >
          {{ item.fields.name }}
        </nuxt-link>
      </template>
    </v-data-table>
  ...
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    selected: [],			// 追記
    ...
  }),
	...
}
</script>
  • v-model="selected"

    このselectedの配列にチェックを入れたアイテムが保管されます。

3. プログレスバーをカスタマイズしたい【v-slot:progress】

プログレスバーとは、ロード中に表示されるのバーことです。

バーの色をred、高さをデフォルトより少し高くしました。

完成イメージ

2019-11-01 13-46-32

pages/tags/index.vue
<template>
  ...
    <v-data-table
      :headers="headers"
      :items="isLoading ? [] : tableItems" <!-- 変更 -->
      :search="search"
      :loading="isLoading"                 <!-- 追記 -->
      loading-text="Loading..."            <!-- 追記 -->
      item-key="sys.id"
    >
  		<!-- プログレスバーのカスタマイズ -->
      <template v-slot:progress>
        <v-progress-linear
          color="red"
          :height="5"
          indeterminate
        />
      </template>

      <template v-slot:item.fields.name="{ item }">
        <v-icon size="18">
          mdi-tag-outline
        </v-icon>

        <nuxt-link
          :to="linkTo('tags', item)"
        >
          {{ item.fields.name }}
        </nuxt-link>
      </template>
    </v-data-table>
	...
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  data: () => ({
    isLoading: false, // 追記
    ...
  }),
	...

  // 追記
  created() {
    this.isLoading = true
  },
  mounted() {
    this.$nextTick().then((res) => {
      setTimeout(() => { res.isLoading = false }, 500)
    })
  }
}
</script>

他にもいろいろカスタマイズできますが、基本的にはv-slotを使わなくても、プロパティだけでおおよそのカスタマイズが可能です。

できるだけシンプルに、

どうしてもという部分だけ使いましょう。

参考

Data table -Vuetify

(おしまい)

現在、カテゴリー「Rails apiとNuxt.jsでSPA開発」のデモアプリを構築中です。記事になるまでもう少々のお時間が必要です。ブログの更新が止まって申し訳ありません。デモアプリの進捗状況は こちらの記事 で随時お伝えしてまいります。
スポンサー広告
次の記事はこちらです
ブログ構築
1. 今回作るアプリケーション
#01
Nuxt.jsとContentfulで作るマイブログ
今日のTweet
スポンサー広告