kintoneでスクリプト処理が完了するまでレコード全体を非表示にする

## はじめに
kintoneではレコード数やフィールド数が多くなると、JavaScriptによるスクリプト処理が終わる前にレコード画面が表示されることがあります。
この場合、setFieldShownや[フィールド].disabled=trueの処理が一瞬遅れることにより、非表示にしたいフィールドが一瞬見えてしまったり無効化したいフィールドが一瞬有効になったりしてしまいます。  

実務上はほとんど問題ないのですが、何となくキモチ悪いのでこれを防ぐ方法を探したいと思います。

【注意】
この記事ではDOM操作を行っています。
将来のkintoneのアップデートにより動作しなくなる場合がありますので、あらかじめご了承ください。



## 基本方針
はじめに書いた通り、画面表示が一瞬遅れるのはスクリプトによる処理が終わる前にレコード画面を表示してしまうからです。
それなら、最初はレコード画面を非表示にしておいてスクリプト処理が完了した時点で表示するようにすれば解決できそうです。  

想定される動作は以下の通りです。

* レコード詳細・追加・編集・印刷画面において、レコード全体の初期状態を非表示にする。
* スクリプトの処理によりレコード全体を表示する。



## レコード全体の非表示化
kintoneのレコード全体を示すdiv要素は``id="record-gaia"``となっています。
まずはこのdiv要素を非表示にします。  

と言っても、スクリプトを使って非表示にしたところで非表示にする処理自体が画面表示よりも遅れるため意味がありません。  
ここは、CSSを使って目的のdiv要素を非表示にします。

```css:desktop.css
#record-gaia
{
    visibility: hidden;
}
```



## レコード全体を表示
スクリプトを使ってレコード全体を表示させます。

```javascript:desktop.js
(function() {
    "use strict";

    kintone.events.on([
        "app.record.create.show",
        "app.record.edit.show",
        "app.record.detail.show",
        "app.record.print.show"
    ], function(event) {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            recordGaia.style.visibility = "visible";
        }

        return event;
    });
})();
```
 
<br/>これで無事、スクリプト処理が終わった時点でレコード全体が表示されるようになりましたとさ。
めでたし、めでたし。



## そうは問屋が卸さない
これだけで済めばよかったのですが、実はこのスクリプトでは問題があります。  
kintoneでは、一部のボタンについては「クリックした際に画面遷移はする（＝kintone上での画面表示イベントは発生する）がページは再読み込みされない」という動作仕様になっているようです。

これの何が問題かと言うと、前の画面を表示した際の画面表示イベントにより```visibility```が```visible```になった状態のまま次の画面を表示した際の画面表示イベントが処理される、つまり画面表示イベント発生時点で初期状態が非表示になっていないケースがあるということです。  
このままだと、それらのボタンをクリックした場合は画面表示が一瞬遅れる状態のままになってしまいます。


## ボタンクリック時のイベントを追加
この問題を解消するため、対象のボタンに対して「クリック時にレコード全体を非表示にするイベント」を追加することにします。  
今回の場合、クリックしてもページが再読み込みされないボタンは次の通りです（カッコ内はクラス名）。

* 詳細画面：レコードを編集する（gaia-argoui-app-menu-edit）
* 詳細画面：前のレコードに移動する（gaia-argoui-app-pager-prev）
* 詳細画面：次のレコードに移動する（gaia-argoui-app-pager-next）
* 編集画面：キャンセル（gaia-ui-actionmenu-cancel）
* 編集画面：保存（※下記参照）

<br/>それぞれのボタンに対して、``addEventListener``を使って「クリック時にレコード全体を非表示にするイベント」を追加します。
なお、編集画面での保存ボタンについては``app.record.edit.submit.success``イベントで代用できるため、そちらを使用します。

```javascript
function hideRecordGaia() {
    const recordGaia = document.getElementById("record-gaia");
    if (recordGaia) {
        recordGaia.style.visibility = "hidden";
    }
}

kintone.events.on([
    "app.record.edit.submit.success"
], function(event) {
    hideRecordGaia();

    return event;
});

kintone.events.on([
    "app.record.detail.show",
    "app.record.edit.show"
], function(event) {
    const classNames = {
        "detail": ["gaia-argoui-app-menu-edit", "gaia-argoui-app-pager-prev", "gaia-argoui-app-pager-next"],
        "edit": ["gaia-ui-actionmenu-cancel"]
    };
    const eventType = event.type.replace(/^app\.record\.([^.]+)\.show$/, "$1");

    classNames[eventType].forEach(function(className) {
        const elms = document.getElementsByClassName(className);
        if (elms.length === 1) {
            elms[0].addEventListener("click", hideRecordGaia, false);
        }
    });

    return event;
});
```

<br/>これで、「レコードを編集する」などのボタンをクリックした瞬間に一度```visibility```が```hidden```になり、その後スクリプト処理が完了した時点で```visibility```が```visible```になります。  

今度こそ、処理が完了するまでレコード全体を非表示にするスクリプトが完成しましたとさ。
どっとはらい。



## それでも問屋は卸さない
これで完成かと思いきや、実際に動かしてみるとレコード詳細表示画面からレコード編集画面に遷移した際にレコード全体が表示されないことがありました。  

これは何故でしょうか。
処理の動きを時系列で考えてみます。

1. レコード詳細表示画面で編集ボタンをクリック。
1. 追加したクリックイベントが発火し、```visibility```を```hidden```にする。
1. レコード編集画面での画面表示のイベントにより、```visibility```を```visible```にする。

本来はこのような順番で処理されるのが理想なのですが、レコード数やフィールド数が少ない時には2.と3.の順番が逆になる場合があることが分かりました。  
つまり、

1. レコード詳細表示画面で編集ボタンをクリック。
1. レコード編集画面での画面表示のイベントにより、```visibility```を```visible```にする。
1. 追加したクリックイベントが発火し、```visibility```を```hidden```にする。

という順番で処理される場合があるのです。 
こうなってしまうと、最終的に```visibility```が```hidden```になってしまうため、レコード全体が表示されなくなってしまいます。



## 非同期で処理を遅延
クリックイベントとkintone上での画面表示イベント、どちらが先に処理されるかはその時々で変わるため分かりません。
分からないものは仕方ないので、```visibility```を```visible```にする際に、その時点の```visibility```を取得して判定することにします。  

取得した```visibility```が```visible```の場合は、ボタンクリック時のイベントがまだ処理されていないと考えられるため```visibility```が```hidden```になるまで待機します。
```visibility```が```hidden```になった、つまりボタンクリック時のイベントが処理されたのを確認した上で```visibility```を```visible```に変更します。  

ただし、何かの手違いで```visibility```が```hidden```にならない場合を考慮して、一定回数試行した後は```visibility```の状態に関わらず```visible```に変更するようにしておきます。  

これらの処理を、setTimeoutを使って非同期ループ処理で実装します。
チェック間隔``interval``は10msec、試行回数``maxNum``は20回にしてみます。

```javascript
const interval = 10;
const maxNum = 20;

function loop(num) {
    setTimeout(function() {
        const style = window.getComputedStyle(recordGaia, null);
        if (num > maxNum || style.visibility === "hidden") {
            recordGaia.style.visibility = "visible";
        } else {
            loop(num + 1);
        }
    }, interval)
}

loop(0);
```



## 完成
ようやく完成しました。
最終的なコードは以下の通りです。

```javascript:desktop.ja
(function() {
    "use strict";

    function hideRecordGaia() {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            recordGaia.style.visibility = "hidden";
        }
    }

    kintone.events.on([
        "app.record.edit.submit.success"
    ], function(event) {
        hideRecordGaia();

        return event;
    });

    kintone.events.on([
        "app.record.detail.show",
        "app.record.edit.show"
    ], function(event) {
        const classNames = {
            "detail": ["gaia-argoui-app-menu-edit", "gaia-argoui-app-pager-prev", "gaia-argoui-app-pager-next"],
            "edit": ["gaia-ui-actionmenu-cancel"]
        };
        const eventType = event.type.replace(/^app\.record\.([^.]+)\.show$/, "$1");

        classNames[eventType].forEach(function(className) {
            const elms = document.getElementsByClassName(className);
            if (elms.length === 1) {
                elms[0].addEventListener("click", hideRecordGaia, false);
            }
        });

        return event;
    });

    kintone.events.on([
        "app.record.create.show",
        "app.record.edit.show",
        "app.record.detail.show",
        "app.record.print.show"
    ], function(event) {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            const interval = 10;
            const maxNum = 20;

            function loop(num) {
                setTimeout(function() {
                    const style = window.getComputedStyle(recordGaia, null);
                    if (num > maxNum || style.visibility === "hidden") {
                        recordGaia.style.visibility = "visible";
                    } else {
                        loop(num + 1);
                    }
                }, interval)
            }

            loop(0);
        }

        return event;
    });
})();
```

```css:desktop.css
#record-gaia
{
    visibility: hidden;
}
```



## おわりに
実際の運用ではレコード一覧画面でも同様の処理を行った方が良いですのが、一覧画面は一覧画面で色々と検討すべき点があり長くなるので本記事では割愛します。  

<br/>なお、最後に一つ注意点があります。  
今回の方法ではCSSを使ってレコード全体を非表示にしているため、当然ですがスクリプトが実行されないとレコードの中身が表示されません。
そして、kintoneは仕様上、複数登録したスクリプトのうちどれか一つでエラーが発生すると他のスクリプトの実行も停止してしまいます。  
他にもスクリプトやプラグインを登録している場合は、そちらでスクリプトエラーが発生した時にもレコードが表示されなくなってしまいますので、使用する際は注意してください。