jquery Tabulator: 重複コードが多くなりがちだよねー

やぁねぇ。

jquery Tabulator: 重複コードが多くなりがちだよねー、との格闘

そだねー

この話、Tabulator に限らず、「jQuery な API のノリのインフラ」全部について言えることで、Cytoscape なんぞもまさに同じなんだけどさ、「最初の初期化で一気に全部のオプションを渡す」ことでしか構築できないわけね。まぁそれはそれで悪いということでもないんだけれど、なんせこういったものは「大量の定義」を渡す必要があるんで、「漸近的に」構築できる手段がないと、どうしてもストレスに感じてしまう。

ワタシの例で、まさにこれが「やだねー」:

 1 $("#my-table-5").tabulator({
 2     "columns": [
 3         {
 4             "field": "fetched",
 5             "formatter": "progress",
 6             "width": "4em",
 7         },
 8         {
 9             "title": "Actor",
10             "columns": [
11                 {
12                     "field": "v.n",
13                     "formatter": myTextFormatter,
14                     "headerFilter": "input",
15                     "width": "12em",
16                 },
17                 {
18                     "field": "v.j",
19                     "formatter": myTextFormatter,
20                     "headerFilter": "input",
21                     "width": "12em",
22                 },
23                 {
24                     "field": "v.N",
25                     "headerFilter": "input",
26                     "width": "12em",
27                 },
28                 {
29                     "field": "v.dtl.fav",
30                     "title": "Member Favorites",
31                     "headerFilter": "input",
32                     "headerFilterFunc": ">=",
33                     "headerFilterPlaceholder": ">=",
34                     "formatter": function (cell) {
35                         let v = cell.getValue();
36                         return v >= 0 ? v : "";
37                     },
38                     "width": "7em",
39                     "sorter": "number",
40                     "align": "right",
41                 },
42             ],
43         },
44         {
45             "title": "Character",
46             "columns": [
47                 {
48                     "field": "ms",
49                     "title": "Main/Supporting",
50                     "formatter": function (cell) {
51                         return cell.getValue() === 1 ? "M" : "S";
52                     },
53                     "width": "1.5em",
54                 },
55                 {
56                     "field": "c.n",
57                     "formatter": myTextFormatter,
58                     "headerFilter": "input",
59                     "width": "12em",
60                 },
61                 {
62                     "field": "c.j",
63                     "formatter": myTextFormatter,
64                     "headerFilter": "input",
65                     "width": "12em",
66                 },
67                 {
68                     "field": "c.dtl.fav",
69                     "title": "Member Favorites",
70                     "headerFilter": "input",
71                     "headerFilterFunc": ">=",
72                     "headerFilterPlaceholder": ">=",
73                     "formatter": function (cell) {
74                         let v = cell.getValue();
75                         return v >= 0 ? v : "";
76                     },
77                     "width": "7em",
78                     "sorter": "number",
79                     "align": "right",
80                 },
81             ],
82         },
83     ],
84     "layout": "fitColumns",
85     "tooltips": true,
86     "tooltipsHeader": true,
87     "selectable": true,
88     "height": "340px",
89 });

my-table-5 という id が嘘なだけで、残りは全部実際にほんとにワタシが使ってる定義。そして「-5」はおおげさでなくて、ほんとに「ほんの少しだけ、そして必ず一つは違うが基本滅茶苦茶「おんなじ」」な Tabulator テーブルが何個もある。「アプリケーション」てそんなもんでしょ。

同じフォーマッタがいるよね、無論「名前つけてまとめろや」てのは、アタシだってわかってやってた。ただ「それだけやったからといって全体の印象は良くはならない」(保守性がすごくよくなるわけではない)ので、全体的に適用できる綺麗な整理の仕方を見つけるまでは、と保留にしていた。

あとなぁ、「3.3 までは必要だったこと」が、3.4 に乗り換えたらいらなくなっちゃった、みたいなことも結構多くて、まぁインフラの進化は望むべくことだけれど、上に書いたフォーマッタやほかで書いたフォーマッタ、ソータは、いらなくなってるものもあって、その整理もしたくてな、なので余計に億劫になっておった。

そだねーに入れなかった image フォーマッタの話から

まず、こんなんを書いてた:

 1 function myImageFormatter(cell/*, formatterParams*/) {
 2     // cell - the cell component
 3     // formatterParams - parameters set for the column
 4     var v = cell.getValue();
 5     if (v) {
 6         var img = $("<img height='24px' src='" +
 7             _datmng._urlbuilder.get_image(v, "thumbnail", true) + "'/>");
 8         img.on("load", function () {
 9             cell.getRow().normalizeHeight();
10         });
11         return img;
12     }
13     return "";
14 }

_datmng はアタシのやつの固有のもんなので誰の役にもたたないよ。今したい話は2つ。

まず、この myImageFormatter を「使えなかったので繰り返しで書いちゃってる箇所があった」件。見たらすぐにわかると思うけど、中身が硬直してて、「ちょっと違うこと」をしたくなったら即アウトな作りでしょ。まずはこれをどうにかする:

 1 function myImageFormatter(cell, formatterParams) {
 2     let v = cell.getValue();
 3     let fp = formatterParams || {};
 4     if (v) {
 5         var img = $("<img height='" +
 6                     (fp.height || "24px") +
 7                     "' src='" +
 8                     _datmng._urlbuilder.get_image(
 9                         v, fp.context || "thumbnail", true) +
10                     "'/>");
11         img.on("load", function () {
12             cell.getRow().normalizeHeight();
13         })
14         return img;
15     }
16     return "";
17 }

使うほうは:

 1 //...
 2     let tab = $("my-table-4");
 3     tab.tabulator({
 4         "columns": [
 5             {
 6                 "field": "p",
 7                 "formatter": myImageFormatter,
 8                 "formatterParams": {
 9                     "height": "240px",
10                     "context": context,
11                 },
12             },
13 //...

ここまでは別にいいでしょ。で2つ目の話:

 1 function myImageFormatter(cell, formatterParams) {
 2     let v = cell.getValue();
 3     let fp = formatterParams || {};
 4     if (v) {
 5         var img = $("<img height='" +
 6                     (fp.height || "24px") +
 7                     "' src='" +
 8                     _datmng._urlbuilder.get_image(
 9                         v, fp.context || "thumbnail", true) +
10                     "'/>");
11         img.on("load", function () {
12             cell.getRow().normalizeHeight();
13         });
14         return img;
15     }
16     return "";
17 }

ハイライトした部分、これははじめてのお使い時の措置で入れたものだけれど、最近 3.4 に乗り換えたら、なんかいらなくなってるような気がするんだよね。

もっとちゃんと検証は必要なんだけれど、3.3 までで必要だった措置の結構色んなものが不要になってそうなんだわ。undefined, blank を含んでいるケースのソートが、作者によれば完全な措置はしてないようなんだけれど、なんか少し改善してんだよね。(この件への措置は自分で sorter を書くか、setData するデータをどうにかするか、mutator を書くか、などいろいろあったが、いずれにしても「何かはやらないと非常に不愉快」だった。)

というわけで、ついさっきまで 3.3 を使っていた人で、ワタシと同じように重複や冗長コードが気になりだした人は、「3.4 ではいらん措置かもしらん」を疑うところから始めると、整理がはかどるんじゃないかと思う。

Extending Extensions

まさにこのためにある。

要するに「何度も使うものを「公式な手段で」登録」しておける場所、と考えればいい。(「自分のアプリケーションの名前空間を汚すことで整理」するのではなくて、Tabulator ワールドに「オレ流」を予め登録しておける。)

ワタシの例では「おんなじフォーマッタを何度も使う」のでフォーマッタに関して:

 1 // common formatters for Tabulator
 2 Tabulator.extendExtension("format", "formatters", {
 3     // general
 4     "html_decode": function (cell) {
 5         let v = cell.getValue();
 6         if (v) {
 7             return $("<textarea/>").html(v).text();
 8         }
 9         return "";
10     },
11     "positive_number": function (cell) {
12         let v = cell.getValue();
13         return v >= 0 ? v : "";
14     },
15 
16     // application specific
17     "MAL_mainsup": function (cell) {
18         return cell.getValue() === 1 ? "M" : "S";
19     },
20     "MAL_image": function (cell, formatterParams) {
21         let v = cell.getValue();
22         let fp = formatterParams || {};
23         if (v) {
24             var img = $("<img height='" +
25                         (fp.height || "24px") +
26                         "' src='" +
27                         _datmng._urlbuilder.get_image(
28                             v, fp.context || "thumbnail", true) +
29                         "'/>");
30             return img;
31         }
32         return "";
33     },
34     "MAL_imageurl": function (cell, formatterParams) {
35         let v = cell.getValue();
36         let fp = formatterParams || {};
37         if (v) {
38             let url = _datmng._urlbuilder.get_image(
39                 v, fp.context || "thumbnail", true);
40             return url;
41         }
42         return "";
43     },
44     "MAL_aired": function (cell) {
45         let aird = cell.getValue();
46         let s = "";
47         if (aird) {
48             if (aird[0]) {
49                 s += aird[0].slice(
50                     0, "yyyy-mm".length);
51             }
52             if (aird[1]) {
53                 s += " \u2192 ";
54                 s += aird[1].slice(
55                     0, "yyyy-mm".length);
56             }
57         }
58         return s;
59     },
60 });

中身は皆が汎用で使えるようなもんはなく、ワタシのヤツでしか意味がないものたちだが、ともあれ Tabulator.extendExtension で、ほかの出来合いのフォーマッタと同じように使えるフォーマッタを追加出来る。(なお、ドキュメントに説明がある通り、tabulator.js をインクルード後でなおかつどの Tabulator テーブルもインスタンス化する前に実行しなければならない。)

使う側はほかのたとえば progress とかとおんなじノリで使える:

 1         "columns": [
 2             {
 3                 "field": "ms",
 4                 "title": "Main/Supporting",
 5                 "formatter": "MAL_mainsup",
 6                 //OLD "formatter": function (cell) {
 7                 //OLD     return cell.getValue() === 1 ? "M" : "S";
 8                 //OLD },
 9                 "width": "1.5em",
10             },

Default Options

上でやったのは「カラム定義」だが、テーブルそのものの設定も「結構アプリケーション全体ではおんなじ設定ばっか使う」ことになり、そのたんびに:

 1 //...
 2 $("#my-table-3").tabulator({
 3     // ... とても長大な "columns": {...}
 4     "layout": "fitColumns",
 5     "tooltips": true,
 6     "tooltipsHeader": true,
 7     "selectable": true,
 8     "height": "340px",
 9 });
10 $("#my-table-4").tabulator({
11     // ... とても長大な "columns": {...}
12     "layout": "fitColumns",
13     "tooltips": true,
14     "tooltipsHeader": true,
15     "selectable": true,
16     "height": "340px",
17 });
18 $("#my-table-5").tabulator({
19     // ... とても長大な "columns": {...}
20     "layout": "fitColumns",
21     "tooltips": true,
22     "tooltipsHeader": true,
23     "selectable": true,
24     "height": "340px",
25 });

こんなんやだろ、てわけである。

そう、最初に言ったとおり、「所詮は設定の辞書を渡すだけ」のその「辞書を綺麗に構築するシカケ」の欠落のために、「こうしたことをキレイにやりたきゃ自分でキレイに書け」てことなわけなんだけれど、ただ「ほんとにいつでもオレにとっておんなじならば」、デフォルトそのものを変更出来る。ワタシのケースの場合はたとえば:

 1 // default options for Tabulator
 2 $.widget("ui.tabulator", $.ui.tabulator, {
 3     options: {
 4         "variableHeight": true,
 5         "tooltips": true,
 6         "tooltipsHeader": true,
 7         "layout": "fitColumns",
 8         "selectable": true,
 9     },
10 });

あくまでもデフォルトを変えるだけなので、たとえば “selectable” に一つだけ逆らいたいならそこではこうすればいい:

1             tab.tabulator({
2                 "columns": [
3                     // ...
4                 ],
5                 "selectable": 1, // 選択は常に一つだけ可能
6                 "height": "320px",

もう一声…

Tabulator そのもので可能なのはたぶんここまで。実際最初に挙げた例の「やだねー」のとりわけこれがやなのだ:

 1                 {
 2                     "field": "v.dtl.fav",
 3                     "title": "Member Favorites",
 4                     "headerFilter": "input",
 5                     "headerFilterFunc": ">=",
 6                     "headerFilterPlaceholder": ">=",
 7                     "formatter": function (cell) {
 8                         let v = cell.getValue();
 9                         return v >= 0 ? v : "";
10                     },
11                     "width": "7em",
12                     "sorter": "number",
13                     "align": "right",
14                 },

ここの設定は無論「独立した、個々に異なる制御を行う」ものなのだが、論理的な導出から「一方がこうなら他方は普通はこうだろ」という従属関係になるものが結構多いのだ。つまり「数字なんだから右詰めっしょ、大きいほうがエラい数値ならフィルターは「これよりデカけろフィルター」っしょ、数値なんだからソータは number に決まってるっしょ」てこと。

要するにこうしたものを「まとめて論理的な思考に基づいて」漸近的に組み上げていきたいわけなのだ。

Tabulator そのものにはないんだもん、しょうがないからワタシはこんなふうにした:

 1 // common templates of column's definition for Tabulator
 2 function _mk_tabulator_coldef(keys, upd) {
 3     const templates = {
 4         "text": {
 5             "formatter": "html_decode",
 6         },
 7         "pos_num": {
 8             "formatter": "positive_number",
 9             "sorter": "number",
10             "align": "right",
11         },
12         "MAL_ms": {
13             "title": "Main/Supporting",
14             "formatter": "MAL_mainsup",
15         },
16         "filter": {
17             "headerFilter": "input",
18         },
19         "ge_filter": {
20             "headerFilter": "input",
21             "headerFilterFunc": ">=",
22             "headerFilterPlaceholder": ">=",
23         },
24     };
25     let result = {};
26     keys.forEach(function (key) {
27         result = jQuery.extend(true, result, templates[key]);
28     });
29     result = jQuery.extend(true, result, upd);
30     return result;
31 }

で、ここまでのものすべてを駆使して最初の「やだねー」はこうなった:

 1 $("#my-table-5").tabulator({
 2     "columns": [
 3         {
 4             "field": "fetched",
 5             "formatter": "progress",
 6             "width": "4em",
 7         },
 8         {
 9             "title": "Actor",
10             "columns": [
11                 _mk_tabulator_coldef(["text", "filter"], {
12                     "field": "v.n",
13                     "width": "12em",
14                 }),
15                 _mk_tabulator_coldef(["text", "filter"], {
16                     "field": "v.j",
17                     "width": "12em",
18                 }),
19                 _mk_tabulator_coldef(["text", "filter"], {
20                     "field": "v.N",
21                     "width": "12em",
22                 }),
23                 _mk_tabulator_coldef(["pos_num", "ge_filter"], {
24                     "field": "v.dtl.fav",
25                     "title": "Member Favorites",
26                     "width": "7em",
27                 }),
28             ],
29         },
30         {
31             "title": "Character",
32             "columns": [
33                 _mk_tabulator_coldef(["MAL_ms"], {
34                     "field": "ms",
35                     "width": "1.5em",
36                 }),
37                 _mk_tabulator_coldef(["text", "filter"], {
38                     "field": "c.n",
39                     "width": "12em",
40                 }),
41                 _mk_tabulator_coldef(["text", "filter"], {
42                     "field": "c.j",
43                     "width": "12em",
44                 }),
45                 _mk_tabulator_coldef(["pos_num", "ge_filter"], {
46                     "field": "c.dtl.fav",
47                     "title": "Member Favorites",
48                     "width": "7em",
49                 }),
50             ],
51         },
52     ],
53     "height": "340px",
54 });

まぁ現状はこんなんしかないと思うんだけどね。ただなぁ、「カラム定義以外」「カラム定義」「データ」を全部最初からバラバラに渡すようなインターフェイスの方がありがたかったなぁ…。しかもカラム定義は「追加」していけるような…、要するに「ビルダパターン」のインターフェイスだったらもうちっと別の解もあるのに、と思う。(そして一番最初に書いた通り、これって結構 jQuery マナーの API に共通。)



Related Posts