List.jsのsortfunctionの仕組みを使って、複数カラムのソートを実現する

List.jsはHTMLのテーブル形式のデータを簡単にソートできますが、1カラムでのソートしかできません。List.jsのsortメソッドを使って、複数行でのソートを実現します。

#使ったもの
List.js (ver1.5)

#やってみよう
##1.HTMLテーブルを作る
前回同様、本家のHTMLを借りてきましたが、カラムが3つ以上ないと面白くないのでdeptのカラムを追加しました。

```html
<div id="users">
  <table border="1">
    <thead>
      <tr>
        <th class="sort" data-sort="name">Name</th>
        <th class="sort" data-sort="born">Born</th>
        <th class="sort" data-sort="dept">Dept</th>
      </tr>
    </thead>
    <!-- IMPORTANT, class="list" have to be at tbody -->
    <tbody class="list">
      <tr>
        <td class="name">Jonny Stromberg</td>
        <td class="born">1985</td>
        <td class="dept">sales</td>
      </tr>
      <tr>
        <td class="name">Jonas Arnklint</td>
        <td class="born">1986</td>
        <td class="dept">marketing</td>
      </tr>
      <tr>
        <td class="name">Martina Elm</td>
        <td class="born">1986</td>
        <td class="dept">sales</td>
      </tr>
      <tr>
        <td class="name">Gustaf Lindqvist</td>
        <td class="born">1983</td>
        <td class="dept">marketing</td>
      </tr>
      <tr>
        <td class="name">Jonny </td>
        <td class="born">1987</td>
        <td class="dept">tech</td>
      </tr>
    </tbody>
  </table>
</div>
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.min.js"></script>

<script>
  var options = {
    valueNames: [ 'name', 'born', 'dept' ],
  };
  var userList = new List('users', options);
</script>

```
userListを作るときに指定するvalueNamesはname, born, deptの3つに増やしています。

##2.ソートメソッドの動きを理解する
List.jsのソートメソッドには、利用者側で指定可能なオプションがいくつか用意されています。

本家のマニュアルの[sortメソッド](http://listjs.com/api/#sort) を見てみます。

```javascript
sort(valueName, {
  order: 'desc',
  alphabet: undefined,
  insensitive: true,
  sortFunction: undefined
})
```

order, alphabet, insensitive sortFunstionの4つを指定してソートをすることができるようです。カラムをクリックして呼びだされているメソッドですが、自分で呼び出すこともできます。今回は自分で呼び出す方法を使って試します。

###sortメソッドのsortFunctionを理解する
まず、sortFunctionの引数がどう作用するのかを理解しないといけません。公開されているコードを読むと、(itemA, itemB, options)が渡されるようです。
https://github.com/javve/list.js/blob/master/src/sort.js#L72


###待ち構えてみる
userListのソートメソッドを自分で呼ぶようにしてみます。

```javascript
//List.jsのソートを呼び出す部分
  function originalsort(sortorder){
    userList.sort('name', {
      order: sortorder,
      alphabet: undefined,
      insensitive: true,
      sortFunction: customsort
    });
  }
  

//ソートメソッドを実行すると呼び出される関数
  function customsort(itemA, itemB, option){
    console.log(itemA);
    console.log(itemB);
    console.log(option);
    
    return 1;
  }
```

二つの部分でできています。
originalsort関数は、userListのsortメソッドを呼び出しているだけです。今回はボタンを設置して関数を呼び出します。
customsort関数は、List.js側から呼び出されるもので、引数はList.jsのものに合わせて定義しました。

HTMLのボタンを押して、ascとdescで呼び出してみます。

```HTML
<button onclick="javascript:originalsort('asc');">ASC</button>
<button onclick="javascript:originalsort('desc');">DESC</button><br>
```

###実行してみる
ボタンを押してconsoleを見ると、このようなログがでます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/388e735f-f26c-f905-d4e7-f5c257cb2b13.png)


1. itemAとitemBはListの中のItemであり、比較対象の2つあること
2. itemの_valuesにはitemの要素がmapされていること
3. optionsは、userList.sort()で呼び出したものをそのまま受け取れること


が分かります。

##3.ソートを実現する
###値を取り出す
ここまでくると値の取り方が分かってきます。List.jsのコードではこんな感じで受け取っています。
https://github.com/javve/list.js/blob/master/src/sort.js#L87

```javascript
itemA.values()[options.valueName]
```

1. itemA.values()で全データを取り出し
2. ソートしたいカラム名をoptionsから取り出し
3. itemAのソート対象のカラムの値を取り出します。

今回は複数のカラムでソートをするので、optionsで取れる名前は使わずにname, born, deptの3つの値を全部取り出して比較をします。values()を使えば簡単に取れます。

```javascript

  function customsort(itemA, itemB, option){
    console.count("sort count");
    n1 = itemA.values()['name'];
    n2 = itemB.values()['name'];
    b1 = itemA.values()['born'];
    b2 = itemB.values()['born'];
    d1 = itemA.values()['dept'];
    d2 = itemB.values()['dept'];

    resultn = userList.utils.naturalSort(n1,n2);
    resultb = userList.utils.naturalSort(b1,b2);
    resultd = userList.utils.naturalSort(d1,d2);
    
    console.log('Name a=%s, b=%s, result=%s',n1,n2,resultn);
    console.log('Born a=%s, b=%s, result=%s',b1,b2,resultb);
    console.log('Dept a=%s, b=%s, result=%s',d1,d2,resultd);

    ans =0;
    if( resultb != 0 ){
      ans = resultb;
    }else if ( resultd !=0){
      ans = resultd;
    }else{
      ans = resultn;
    }
    console.log('Sort answer =%s', ans);
    return ans;
  }
```

野暮ったいコードですが、ポイントは2点です。
取り出した値をどうやって比較するか？
ソートのソート優先度をどう実現するか？

これを考慮した上で、比較結果を値として返します。

###比較をする
List.jsのインスタンスには、utilsの中にnaturalSortという関数が隠されているので、それをそのまま使いました。値が同じ場合には0が返ってきます。name, born, deptの比較結果を全部受け取っておきます。

n1とn2は文字列の比較をする必要があり、b1とb2は数値として比較する必要があるなど、ソートの要件が細かい場合には、naturalSortに変わる関数を呼び出せばよいです。数値を数値として比較する、金額のようなカンマ含みの数値を比較するようなことをしたい場合には、naturalSortに渡す前に値を整えてあげればよいです。


###比較結果を返す
itemAとitemBを比較した結果をcustomsort関数の結果としてreturnします。どういう基準で値を返すのかは実装者に任されます。itemAとitemBの全ての値が渡されるので自由に使って判断します。Aが大きいときは正の数値、Aが小さいときは負の数値を返すようにするのがルールです。

２つのItemの大きさを判定するだけでよいです。ASCとDESCは気にする必要はありません。List.js側で正しく処理されます。

今回はソート優先度はborn, dept, nameとしています。
1. born同士の比較結果が0でない（同じでない）場合には、bornのソートの結果とし、
2. bornが同じときには、dept同士の比較結果を返し、
3. bornもdeptも同じ場合には、nameの結果を返しています。

```javascript

    if( resultb != 0 ){
      ans = resultb;
    }else if ( resultd !=0){
      ans = resultd;
    }else{
      ans = resultn;
    }
```
この順序は残念ながらコード上にハードコーディングすることで実現しています。

##4.実行する
では、HTML上に置いたボタンを押してテーブルをソートしてみましょう。
nameの列が反応しているのは、originalsort関数にあるuserList.sortの第一引数にnameを指定しているからです。オリジナルのソート関数を差し込んだ場合でも、List.jsの機能の良い部分は享受できるようです。▲マークが不要な場合には、sortの第一引数を""にすればよいです。

カスタムソートを使った場合でも、カラムクリックでのソートは使える状態のままです。ただし、カラムをクリックしたときには複数カラムでのソートではなく、今まで通りの単一カラムでソートになります。

初期表示
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/3f8614dc-f174-1b45-aff9-1e6ea0a82316.png)

**ASC**ボタンを押した画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/8eb6163e-14e3-a144-6631-7fa664666bc0.png)

**DESC**ボタンを押した画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/bca8cfcb-4ccc-90b1-5184-4d41ab32bb1c.png)


consoleのログ画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/98cabe85-c525-505f-3a00-bb89bcaf1c0f.png)

##やってみて気が付いた改善が必要な個所
ASCでソートすると3カラムすべてASC、DESCだとすべてがDESCになってる。これはあまり実用的ではない気がする（salaryとdeptはASC/DESCするけど、名前はABCの順のほうが分かりやすい、とか）。

#まとめ
List.jsのsortメソッドのカスタムファンクションを使って、複数のカラムでソート順できるようにカスタマイズする方法を紹介しました。コード内にカラムの優先度がハードコーディングされてしまうのが残念なところです。
次回は、カラムの優先度をcustomsort関数内に書かずにもうちょい自由度を上げるところ、複数カラムでソートしたときも、ソートしているカラムにソートマークが付けられるように実験をしてみます。