Vue.js+Vuetify.js を使って、レコードの一覧と詳細をシングルページで作成しよう!

著者名:Mamoru Fujinoki( Fuji Business International (External link)

目次

はじめに

Cybozu CDN にも公開されているように Vue.js の JavaScript ライブラリーが近年シングルページアプリケーション(SPA)の開発環境として人気を博しているようです。
AngularJS や React よりも比較的に覚えやすく、フロントエンド初心者でもとっつき易いプラットフォームだと思います。

今回は、この Vue.js のライブラリーと Vue.js 用の UI ライブラリーの Vuetify.js を使って、カスタマイズビュー上でレコード一覧と詳細をシングルページ上で実現します。
リアクティブな検索機能も作成します。

開発の流れ

STEP1:kintone アプリの設定・変更

アプリの追加

kintone アプリストアにて、検索テキストボックスに「顧客リスト」と入力し検索します。
そして、検索結果の「顧客リスト」アプリを追加します。

フィールドコードの変更

「顧客リスト」アプリを開き、「アプリの設定」画面の「フォーム」タブで次のテーブルを参考にフィールドの設定を確認・変更します。

フィールドの種類 フィールド名 フィールドコード
文字列(1 行) 会社名 Company_name
文字列(1 行) 部署名 Department
文字列(1 行) 担当者名 Representative
文字列(1 行) 郵便番号(数字のみ) Zip_code
文字列(1 行) TEL(数字のみ) Phone
文字列(1 行) FAX(数字のみ) Fax
文字列(1 行) 住所 Address
ドロップダウン 顧客ランク Rank
文字列(1 行) メールアドレス Mail
文字列(複数行) 備考 Note
レコード番号 レコード番号 record_no
数値 緯度 lat
数値 経度 lng

ライブラリの追加

次に「設定」タブを開き、「JavaScript / CSS でカスタマイズ」をクリックして、以下の JavaScript ファイル、CSS ファイルの URL を指定し、設定を保存します。

JavaScript ファイル
  • https://js.kintone.com/vuejs/v2.6.9/vue.min.js
  • https://cdn.jsdelivr.net/npm/vuetify@1.5.12/dist/vuetify.min.js
CSS ファイル
  • https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons
  • https://cdn.jsdelivr.net/npm/vuetify@1.5.12/dist/vuetify.min.css

STEP2:カスタムビューでの Vue.js のテンプレート開発

今度は、「一覧」タブで、「+」サインをクリックして、一覧を追加します。

一覧設定画面で、以下を設定します。

  1. 一覧名を入力する。
  2. 表示形式に「カスタマイズ」を選択する。
  3. 一覧 ID をメモしておく。
  4. 「ページネーションを表示する」を外す。
  5. 下記を参考に HTML コードを入力する。

以下のように Vuetify.js でテンプレートを作成します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<!--
* vue.js + vuetify.js + Custom view sample css
* Copyright (c) 2019 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
-->
<div id="app">
  <v-app>
    <v-content>
      <v-container fluid>
        <template v-if="!detailView">
          <v-card>
            <v-card-title>
                顧客一覧
                <v-spacer></v-spacer>
                <v-text-field
                v-model="search"
                append-icon="search"
                label="検索"
                single-line
                hide-details
                ></v-text-field>
            </v-card-title>
            <v-data-table
                :headers="headers"
                :items="customers"
                :search="search"
                class="elevation-1"
                :pagination.sync="pagination"
                hide-actions
            >
              <template v-slot:items="props">
                <td>
                    <v-icon 
                        large
                        color="primary"
                        @click="showDetail(props.item)"
                    >
                        pageview
                    </v-icon>
                </td>
                <td>{{ props.item.record_no.value }}</td>
                <td>{{ props.item.Company_name.value }}</td>
                <td>{{ props.item.Department.value }}</td>
                <td>{{ props.item.Representative.value }}</td>
                <td>{{ props.item.Address.value }}</td>
              </template>
            </v-data-table>
            <div class="text-xs-center pt-2">
                <v-pagination v-model="pagination.page" :length="pages"></v-pagination>
            </div>
          </v-card>
        </template>
        <template v-else>
          <v-layout justify-left>
            <v-flex xs12 sm10 md8>
              <v-card>
                <v-card-actions>
                  <v-layout justify-left>
                    <v-btn class="text-xs-center" large outline color="primary" @click="back">一覧に戻る</v-btn>
                    <v-btn class="text-xs-center" large color="primary" @click="save">変更を保存</v-btn>
                  </v-layout>
                </v-card-actions>
                <v-divider></v-divider>
                <v-card-title>
                  顧客詳細
                </v-card-title>
                <v-card-text>
                  <v-layout row justify-space-around>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Company_name.value"
                        label="会社名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Department.value"
                        label="部署名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Representative.value"
                        label="担当者名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Zip_code.value"
                        label="郵便番号"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Phone.value"
                        label="電話番号"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Fax.value"
                        label="Fax"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 sm6 md5>
                      <v-text-field
                        v-model="customer.Address.value"
                        label="住所"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md5>
                      <v-select
                        v-model="customer.Rank.value"
                        :items="rankList"
                        label="顧客ランク"
                        box
                        attach
                      >
                      </v-select>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 md11>
                      <v-text-field
                        v-model="customer.Mail.value"
                        label="メールアドレス"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 md11>
                      <v-text-field
                        v-model="customer.Note.value"
                        abel="備考"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 md11>
                      <v-text-field
                        v-model="customer.record_no.value"
                        label="レコード番号"
                        disabled
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  <v-layout row justify-space-around>
                    <v-flex xs12 sm6 md5>
                      <v-text-field
                        v-model="customer.lat.value"
                        label="緯度"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md5>
                      <v-text-field
                        v-model="customer.lng.value"
                        label="経度"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                </v-card-text>
              </v-card>
            </v-flex>
          </v-layout>
        </template>
      </v-container>
    </v-content>
  </v-app>
</div>

以上の設定後、変更を保存します。

また、以下のように css をカスタマイズすることで、一覧の行表示の色分けが可能です。
「vuetify_sample.css」のように適当なファイル名をつけて保存し、「JavaScript / CSS でカスタマイズ」の設定画面で、アップロードします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*
 * vue.js + vuetify.js + Custom view sample css
 * Copyright (c) 2019 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
tbody tr:nth-of-type(odd) {
  background-color: rgba(0, 0, 0, .05);
}

解説

一覧表示と詳細表示を detailView のフラグで切り替えています。

1
2
3
4
5
6
<template v-if="!detailView">
 ...
</template>
<template v-else>
 ...
</template>

ページのタイトルと検索フィールドを表示します。
search プロパティーでテーブルのフィルター機能と連携しています。
v-model で指定することにより、双方向のデータバインディングを実現しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
            <v-card-title>
                顧客一覧
                <v-spacer></v-spacer>
                <v-text-field
                v-model="search"
                append-icon="search"
                label="検索"
                single-line
                hide-details
                ></v-text-field>
            </v-card-title>

一覧テーブルを表示します。
フッター部分にページ切り替え表示を加えます。
headers items search のデータをバインディングしています。
hide-actions ではデフォルトのページ切り替え表示を無効にして、カスタムのページ切り替えを表示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
            <v-data-table
                :headers="headers"
                :items="customers"
                :search="search"
                class="elevation-1"
                :pagination.sync="pagination"
                hide-actions
            >
              <template v-slot:items="props">
                <td>
                    <v-icon 
                        large
                        color="primary"
                        @click="showDetail(props.item)"
                    >
                        pageview
                    </v-icon>
                </td>
                <td>{{ props.item.record_no.value }}</td>
                <td>{{ props.item.Company_name.value }}</td>
                <td>{{ props.item.Department.value }}</td>
                <td>{{ props.item.Representative.value }}</td>
                <td>{{ props.item.Address.value }}</td>
              </template>
            </v-data-table>
            <div class="text-xs-center pt-2">
                <v-pagination v-model="pagination.page" :length="pages"></v-pagination>
            </div>

詳細表示のトップにボタンを配置します。
今回は、「一覧に戻る」と「変更を保存」のボタンを作成しました。

1
2
3
4
5
6
                <v-card-actions>
                  <v-layout justify-left>
                    <v-btn class="text-xs-center" large outline color="primary" @click="back">一覧に戻る</v-btn>
                    <v-btn class="text-xs-center" large color="primary" @click="save">変更を保存</v-btn>
                  </v-layout>
                </v-card-actions>

顧客の詳細情報を表示します。
ここで変更したフィールドのデータは即座に一覧のデータにも反映されていますが、「変更を保存」しない限り、kintone のデータベースへは変更が反映されていません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
                <v-card-text>
                  <v-layout row justify-space-around>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Company_name.value"
                        label="会社名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Department.value"
                        label="部署名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                    <v-flex xs12 sm6 md3>
                      <v-text-field
                        v-model="customer.Representative.value"
                        label="担当者名"
                        box
                      >
                      </v-text-field>
                    </v-flex>
                  </v-layout>
                  .
                  .
                  .
                <v-card-text>

STEP3:Vue.js によるプログラムの開発

以下のサンプルコードを参考に Vue.js のプログラムを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
 * vue.js + vuetify.js + Custom view sample program
 * Copyright (c) 2019 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
(function() {
  kintone.events.on('app.record.index.show', (event) => {
    if (event.viewId !== 123) { // 作成したカスタマイズビューのIDを指定
      return event;
    }
    const appId = kintone.app.getId();
    const query = kintone.app.getQuery();
    kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {app: appId, query: query}, (resp) => {
      const vm = new Vue({
        el: '#app',
        data() {
          return {
            headers: [
              {text: '詳細表示', sortable: false},
              {text: 'レコード番号', align: 'left', sortable: true, value: 'record_no.value'},
              {text: '会社名', value: 'Company_name.value'},
              {text: '部署名', value: 'Department.value'},
              {text: '担当者名', value: 'Representative.value'},
              {text: '住所', value: 'Address.value'}
            ],
            detailView: false,
            customers: resp.records,
            customer: {},
            search: '',
            rankList: ['A', 'B', 'C'],
            pagination: {rowsPerPage: 10}
          };
        },
        methods: {
          showDetail: function(item) {
            this.detailView = true;
            this.customer = item;
          },
          back: function() {
            this.detailView = false;
          },
          save: function() {
            const param = {
              app: appId,
              id: this.customer.record_no.value,
              record: {
                Company_name: {value: this.customer.Company_name.value},
                Department: {value: this.customer.Department.value},
                Representative: {value: this.customer.Representative.value},
                Zip_code: {value: this.customer.Zip_code.value},
                Phone: {value: this.customer.Phone.value},
                Fax: {value: this.customer.Fax.value},
                Address: {value: this.customer.Address.value},
                Rank: {value: this.customer.Rank.value},
                Mail: {value: this.customer.Mail.value},
                Note: {value: this.customer.Note.value},
                lat: {value: this.customer.lat.value},
                lng: {value: this.customer.lng.value}
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', param).then((_resp) => {
              alert('データが更新されました。');
            }, (err) => {
              alert(err.message);
            });
          }
        },
        computed: {
          pages() {
            if (this.pagination.rowsPerPage === null || this.pagination.totalItems === null) {
              return 0;
            }
            return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage);
          }
        }
      });
    });
    return event;
  });
})();

プログラム作成後、「sample_vue.js」等のファイル名を指定して、保存し、kintone の「JavaScript / CSS でカスタマイズ」の設定画面にて、アップロードします。

解説

上記でメモしておいた一覧 ID が一致した場合のみ、一覧表示のイベントで処理を続行します。
また、「ページネーションを表示する」を外したため、イベントの発生時にレコードが取得されていません。
よって、kintone API より、レコードの一括取得を行います。
最大取得数は 500 件ですが、何も指定しない場合、初期値は 100 件までです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  kintone.events.on('app.record.index.show', (event) => {
    if (event.viewId !== 123) { // 作成したカスタマイズビューのIDを指定
      return event;
    }
    const appId = kintone.app.getId();
    const query = kintone.app.getQuery();
    kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {app: appId, query: query}, (resp) => {
      // ...
    });
    return event;
  });

Vue.js のインスタンスを生成します。
el プロパティには、テンプレートの最上部の div の id 名を指定しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
      const vm = new Vue({
        el: '#app',
        data() {
          // ...
        },
        methods: {
          // ...
        },
        computed: {
          // ...
        }
      });

data プロパティには、以下を設定しています。

  • headers:一覧のヘッダーのデータオブジェクト、
  • customers:kintone から取得した顧客レコード一覧情報
  • detailsView:画面切り替えフラグ
  • customer:一覧から選択した顧客の詳細情報、
  • search:検索フィールドで入力した文字列
  • rankList:顧客ランクのドロップダウンの値
  • pagenation:一覧に表示されるレコード数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
        data() {
          return {
            headers: [
              {text: '詳細表示', sortable: false},
              {text: 'レコード番号', align: 'left', sortable: true, value: 'record_no.value'},
              {text: '会社名', value: 'Company_name.value'},
              {text: '部署名', value: 'Department.value'},
              {text: '担当者名', value: 'Representative.value'},
              {text: '住所', value: 'Address.value'}
            ],
            detailView: false,
            customers: resp.records,
            customer: {},
            search: '',
            rankList: ['A', 'B', 'C'],
            pagination: {rowsPerPage: 10}
          };
        },

method プロパティで定義している関数は、ボタンがクリックされたときに実行される関数です。
showDetail メソッドは、顧客一覧で詳細表示のアイコンがクリックされると実行され、back メソッドは、「一覧に戻る」ボタンをクリックすると実行されます。
また、save メソッドは、「変更を保存」ボタンをクリックすると実行されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        methods: {
          showDetail: function(item) {
            this.detailView = true;
            this.customer = item;
          },
          back: function() {
            this.detailView = false;
          },
          save: function() {
            const param = {
              app: appId,
              id: this.customer.record_no.value,
              record: {
                Company_name: {value: this.customer.Company_name.value},
                Department: {value: this.customer.Department.value},
                Representative: {value: this.customer.Representative.value},
                Zip_code: {value: this.customer.Zip_code.value},
                Phone: {value: this.customer.Phone.value},
                Fax: {value: this.customer.Fax.value},
                Address: {value: this.customer.Address.value},
                Rank: {value: this.customer.Rank.value},
                Mail: {value: this.customer.Mail.value},
                Note: {value: this.customer.Note.value},
                lat: {value: this.customer.lat.value},
                lng: {value: this.customer.lng.value}
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', param).then((_resp) => {
              alert('データが更新されました。');
            }, (err) => {
              alert(err.message);
            });
          }
        },

最後に、computed プロパティでは、pages でページ切り替えの情報を返します。

1
2
3
4
5
6
7
8
        computed: {
          pages() {
            if (this.pagination.rowsPerPage === null || this.pagination.totalItems === null) {
              return 0;
            }
            return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage);
          }
        }

STEP4:動作確認

一覧表示選択ドロップダウンより、作成したカスタマイズビューを選択します。

「検索」フィールドに検索したい文字列を入力すると、一覧の絞り込みが即座に行われます。

次に詳細表示のアイコンをクリックします。

詳細画面が表示されるので、「会社名」を適当に変更し、「変更を保存」します。
保存が成功した後「一覧に戻る」をクリックします。

一覧で「会社名」の変更が即座に反映されています。

注意事項

このサンプルでは、取得した 100 件までのレコードに対するレコードの検索絞り込みやページネーションのみ有効になります。

また、検索フィールドで絞り込みできるフィールドは、一覧に表示したフィールドのみです。

まとめ

カスタマイズビューを Vue.js と Vuetify.js を使って作成すると一覧と詳細ページがシングルページで比較的簡単に作成できます。
一覧の検索も入力に応じて絞り込みできたり、デバイスの画面の大きさに対して表示を切り替えできたりする機能を実現できます。
Vue.js は、シングルページアプリケーションや携帯端末のアプリケーションの開発に適していますので、ぜひ、試してみてはいかがでしょうか?

参考サイト

information

この Tips は、2019 年 7 月版 kintone で動作を確認しています。