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.cybozu.com/vuejs/v3.4.20/vue.global.prod.js
  • https://cdn.jsdelivr.net/npm/vuetify@3.5.6/dist/vuetify.min.js
CSS ファイル
  • https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css
  • https://cdn.jsdelivr.net/npm/vuetify@3.5.6/dist/vuetify.min.css

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

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

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

  • 一覧名
  • 表示形式:「カスタマイズ」を選択
  • HTML:後述のVueテンプレート
  • ページネーションを表示する:未選択

また、「一覧ID」をメモしておいてください。

以下のように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
<!--
* 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-main>
      <v-container fluid>
        <template v-if="!detailView">
          <v-card>
            <v-card-title>
              顧客一覧
              <v-spacer></v-spacer>
              <v-text-field
                v-model="search"
                prepend-inner-icon="mdi-magnify"
                label="検索"
                single-line
                hide-details
              ></v-text-field>
            </v-card-title>
            <v-data-table
              :headers="headers"
              :items="customers"
              :search="search"
              class="elevation-1"
            >
              <template v-slot:item="props">
                <tr>
                  <td>
                    <v-icon 
                      large 
                      color="primary" 
                      @click="showDetail(props.item)"
                      icon="mdi-magnify"
                    >
                    </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>
                </tr>
              </template>
            </v-data-table>
          </v-card>
        </template>
        <template v-else>
          <v-card>
            <v-card-actions>
              <v-btn size="large" variant="outlined" color="primary" @click="back">一覧に戻る</v-btn>
              <v-btn size="large" variant="flat" color="primary" @click="save">変更を保存</v-btn>
            </v-card-actions>
            <v-divider></v-divider>
            <v-card-title>顧客詳細</v-card-title>
            <v-card-text>
              <v-row justify="space-around">
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Company_name.value"
                    label="会社名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field 
                    v-model="customer.Department.value"
                    label="部署名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Representative.value"
                    label="担当者名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
              <v-row justify="space-around">
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Zip_code.value"
                    label="郵便番号"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Phone.value"
                    label="電話番号"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Fax.value"
                    label="Fax"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
              <v-row justify="space-around">
                <v-col cols="12" sm="6" md="6">
                  <v-text-field
                    v-model="customer.Address.value"
                    label="住所"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="6">
                  <v-select
                    v-model="customer.Rank.value"
                    :items="rankList"
                    label="顧客ランク"
                    variant="outlined"
                  >
                  </v-select>
                </v-col>
              </v-row>
              <v-row justify="space-around">
                <v-col cols="12">
                  <v-text-field
                    v-model="customer.Mail.value"
                    label="メールアドレス"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
              <v-row justify="space-around">
                <v-col cols="12">
                  <v-text-field
                    v-model="customer.Note.value"
                    label="備考"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
              <v-row justify="space-around">
                <v-col cols="12" sm="6">
                  <v-text-field
                    v-model="customer.record_no.value"
                    label="レコード番号"
                    disabled
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6">
                  <v-text-field
                    v-model="customer.lat.value"
                    label="緯度"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6">
                  <v-text-field
                    v-model="customer.lng.value"
                    label="経度"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
            </v-card-text>
          </v-card>
        </template>
      </v-container>
    </v-main>
  </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="mdi-magnify"
                label="検索"
                single-line
                hide-details
              ></v-text-field>
            </v-card-title>

一覧テーブルを表示します。
フッター部分にページ切り替え表示を加えます。
headers items searchのデータをバインディングしています。

 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
            <v-data-table
              :headers="headers"
              :items="customers"
              :search="search"
              class="elevation-1"
            >
              <template v-slot:item="props">
                <tr>
                  <td>
                    <v-icon 
                      large 
                      color="primary" 
                      @click="showDetail(props.item)"
                      icon="mdi-magnify"
                    >
                    </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>
                </tr>
              </template>
            </v-data-table>

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

1
2
3
4
            <v-card-actions>
              <v-btn size="large" variant="outlined" color="primary" @click="back">一覧に戻る</v-btn>
              <v-btn size="large" variant="flat" color="primary" @click="save">変更を保存</v-btn>
            </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-row justify="space-around">
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Company_name.value"
                    label="会社名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Department.value"
                    label="部署名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
                <v-col cols="12" sm="6" md="4">
                  <v-text-field
                    v-model="customer.Representative.value"
                    label="担当者名"
                    variant="outlined"
                  >
                  </v-text-field>
                </v-col>
              </v-row>
                  .
                  .
                  .
            <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/
*/
(()=> {
  kintone.events.on('app.record.index.show', (event) => {
    if (event.viewId !== 123) { // 作成したカスタマイズビューのIDを指定
      return event;
    }

    // Vue3 と Vuetify3 を使用する準備
    const {createApp, ref, reactive, toRefs, computed} = Vue;
    const vuetify = Vuetify.createVuetify();

    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 app = createApp({
        setup() {
          const detailView = ref(false);
          const customers = ref(resp.records);
          const customer = reactive({});
          const search = ref('');
          const rankList = ['A', 'B', 'C'];
          const headers = [
            {title: '詳細表示', key: 'actions', sortable: false},
            {title: 'レコード番号', key: 'record_no.value'},
            {title: '会社名', key: 'Company_name.value'},
            {title: '部署名', key: 'Department.value'},
            {title: '担当者名', key: 'Representative.value'},
            {title: '住所', key: 'Address.value'}
          ];

          // 詳細表示のアイコンクリック
          const showDetail = (item) => {
            detailView.value = true;
            Object.assign(customer, item);
          };

          // 一覧に戻るボタンクリック
          const back = () => {
            detailView.value = false;
          };

          // 変更を保存ボタンクリック
          const save = () => {
            const param = {
              app: appId,
              id: customer.record_no.value,
              record: {
                Company_name: {value: customer.Company_name.value},
                Department: {value: customer.Department.value},
                Representative: {value: customer.Representative.value},
                Zip_code: {value: customer.Zip_code.value},
                Phone: {value: customer.Phone.value},
                Fax: {value: customer.Fax.value},
                Address: {value: customer.Address.value},
                Rank: {value: customer.Rank.value},
                Mail: {value: customer.Mail.value},
                Note: {value: customer.Note.value},
                lat: {value: customer.lat.value},
                lng: {value: customer.lng.value}
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', param)
              .then(() => alert('データが更新されました。'))
              .catch(err => alert(err.message));
          };

          return {...toRefs({detailView, customers, customer, search, rankList, headers}), showDetail, back, save};
        }
      });

      app.use(vuetify).mount('#app');
    });
    return event;
  });
})();

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

解説

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  kintone.events.on('app.record.index.show', (event) => {
    if (event.viewId !== 123) { // 作成したカスタマイズビューのIDを指定
      return event;
    }

    // Vue と Vuetify を使用する準備
    const {createApp, ref, reactive, toRefs, computed} = Vue;
    const vuetify = Vuetify.createVuetify();

    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;
  });

createApp()でVue.jsのインスタンスを生成し、テンプレートのDOMにマウントします。
また、このときVuetifyを利用するためにuse()でVuetifyを読み込みます。

1
2
3
4
      const app = createApp({
        // ...
      });
      app.use(vuetify).mount('#app');

setupフックでは、以下の変数を定義しています。

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

以下は、ボタンがクリックされたときに実行される関数を定義しています。

  • 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
35
          // 詳細表示のアイコンクリック
          const showDetail = (item) => {
            detailView.value = true;
            Object.assign(customer, item);
          };

          // 一覧に戻るボタンクリック
          const back = () => {
            detailView.value = false;
          };

          // 変更を保存ボタンクリック
          const save = () => {
            const param = {
              app: appId,
              id: customer.record_no.value,
              record: {
                Company_name: {value: customer.Company_name.value},
                Department: {value: customer.Department.value},
                Representative: {value: customer.Representative.value},
                Zip_code: {value: customer.Zip_code.value},
                Phone: {value: customer.Phone.value},
                Fax: {value: customer.Fax.value},
                Address: {value: customer.Address.value},
                Rank: {value: customer.Rank.value},
                Mail: {value: customer.Mail.value},
                Note: {value: customer.Note.value},
                lat: {value: customer.lat.value},
                lng: {value: customer.lng.value}
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', param)
              .then(() => alert('データが更新されました。'))
              .catch(err => alert(err.message));
          };

STEP4:動作確認

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

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

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

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

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

注意事項

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

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

まとめ

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

参考サイト

information

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