kintone「イベントハンドラー登録の適切なタイミングについて」考えてみた

# はじめに

書き始めて長くなり上手くまとめられないので、結論だけ知りたい方は最後の方だけお読みください。

# イベントハンドラー登録の適切なタイミング

[cybozu developer netwaork](https://developer.cybozu.io/hc/ja) 内の [kintone Tips > 開発ノウハウ](https://developer.cybozu.io/hc/ja/sections/360000188063) の
[イベントハンドラー登録の適切なタイミングについて ](https://developer.cybozu.io/hc/ja/articles/360000882123) には下記のような記述があります。少し長いですが引用します。

https://developer.cybozu.io/hc/ja/articles/360000882123#event


<details><summary>引用</summary><div>
>
>kintoneの画面表示イベントが発生するタイミング
>ブラウザが kintoneのページを読み込む時、kintone 本体のJavaScriptや、 カスタマイズによるJavaScriptは次の順番で実行されます。
>>
> 1. ブラウザがページのHTML読み込みを開始する
> 2. kintone の JavaScript API を使うための初期化が行われる
> 3. カスタマイズ JavaScript として登録されたスクリプトが読み込まれる
> 4. kintone 本体の JavaScriptによるページ初期化処理が開始される
> 5. ブラウザがページのHTMLを読み終わり、DOMContentLoadedイベント (※ 1) を発生させる
> 6. HTMLで指定されている画像などのリソース読み込みが完了した後、ブラウザはloadイベント (※ 2) を発生させる
>
>jQueryを使った場合など、慣例的なJavaScriptの書き方の一部には、5.のDOMContentLoadedイベントや 6.のloadイベントのイベントハンドラーで処理を始めるものがあります。
>
> **kintoneでは、これらのイベントよりも早い4.のタイミングで、kintone 本体の処理を始める方法を採用しています。** 
>したがって、DOMContentLoadedイベントよりも、kintoneの画面表示イベントが先に発生することがあります (※ 3)。
>
> **イベントハンドラーの登録よりもイベントが先に発生した場合、イベントハンドラーは呼び出されません。** 
>そのため、DOMContentLoadedやloadイベント内で、kintoneにイベントハンドラーを登録している場合、 それらが呼び出されないことがあります。 **具体的には、次のイベントが該当します。** 
>
>>app.record.create.show
>app.record.edit.show
> **app.record.detail.show** 
>
>これらの画面表示時のイベントを確実にハンドリングするには、3. のカスタマイズ JavaScript が読み込まれた時に 同期的に、kintone.events.on()によりイベントハンドラーを登録してください。

(*太文字部分編集)

</div></details>



私の中で理解が出来ていなくてずっと引っかかっていた点はこの部分。

> **イベントハンドラーの登録よりもイベントが先に発生した場合、イベントハンドラーは呼び出されません。** 

自分なりに読み解くと、

 **[カスタマイズ JavaScript内で行った]** イベントハンドラーの登録よりも **[kintoneの画面表示]** イベントが先に発生した場合、 **[カスタマイズ JavaScript内で行った]** イベントハンドラーは呼び出されません。

(*太文字部分追記)

それでは考察していきます。

# kintone カスタマイズ とは何か？

公式には下記となっています。

* [JavaScriptやCSSでアプリをカスタマイズする](https://help.kintone.com/k/ja/user/app_settings/js_customize.html)
* [cybozu developer network](https://developer.cybozu.io/hc/ja)

cybozu developer networkには下記の記載があります。
>![スクリーンショット 2019-10-29 12.19.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/be770698-5e63-6f0a-6b7f-c091f8e0975c.png)

今回取り上げている「イベントハンドラ〜」の話しは、
>kintoneの画面をカスタマイズ
JavaScript API で画面をカスタマイズできます

の部分。Garoonは別として、REST APIの話しも今回関係ありません。つまり、「kintoneの画面の見た目（や動き）をkintoneで提供されるJavaScript API を通して、ある程度ユーザー側で変更出来ますよ」ということ。

カスタマイズ全般だと幅広いので今回取り上げるのは **[app.record.detail.show](https://developer.cybozu.io/hc/ja/articles/201941974#step1)** イベントが発生した時の話しに限って考えてみます。

# kintone JavaScript の世界へようこそ

まずは下記サンプルコードをご覧ください。

>レコード詳細画面が表示された時にメッセージを表示します。
>
>kintone.events.on('app.record.detail.show', function(event) {
>　　alert("レコード詳細画面を開きました");
>});

 **kintone.events.on** て何よ？

上記コード、実際はkintoneカスタマイズのお作法により下記のようになります。

* [kintone JavaScript コーディングガイドライン](https://developer.cybozu.io/hc/ja/articles/201793484)

```javascript
// 始まりはIIFE (即時実行関数式)にします
// https://developer.mozilla.org/ja/docs/Glossary/IIFE
// 関数書き出しの
function(){} // これを
// さらに (ここにfunction)+() でラップする
(function(){})() // こんな感じ
// こうなる
(function(){
    kintone.events.on('app.record.detail.show', function(event) {
        alert("レコード詳細画面を開きました");
    });
})()
```

区別するとこんな感じ

```javascript

(function(){ // 通常のJavaScriptの世界
    kintone.events.on( // ここは kintone JavaScript API の世界
        'app.record.detail.show', // ここも kintone JavaScript API の世界
        function(event) { // 通常のJavaScriptの世界
        alert("レコード詳細画面を開きました"); // 通常のJavaScriptの世界
    });
})() 
```

# kintone.events.on

kintoneカスタマイズでは嫌という程 kintone.events.on を書きます。

チュートリアルの記事

* [はじめようJavaScript 第12回 kintone JavaScript カスタマイズでkintoneのデータを見てみる](https://developer.cybozu.io/hc/ja/articles/360000903686)
    * レコード詳細ページでkintoneのrecordデータ取得と表示 あたりを参照
* [第1回 kintone JavaScript APIのイジりかた](https://developer.cybozu.io/hc/ja/articles/201755040)
    * kintoneのイベント処理に触れてみる あたりを参照

[kintone.events.on](https://developer.cybozu.io/hc/ja/articles/201941954#step2) は kintoneに用意された、イベントハンドラー登録用関数です。

 **[kintone.events.on]===kintoneに用意されたイベントハンドラー登録用関数** 

# イベントハンドラー

イベントハンドラーって何？の方に向けて少しまとめておきます。

* [イベントの紹介](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Building_blocks/Events)
* [EventTarget.addEventListener()](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)
* [DOM on-イベントハンドラ](https://developer.mozilla.org/ja/docs/Web/Guide/Events/Event_handlers)
* [イベントと DOM](https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model/Events)

ちょっと混乱してきたので、言い換えると

 **[kintone.events.on]===kintoneに用意されたイベントリスナー** 

 **イベントハンドラーはただのJavaScriptで書かれた関数** 

再度[kintone.events.on]を考えてみます。

# kintone.events.on 再び

```javascript

kintone.events.on() // kintoneから提供されたただの関数(APIとも言う)

kintone.events.on(引数1, 引数2) // 引数を２つ取る

kintone.events.on('app.record.detail.show', 引数2) // 引数1にはkintoneで提供するイベント名が入る

kintone.events.on('app.record.detail.show', function(){}) // 引数2はただのJavaScriptで書いた関数。ユーザーが実装する

kintone.events.on('app.record.detail.show', function(){alert('Hi!')}) // 例えばalertを表示させるとか

```

実際に動く
![スクリーンショット 2019-10-29 14.39.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/3decfc3d-4dc5-d629-5a78-9dbfca2a372d.png)

コード
![スクリーンショット 2019-10-29 14.40.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/d19b8093-9f3d-7cba-feb6-2d186311fd7b.png)

# イベントハンドラー登録

kintoneの画面表示イベントが発生するタイミングは次の通りでした。

>
> 1. ブラウザがページのHTML読み込みを開始する
> 2. kintone の JavaScript API を使うための初期化が行われる
> 3. カスタマイズ JavaScript として登録されたスクリプトが読み込まれる
> 4. kintone 本体の JavaScriptによるページ初期化処理が開始される
> 5. ブラウザがページのHTMLを読み終わり、DOMContentLoadedイベント (※ 1) を発生させる
> 6. HTMLで指定されている画像などのリソース読み込みが完了した後、ブラウザはloadイベント (※ 2) を発生させる
>

最初、次が引っかかっていました。

> **イベントハンドラーの登録よりもイベントが先に発生した場合、イベントハンドラーは呼び出されません。** 

 **[カスタマイズ JavaScript内で行った]** イベントハンドラーの登録よりも **[kintoneの画面表示]** イベントが先に発生した場合、 **[カスタマイズ JavaScript内で行った]** イベントハンドラーは呼び出されません。

ではどこにユーザー処理の イベントハンドラー を登録すれば良いのか？ と言うと、

>これらの画面表示時のイベントを確実にハンドリングするには、3. のカスタマイズ JavaScript が読み込まれた時に 同期的に、kintone.events.on()によりイベントハンドラーを登録してください。

つまり、

 **app.record.detail.show** のイベント発生時に何か処理を入れたい時は、ユーザー作成のカスタマイズJavaScript内に同期的に(意図した順番で処理されること) **kintone.events.on()を使ってイベントハンドラーを登録する** と言うことになります。


最初のコードを少し変更して、ブラウザの開発者ツールのコンソールにログを出力するように変更します。
kintoneアプリのフィールド要素を、kintone JavaScript APIの [kintone.app.record.getFieldElement(fieldCode)](https://developer.cybozu.io/hc/ja/articles/201942014#step3) で取得してみます。

```javascript
(function(){
    kintone.events.on('app.record.detail.show', function(){console.log('Hi!')})
})()
```

![スクリーンショット 2019-10-29 16.27.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/bd6d357e-1cd1-6d80-825f-edb1c1f23080.png)



# DOMContentLoadedイベント

> 5. ブラウザがページのHTMLを読み終わり、DOMContentLoadedイベント (※ 1) を発生させる

5.のタイミングではkintoneに登録したイベントハンドラーは呼び出されないと言うことでした。

実際に試してみます。

ブラウザのDocumentオブジェクトのDOMContentLoadedイベント発生時のハンドラに登録してみます。

* [DOMContentLoaded](https://developer.mozilla.org/ja/docs/Web/Events/DOMContentLoaded)

```javascript
(function(){
    // ここでは画面の描画が終わっていないので、kintone APIでフィールド要素を取得できない。
    window.addEventListener('DOMContentLoaded', function(){console.log('1', kintone.app.record.getFieldElement('datetime'))}); // 1 null
})();

(function(){
    // ここでは画面の描画が終わっていないので、kintone APIでフィールド要素を取得できない。
    window.addEventListener('DOMContentLoaded', function(){console.log('2', kintone.app.record.getFieldElement('datetime'))}); // 2 null

    // kintone.events.on()関数内でイベントハンドラーを登録する。
    kintone.events.on('app.record.detail.show', function(){
        console.log('Hi!'); // Hi!
        // DOMContentLoadedイベントのタイミングではイベントハンドラーは呼び出されない。
        window.addEventListener('DOMContentLoaded', function(){console.log('3', kintone.app.record.getFieldElement('datetime'))}); // 何も表示されない

        // Loadイベントのタイミングではイベントハンドラーは呼び出されない。
        window.addEventListener('load', function(){console.log('4', kintone.app.record.getFieldElement('datetime'))}); // 何も表示されない又は状況により表示されるが同期的では無い

        // kintone.events.on内に登録したイベントハンドラは呼び出される。
        console.log('5', kintone.app.record.getFieldElement('datetime')); // 5 <div class="control-value-gaia value-5544356">...</div>

        // 即時関数の外で定義しているイベントハンドラも呼び出される。
        datetime(); // 6 <div class="control-value-gaia value-5544356">...</div>
    });
})();

var datetime = function(){console.log('6', kintone.app.record.getFieldElement('datetime'))};

```

開発者コンソール

```bash
1 null
2 null
Hi!
5 <div class=​"control-value-gaia value-5544356">​…​</div>​
6 <div class=​"control-value-gaia value-5544356">​…​</div>​
4 <div class=​"control-value-gaia value-5544356">​…​</div>​
```

う〜ん。４.は表示されますね。

# 事例 kintoneに独自処理を入れるボタンを設置する

良くあるカスタマイズ事例です。

この場合のパターンは下記のようになります。

1. kintoneアプリにボタン設置用のスペースフィールドを設置。フィールドコードを[button]にする。
2. kintone JavaScriptカスタマイズにて、kintone JavaScript API の [kintone.app.record.getSpaceElement(id)](https://developer.cybozu.io/hc/ja/articles/201942014#step6) 関数で、スペースフィールド[button]の要素を取得する。
3. ボタン要素をJavaScriptで作成する。
4. 取得した要素に3.で作成したボタン要素を追加する。
5. ボタンを押した時に何かする処理を入れる。

---

1. kintoneアプリにボタン設置用のスペースフィールドを設置。フィールドコードを[button]にする。
![スクリーンショット 2019-10-29 19.11.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/f9b831fa-6f3a-876a-b36d-cc6f1982f2e7.png)

## コード

```index.js
(function($){
    // 3. ボタン要素をJavaScriptで作成する。
    var objButton = function(elm){$(elm).append('<button id="kintone-event-test-button">ボタン</button>')};
    kintone.events.on('app.record.detail.show', function(){
        // 2. kintone JavaScriptカスタマイズにて、kintone JavaScript API の [kintone.app.record.getSpaceElement(id)](https://developer.cybozu.io/hc/ja/articles/201942014#step6) 関数で、スペースフィールド[button]の要素を取得する。
        var elmButton = kintone.app.record.getSpaceElement('button');
        // 4. 取得した要素に3.で作成したボタン要素を追加する。
        objButton(elmButton);
        // 5. ボタンを押した時に何かする処理を入れる。
        $('#kintone-event-test-button').on('click', function(){alert('Hi!')});
    });
})(jQuery);

```

実行結果
![スクリーンショット 2019-10-29 19.16.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/aafdbeeb-ef33-07fd-e742-4d292175b771.png)

## 説明

この事例の場合、kintone.events.on()のイベントハンドラ内で処理するのは、2.と4.と5.。
2.はkintone.events.on内のイベントハンドラでなければ、kintoneがHTMLを描画できていないのでここで取得するのが正解ですね。

# kintone.events.on をちょっと覗いてみる

[ここ](https://qiita.com/spica/items/7770c5fa70f7f5a5cedc)にあるように、kintone.events.on は書き換えが出来るようです。
（おすすめはしませんが）

ちょっとどうなっているか覗いてみたいと思います。

```javascript
var refKintoneEventsOn;
(function(){
    var referenceKintoneEventsOn = kintone.events.on; // オリジナル kintone.events.on の参照を変数にセット
    refKintoneEventsOn = kintone.events.on; // クロージャー
    var objCallbackEvents = {}; // ユーザーのcallback関数を保管

    // kintone.events.on の書き換え
    kintone.events.on = function(_events, _callback) {
        if(typeof _events === "string") {
            _events = [_events];
        }
        if(!(_events instanceof Array) || (typeof _callback !== "function")) {
            return false;
        }
        _events.forEach(function(event){
            if (objCallbackEvents[event]) {
                objCallbackEvents[event].push(_callback);
            } else {
                objCallbackEvents[event] = [_callback];
            }
        });
        console.log(objCallbackEvents);
    };
})();

(function($){
    var objButton = function(elm){$(elm).append('<button id="kintone-event-test-button">ボタン</button>')};
    // kintone.events.on('app.record.detail.show', function(myCheckEvents){
    refKintoneEventsOn('app.record.detail.show', function(myCheckEvents){ // オリジナルのkintone.events.on を使う
        console.log('function(myCheckEvents)=>', myCheckEvents);
        var elmButton = kintone.app.record.getSpaceElement('button');
        objButton(elmButton);
        $('#kintone-event-test-button').on('click', function(){alert('Hi!')});
    });
})(jQuery);

(function(){
    kintone.events.on('app.record.detail.show', function(myCheckEvents2){
        console.log('function(myCheckEvents2)=>', myCheckEvents2);
        var datetime = function(){console.log('2', kintone.app.record.getFieldElement('datetime'))};
        datetime;
    });
})();

(function(){
    kintone.events.on('app.record.index.show', function(event){
        console.log('function(event)=>', event);
    });
    refKintoneEventsOn('app.record.detail.show', function(event){
        console.log('クロージャーを使って外部からオリジナルのkintone.events.onを参照する');
        console.log(event);
    });
})();
```

デバックしてみるとイベントハンドラの登録がよく分かります。
![スクリーンショット 2019-10-30 20.00.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/5dc62c49-1914-c14a-3f37-8ace5bbf0051.png)

# 結論

* JavaScriptは奥が深い
* JavaScriptの世界を知る事でkintoneをより理解できる（気がする）


---

# 関連リンク

## kintone関連

* [イベント処理の記述方法](https://developer.cybozu.io/hc/ja/articles/201941954)
* [イベントハンドラー登録の適切なタイミングについて](https://developer.cybozu.io/hc/ja/articles/360000882123)
* [kintone.events.on をハックする](https://qiita.com/spica/items/7770c5fa70f7f5a5cedc)

## JavaScript関連

* [イベントの紹介](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Building_blocks/Events)
* [EventTarget.addEventListener()](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)
* [DOM on-イベントハンドラ](https://developer.mozilla.org/ja/docs/Web/Guide/Events/Event_handlers)
* [イベントと DOM](https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model/Events)