Vue.jsでSortable Table作ってみた

Vue.jsの虜になって色々勉強しています。
今回はソート機能をもったテーブルを作ってみることにしました。
一週間前に始めたので未熟ですが、とにかくコード書いてるとこです。

ということで以下のように`custom-thead`と`custom-tbody`のコンポーネントを
作成して任意のデータを渡してあげるとソート可能なテーブルを表示させてみたいと思います。

# こんな感じでhtmlタグを記述したい

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ページタイトル</title>
</head>
<body>

<div id="sortableTable">
  <table>
    <custom-thead></custom-thead>
    <custom-tbody></custom-tbody>
  <table>
</div>

</body>
</html>


```


# カスタムのテーブルヘッダー

ヘッダーのカラムも任意にデータを渡して汎用的にしたかったので`th`タグの部分もカスタムコンポーネントにしてみました。

```js:index.js
Vue.component('custom-thead', {
    template: `<thead>
                 <tr>
                   <custom-th v-for="title in headertitlelist" :title="title.title" :sortFilterAttribute="title.sort" :key="title.title"></custom-th>
                 </tr>
               </thead>`
});

Vue.component('custom-th', {
    template: `<th scope="col">{{ title }}
               　<a href="#">
                 　<i v-show="sortFilterAttribute == 2" class="material-icons">arrow_drop_down</i>
                   <i v-show="sortFilterAttribute == 1" class="material-icons">arrow_drop_up</i>
                   <i v-show="sortFilterAttribute == 0" class="material-icons">sort</i>
                 </a>
               </th>`
});

```
`headertitlelist`という配列にヘッダーのタイトル情報を保持する想定で、そのヘッダーのタイトル分だけ`v-for`ディレクティブを使ってカラムを生成します。
カラムの部分にはソート用のアイコンも表示します。この部分もクリック時に制御させます。

```js:index.js
Vue.component('custom-thead', {
    props: ['headertitlelist'],
    template: `<thead>
                 <tr>
                   <custom-th v-for="title in headertitlelist" :title="title.title" :sortFilterAttribute="title.sort" :key="title.title" v-on:sortEvent="sortColumn"></custom-th>
                 </tr>
               </thead>`
});

Vue.component('custom-th', {
    props: ['title', 'sortFilterAttribute'],
    template: `<th scope="col">{{ title }}
               　<a href="#" v-on:click="sort">
                 　<i v-show="sortFilterAttribute == 2" class="material-icons">arrow_drop_down</i>
                   <i v-show="sortFilterAttribute == 1" class="material-icons">arrow_drop_up</i>
                   <i v-show="sortFilterAttribute == 0" class="material-icons">sort</i>
                 </a>
               </th>`
});

new Vue({
    el: '#sortableTable',
    data: {
        headertitlelist: [
            { title: 'id', sort: 0 },
            { title: 'testA', sort: 0 },
            { title: 'testB', sort: 0 },
            { title: 'testC', sort: 0 },
            { title: 'testD', sort: 0 },
            { title: 'testE', sort: 0 }
        ]
    }
});

```
このようにVueインスタンス作成時にデータを定義してあげることで任意のヘッダーを作れるようになります。親コンポーネントから子コンポーネントへのデータの受け渡しは`props`プロパティに設定してあげることで行えます。

次はソートアイコンクリック時のイベントを定義していきます。

## ソート処理を定義
子コンポーネントからイベントを親に伝番するには`$emit`を利用します。
今回は各カラムでイベントの発生時に親コンポーネント側でソート処理をするようにしてみました。

```js:index.js
Vue.component('custom-thead', {
    props: ['headertitlelist', 'list'],
    template: `<thead>
               　<tr>
                 　<custom-th v-for="title in headertitlelist" :title="title.title" :sortFilterAttribute="title.sort" :key="title.title" v-on:sortEvent="sortColumn"></custom-th>
                 </tr>
               </thead>`,

    methods: {
        sortColumn: function (title) {
            // 子コンポーネントから受け取ったタイトルでソートKeyを指定
            let targetColumn = this.headertitlelist.filter(function (item, index) {
                if (item.title == title) return true;
            });

            if (targetColumn[0].sort == 0 || targetColumn[0].sort == 2) {
                sortAsc(this.list, title);
                targetColumn[0].sort = 1;
            } else {
                sortDesc(this.list, title);
                targetColumn[0].sort = 2;
            }
        }
    }
});

Vue.component('custom-th', {
    props: ['title', 'sortFilterAttribute'],
    template: `<th scope="col">{{ title }}
                <a href="#" v-on:click="sort">
                  <i v-show="sortFilterAttribute == 2" class="material-icons">arrow_drop_down</i>
                  <i v-show="sortFilterAttribute == 1" class="material-icons">arrow_drop_up</i>
                  <i v-show="sortFilterAttribute == 0" class="material-icons">sort</i>
                </a>
               </th>`,

    methods: {
        sort: function () {
            // 親コンポーネントのソートメソッドを呼び出す
            this.$emit('sortEvent', this.title);
        }
    }
});

```

`sortAsc`と`sortDesc`は最後にソースコード纏めて記載するときに載せます。

# カスタムのテーブルボディ
次はボディの部分です。こちらもヘッダー同様に任意のデータ（ヘッダーにあわせた）を表示させます。

```js:index.js
Vue.component('custom-tbody', {
    props: ['list'],
    template: `<tbody>
                <custom-td v-for="record in list" :record="record" :key="record.id"></custom-td>
               </tbody>`
});

Vue.component('custom-td', {
    props: ['record'],
    template: `<tr>
                <td v-for="val in record">{{ val }}</td>
               </tr>`
});

new Vue({
    el: '#about',
    data: {
        list: [
            { id: '3', testA: 'aaaaa', testB: 'bbbb', testC: 'ccc', testD: 'ddddd', testE: 'ljh' },
            { id: '4', testA: 'sdh', testB: 'bbsdfhabb', testC: 'sd', testD: 'dgla', testE: 'egve' },
            { id: '1', testA: 'bgf', testB: 'w', testC: 'hsdf', testD: 'lkh', testE: 'fsee' },
            { id: '2', testA: 'aaaaa', testB: 'bbbb', testC: 'ccc', testD: 'aa', testE: 'esde' },
            { id: '5', testA: 'bdsaa', testB: 'dbsb', testC: 'vc', testD: 'hra', testE: 'esde' }
        ]
    }
});

```

ちょっと変数名よろしくないですがこんな感じです。要領はヘッダーの時と同じです。
ここまででテーブルに必要なパーツとメソッドはすべて用意できました。

# データを取得する
今回はインスタンス作成時にデータを固定で指定していましたが、サーバからデータを取得したい場合にはAjaxリクエストで取得すればいいと思います。
サーバ側でどのようなテーブルにしたいかによって返すデータを加工する想定です。

```js:index.js
new Vue({
    el: '#about',
    data: {
        headertitlelist: [],
        list: []
    },
    created: {
      // ここでAjax使ってサーバーからデータを取得するか
      // すでに用意しているデータを変数に詰めてあげる
    }
});

```

# 最後に
ここまでのソースコードです。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ページタイトル</title>
</head>
<body>

<div id="sortableTable">
  <table>
    <custom-thead :headertitlelist="headertitlelist" :list="list"></custom-thead>
    <custom-tbody :list="list"></custom-tbody>
  <table>
</div>

</body>
</html>

```

```js:index.js
Vue.component('custom-thead', {
    props: ['headertitlelist', 'list'],
    template: `<thead>
                <tr>
                 <custom-th v-for="title in headertitlelist" :title="title.title" :sortFilterAttribute="title.sort" :key="title.title" v-on:sortEvent="sortColumn"></custom-th>
                </tr>
               </thead>`,

    methods: {
        sortColumn: function (title) {
            let targetColumn = this.headertitlelist.filter(function (item, index) {
                if (item.title == title) return true;
            });

            if (targetColumn[0].sort == 0 || targetColumn[0].sort == 2) {
                sortAsc(this.list, title);
                targetColumn[0].sort = 1;
            } else {
                sortDesc(this.list, title);
                targetColumn[0].sort = 2;
            }
        }
    }
});

Vue.component('custom-tbody', {
    props: ['list'],
    template: `<tbody>
                 <custom-td v-for="record in list" :record="record" :key="record.id"></custom-td>
               </tbody>`
});


Vue.component('custom-th', {
    props: ['title', 'sortFilterAttribute'],
    template: `<th scope="col">{{ title }}
                 <a href="#" v-on:click="sort">
                  <i v-show="sortFilterAttribute == 2" class="material-icons">arrow_drop_down</i>
                  <i v-show="sortFilterAttribute == 1" class="material-icons">arrow_drop_up</i>
                  <i v-show="sortFilterAttribute == 0" class="material-icons">sort</i>
                </a>
               </th>`,

    methods: {
        sort: function () {
            // 親コンポーネントのソートメソッドを呼び出す
            this.$emit('sortEvent', this.title);
        }
    }
});

Vue.component('custom-td', {
    props: ['record'],
    template: `<tr>
                 <td v-for="val in record">{{ val }}</td>
               </tr>`
});

new Vue({
    el: '#sortableTable',
    data: {
        headertitlelist: [
            { title: 'id', sort: 0 },
            { title: 'testA', sort: 0 },
            { title: 'testB', sort: 0 },
            { title: 'testC', sort: 0 },
            { title: 'testD', sort: 0 },
            { title: 'testE', sort: 0 }
        ],

        list: [
            { id: '3', testA: 'aaaaa', testB: 'bbbb', testC: 'ccc', testD: 'ddddd', testE: 'ljh' },
            { id: '4', testA: 'sdh', testB: 'bbsdfhabb', testC: 'sd', testD: 'dgla', testE: 'egve' },
            { id: '1', testA: 'bgf', testB: 'w', testC: 'hsdf', testD: 'lkh', testE: 'fsee' },
            { id: '2', testA: 'aaaaa', testB: 'bbbb', testC: 'ccc', testD: 'aa', testE: 'esde' },
            { id: '5', testA: 'bdsaa', testB: 'dbsb', testC: 'vc', testD: 'hra', testE: 'esde' }
        ]
    }
});

// 昇順ソート関数
function sortAsc(list, key) {
    list.sort(function (a, b) {
        let x = a[key];
        let y = b[key];
        if (x < y) return -1;
        if (x > y) return 1;
        return 0;
    });
}

// 降順ソート関数
function sortDesc(list, key) {
    list.sort(function (a, b) {
        let x = a[key];
        let y = b[key];
        if (x > y) return -1;
        if (x < y) return 1;
        return 0;
    });
}
```

短時間でここまで作れたので結構書きやすくて導入しやすいなぁとVueに対して思いました。
DOMをあまり意識しないでデータを変更すればいいからかなり楽ですね:smile:
もっといろんな題材で練習してみたいと思います。
あと、もっとシンプルに書けないか色々試してみます。