Handsontableを使用してExcel風テーブルを実装する。Vue/cliで実装。

・初期表示データとドロップダウンメニューの選択肢にapiから取得したデータを使用する

・行の追加と削除、コピペ対応、セルの非活性化に対応する。

・APIのバルクインポートにも対応(DELETE, UPDATE, CREATEを1種類のPOSTで行う)

→面倒になってしまった。post処理はただのvueとjsの記述になるので、対応できるようなhandsontableの実装をしただけになる。特にpost処理については触れていない。

【要約・学んだこと】

細かい解説は他の人がいいまとめを作ってくれているので、ざっくりと紹介。

Handsontableをいい部分をざっくり紹介

・とてもExcelっぽくテーブルを実装できる。

・ライブラリで用意されているHooks(関数?設定?)が豊富にあり、拡張性がある。また、カスタマイズも可能。

・React, Vue, Angularのラッパーが公式で存在する。

・無料で使える。(一部制限有り)

・コミュニティに質問を投げると答えてくれる。

2. Handsontabbleのやばい部分をざっくり紹介

・多機能であるがゆえ、ドキュメントが膨大(日本語ドキュメントなし)

・ドキュメント読んでいるとできそうなのに実装を進めるとうまくいかない部分が予期せぬタイミングで発生する。別の方法で実装し直すと、また別の部分でうまくいかない部分が出てくる。の無限ループにハマれる。

・2019年4月30日現在、最新バージョンである7.0.2を商用目的で使用するには、ポーランドに見積もりを依頼しなければいけない。バージョン6.2.2以下は引き続き無料で利用可能。

・ChromeだとうまくいくのにIE11だとうまくいかないことがある。また、ChromeとIE11を比べると、IE11での動作はかなり重い。

Handsontable習得のプロセス

  1. 公式ドキュメントで使用するバージョンを選択し、Getting started とWrapperの部分をざっと読む。
  2. 日本語で解説されてるのを一通り読んで、どんなことができるかを理解する。
  3. 実装する。うまくいかなかったら公式ドキュメントを読み込む
  4. 無理ならコミュニティに質問を投げる。

読むべき日本語の解説

下記にまとめがある。

上記の「使い方」にある「Handsontable 使い方メモシリーズ」はかなりの数のHooksを紹介してくれている。コードとサンプルを合わせて載せてくれているので、最初にこれに目を通すと早い。

Vue.jsでHandsontableを使う

今回は

  1. Vue/cliで実装
  2. 商用で利用する
  3. 初期表示データとドロップダウンメニューの選択肢をapiから取得したデータにする
  4. コピペ対応、行の追加と削除対応
  5. 初期データの一部は編集不可能

というありがちな条件で実装。これが意外とクソむずかった。

まずはインストール。商用で利用できるように古いバージョンを使用する。本体とvueのラッパーをインストール

yarn add handsontable@6.2.2 @handsontable/vue@3.1.0

次に画面にHandsontbaleを表示させる。

ルートとなるvueコンポーネント

src/App.vue<template>
<div id="app">
<TableIndex/>
</div>
</template>
<script>
import TableIndex from "./components/TableIndex.vue";
export default {
name: "app",
components: {
TableIndex
}
};
</script>
<style lang="scss">
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Tableの親コンポーネント

<template>
<Handsontable :members="members" :department="department"/>
</template>
<script>
import api from "axios";
import Handsontable from "./Handsontable";
export default {
name: "TableIndex",
components: {
Handsontable
},
data: function() {
return {
members: [],
department: []
};
},
};
</script>
<style></style>

Handsontableを読み込むTable子コンポーネント

<template>
<div>
<HotTable :settings="hotSettings"/>
</div>
</template>
<script>
import { HotTable } from "@handsontable/vue";
export default {
name: "Handsontable",
components: {
HotTable
},
data: function() {
return {
hotSettings: {
data: [
["", "Ford", "Volvo", "Toyota", "Honda"],
["2016", 10, , 12, 13],
["2017", 20, 11, 14, 13],
["2018", 30, 15, 12, 13]
],
colHeaders: true,
rowHeaders: true
}

};
},
};
</script>
<style src="../../node_modules/handsontable/dist/handsontable.full.css"></style>

ドキュメント通り。ここまで問題なし。

package.jsonは下記のようになっている。先にインストールしたhandsontableと@handsontable/vue以外はvue createで初期にインストールされたもの。

{
"name": "hansontable",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@handsontable/vue": "3.1.0",
"core-js": "^2.6.5",
"handsontable": "6.2.2",
"vue": "^2.6.10",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.7.0",
"@vue/cli-plugin-eslint": "^3.7.0",
"@vue/cli-service": "^3.7.0",
"@vue/eslint-config-prettier": "^4.0.1",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
"vue-template-compiler": "^2.5.21"
}
}

Apiデータの用意

本件と関係ないけど、いい無料APIが見つからなかったので自作。楽チンなjson-serverを使う。

yarn add json-server

今回は名簿的なデータが欲しいので、db.jsonを下記のような感じで作成

// db.json{
"members": [
{
"id": "001",
"name": "Paul",
"mail": "paul@mail.com",
"department": "Marketing",
"position": "Manager"
},
{
"id": "002",
"name": "Tom",
"mail": "tom@mail.com",
"department": "Engineering",
"position": "Manager"
},
...
{
"id": "010",
"name": "Michel",
"mail": "paul@mail.com",
"department": "Marketing",
"position": "Manager"
}
],
"department": ["Marketing", "Engineering", "Accounting"]
}

また、毎回起動するのが面倒なのでscriptを修正

// package.json..."scripts": {
"serve": "vue-cli-service serve | $(npm bin)/json-server --watch db.json",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
...

jsonサーバーができていることを確認

APIデータをテーブルに読み込む。

今はHandsontable.vueのdataに初期テーブルデータをベタ書きしているが、実際にこのような使い方をすることはまれでしょう。APIデータを読み込む。ここが最初のつまずきポイント。

TableIndex.vueでApiデータを取得。

TableIndex.vue<template>
<Handsontable :members="members" :department="department" @getTableContents="getTableContents"/>
</template>
<script>
import api from "axios";
import Handsontable from "./Handsontable";
export default {
name: "TableIndex",
components: {
Handsontable
},
data: function() {
return {
members: [],
department: []
};
},
computed: {
getUrl: () => "http://localhost:3000"
},
methods: {
getMembers: function() {
return api.get(`${this.getUrl}/members`);
},
getDepartment: function() {
return api.get(`${this.getUrl}/department`);
},
getTableContents: async function() {
const getData = await api.all([this.getMembers(), this.getDepartment()]);
const membersData = getData[0].data;
const departmentData = getData[1].data;
this.members = membersData;
this.department = departmentData;

}
}
};
</script>
<style></style>

apiからmembersとdepartmentを取得し、dataに代入する関数をmethodsで記述している。

子コンポーネントのcreatedでその関数が実行される。

membersを表に反映させたい。Handsontable.vueで代入する。

Handsontable.vue<template>
<HotTable :data="members" :settings="hotSettings"/>
</template>
<script>
import { HotTable } from "@handsontable/vue";
export default {
name: "Handsontable",
components: {
HotTable
},
props: {
members: {
type: Array,
default: []
},
department: {
type: Array,
default: []
}
},

data: function() {
return {
hotSettings: {
colHeaders: true,
rowHeaders: true
}
};
},
created: async function() {
await this.$emit("getTableContents");
};
</script>
<style src="../../node_modules/handsontable/dist/handsontable.full.css"></style>

はい、うまくレンダリングされません。リロードしてもダメです。しかし、どこか編集してセーブし、hot reloadが発生するとうまく読み込めます。

これはおそらくapiを取得する前にhandsontableがレンダリングされてしまうために発生する現象だと思われる。これがhandsontableの難しさである。

コレクションの場合は下記のようにcolumnsを指定するとうまく表示される。このようにちょっとした設定違いでうまくいかない部分があるので、場合によって長時間ハマってしまうが、試行錯誤すると大抵乗り越えられる。(ましてや今回はHot reloadだとうまく読み込めるので、試行錯誤の方向性をミスりやすい。)

// Handsontable.vue<template>
<div>
<HotTable :data="members" :settings="hotSettings"/>
</div>
</template>
<script>
import { HotTable } from "@handsontable/vue";
export default {
name: "Handsontable",
components: {
HotTable
},
props: {
members: {
type: Array,
default: []
},
department: {
type: Array,
default: []
}
},

data: function() {
return {
hotSettings: {
colHeaders: true,
rowHeaders: true,
columns: [
{ data: "id", type: "text", placeholder: "ID" },
{ data: "name", type: "text", placeholder: "name" },
{ data: "mail", type: "text", placeholder: "mail" },
{ data: "department", type: "text", placeholder: "department" },
{ data: "position", type: "text", placeholder: "position" }
]
}
};
},
created: async function() {
await this.$emit("getTableContents");
},
methods: {
getTableData: function() {
this.$refs.hotTable.hotInstance.render();
console.log(this.$refs.hotTable.hotInstance.getData());
}
}
};
</script>
<style src="../../node_modules/handsontable/dist/handsontable.full.css"></style>

これでapiデータが画面に反映される

Web上でExcel風のテーブルを実装するHandsontableを使う-2 に続く。

【わからなかったこと】

今はなし。

【感想】

ペースト機能、行の追加削除機能、バルクインポートを行わなければそこまで難しくない。次回以降厄介な部分の回避方法を紹介。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

Responses (1)