jQuery UI Dialog と Tabulator と「そのインスタンス共有」でどツボる (clearData が効かないよー)、の別解

jQuery UI Dialog と Tabulator と「そのインスタンス共有」でどツボる (clearData が効かないよー)」を書いた後にはすぐ気付いていたことなのではあるけれども。

jQuery UI Dialog と Tabulator と「そのインスタンス共有」でどツボる (clearData が効かないよー)」ではとにかく「dialog のインスタンス一個を使いまわす」という考え方で「自爆」してるネタ、なわけだね、言ってみれば。(アレで避けていたのは「dialog1、dialog2 みたいに html にベタ書きで2つ複製で書くこと、だけ。)

で、似たようなことがもうアレっきりならそれまでのことだったんだけれど、これ以外の場所で、「全く同じ弱り方をするハズのこと」をしたくなった。つまり、ワタシのアプリケーションの場合は「アニメノード、キャラクターノード右クリックで Tabulator を抱え込んだ dialog を起動」したくなったのであった。となればこれは「おそらく同じアプローチでは地獄に落ちる」ことが容易に想像出来るわけである。どう考えたって「都度専用のインスタンスを立ち上げる」方がわかりやすいし保守も簡単なはずだ、というわけだ。

ただこのことはすぐに思いついたんだけれど、改造が大変そうな気がしてたのと、抜本的なところの理解不足による不安から、ちょっと優先度下げて、ほかの優先度の高いことを先にやってた。でやっとこれの順番が巡ってきたので、さぁやるか、と。

一応おさらい。作りたい UI はこんな:

上画像の「1」ボタンで「2」のダイアログが起動、「3」で検索すると、Tabulator 表に検索結果が入り、「4」のように選択することで、「5」に入力として(IDが)流し込まれる、という流れ。

やってみればそう大変な改造ではなかったんだけれど、その「抜本的な理解不足」の件は未解決のまま。これはコード引用の後で言うことにして、まずはコード:

本来 (html の) id もダイナミックに構築すべきだがまだサボってる
  1 <html>
  2 <head jang="ja">
  3 <meta charset="UTF-8">
  4 <link rel="stylesheet"
  5  href="https://use.fontawesome.com/releases/v5.0.6/css/all.css">
  6 <link rel="stylesheet"
  7  href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/3.3.2/css/tabulator_site.min.css">
  8 <link rel="stylesheet"
  9  href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
 10 <!-- ... -->
 11 <script type="text/javascript"
 12  src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js">
 13 </script>
 14 <!-- ... -->
 15 <script type="text/javascript"
 16  src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js">
 17 </script>
 18 <script type="text/javascript"
 19  src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/3.3.2/js/tabulator.min.js">
 20 </script>
 21 <!-- ... -->
 22 </head>
 23 <body>
 24 
 25 <!-- 元はここに…(これを今回の解ではダイナミックに作る)
 26 <div id="filters-producer-list" class="dialog"
 27 title="Producers, Studios">
 28 <input id="search-producers" type="text"/>
 29 <div id="producers-list"></div>
 30 </div>
 31 --->
 32 
 33 <!-- ... -->
 34 <button type="submit" id="open-filters-producer-incl-list" class="open-filters-producer-list">
 35 <i class="fas fa-th-list fa-1x" aria-hidden="true" title="from list"></i>
 36 </button>
 37 <button type="submit" id="open-filters-producer-excl-list" class="open-filters-producer-list">
 38 <i class="fas fa-th-list fa-1x" aria-hidden="true" title="from list"></i>
 39 </button>
 40 <!-- ... -->
 41 
 42 <script>
 43 // ...
 44 $(".open-filters-producer-list").on("click", function () {
 45     var triggered = $(this).attr("id");
 46 
 47     $(`
 48     <div id="filters-producer-list" class="dialog"
 49     title="Producers, Studios">
 50     <input id="search-producers" type="text"/>
 51     <div id="producers-list"></div>
 52     </div>
 53     `).appendTo("body");
 54 
 55     $("#filters-producer-list").dialog({
 56         width: 500,
 57         height: 420,
 58         position: {
 59             at: "right-30% bottom-35%",
 60         },
 61         open: function (/*ev, ui*/) {
 62             $("#search-producers").val("");
 63             // ====================================================
 64             $("#search-producers").on("change", function () {
 65                 var find_what = $(this).val().toLowerCase();
 66                 if (find_what) {
 67                     $("#producers-list").tabulator(
 68                         "setData",
 69                         _PRODUCERS.filter(function (d) {
 70                             return (d["name"].toLowerCase().indexOf(
 71                                 find_what) >= 0);
 72                         }));
 73                 }
 74             });
 75             $("#producers-list").tabulator({
 76                 "columns": [
 77                     {
 78                         "field": "id",
 79                         "title": "id",
 80                         "headerFilter": "input",
 81                     },
 82                     {
 83                         "field": "name",
 84                         "title": "name",
 85                         "headerFilter": "input",
 86                     },
 87                 ],
 88                 "height": "300px",
 89                 "layout": "fitData",
 90                 "columnMinWidth": 80,
 91                 "selectable": true,
 92                 "data": [],
 93                 "rowSelectionChanged": function (data/*, rows*/) {
 94                     var target = null;
 95                     if (triggered === "open-filters-producer-incl-list") {
 96                         target = $("#producers-incl");
 97                     } else {
 98                         target = $("#producers-excl");
 99                     }
100                     var origlist = target.val().split(/\s*,\s*/);
101                     var newlist = [];
102                     origlist.forEach(function (e) {
103                         if (e.trim()) {
104                             newlist.push(e.trim());
105                         }
106                     });
107                     data.forEach(function (d) {
108                         if (!newlist.includes(d["id"])) {
109                             newlist.push(d["id"]);
110                         }
111                     });
112                     target.val(newlist.join(", "));
113                     target.trigger("change");
114                 },
115             });
116             // ====================================================
117         },
118         close: function (/*ev, ui*/) {
119             $("#producers-list").remove();
120             $(this).remove();
121         },
122     });
123 });
124 // ...
125 </script>
126 </body>
127 </html>

これで気付いた backtick でマルチラインテキストを使って喜んでるのはまぁ本題ではないけれどとにかく「やっと活用できたぜ」な部分だったりはする。で、jquery の appendTo でダイナミックに body に挿入している、と。ここまでは別になんてことはない。

「改造で面倒だなぁ」言うのは当然「その dialog の子たち」も全部「そのタイミングで」構築せねばならんわけなので、呼び出しタイミングが(中身が完全に同じでも)全部違ってくるわけだね。あぁなんか大変そうだなぁ、と思ったけど、まぁこれは単に入れ子にしちゃえばいいだけのことで、全然大変ではなかった。(引越しするブロックが大きいので苦痛は苦痛なんだけれど、大変、ではない。)

さて。その「抜本的なところの理解不足」の話。つまり「これで大丈夫なのかしら」という不安のまま、「でも一応期待の振る舞いはしてくれてる」というヤツの話。つまり「インスタンスの破棄」問題に関する話。ワタシの上のコードでは dialog の close ハンドラに書いてる部分ね。

Tabulator のインスタンスを「$("#producers-list").remove();」しないとオカシな振る舞いになることは確認済み。なのでこれは「入れて良さそう」ということにはなるわけなんだけれど、何がわかってないかというと、「これ、インスタンスの破棄にちゃんとなってるの?」てこと。何を調べればわかるんだろうなぁこれ。つまり…たぶん C++ を例にするのがわかりやすいね、こういうこと:

1 std::vector<MyClass*> v;
2 v.push_back(new MyClass("a"));
3 v.push_back(new MyClass("b"));

この vector 内のアイテムは、リストアイテムを除去「したからといって、MyClass インスタンスは破棄されない」ということ。いわゆるガーベージコレクタを内蔵した言語の場合はこうしたことが起こった場合には「誰も参照しなくなった宙ぶらりんのインスタンス」を検知していつかは破棄されるんだけれど、それがない C++ では「迷子のまま生き残る」というわけだね。(そうしたことが起こらないようにスマートポインタを使ったり、「起こらないように注意深くコーディングする」。)

上のワタシの javascript の例では、body ノードから filters-producer-list ノードを除去することで「破棄されている」…、という振る舞いには見えないから悩んでる。少なくとも producers-list ノード(Tabulator) も「除去」しないと2度目以降の振る舞いがおかしいので、ということなら「子は破棄されてない」ということになる…のですか? だとしても腑に落ちないよね、なら「producers-list ノード(Tabulator) も「除去」」も「破棄」にはならんのでは?

というのが「わかってない根本的な話」。ただ今のところ「なんかそれっぺー」期待のものには「なってると思しい」ようなので、この抜本的な話については考えるのは保留。いつか何か優秀な文書を見つけるに違いない。あるいは本屋に行って立ち読みでもしてこようかな…。