フィールド値変更イベントで気を付けること

今年もあっという間にアドベントカレンダーの季節がやってまいりましたね。
Qiitaに記事を投稿したことがなかったのですが、自身のアウトプットの**練習**と**備忘録**を兼ねて
何か記事を書けたらなということで、アドベントカレンダーに参加をしてみました。

**2022年04月05日 いただいたコメントを記事に反映いたしました。遅くなり申し訳ございません。**


## はじめに

- Qiita初投稿なのでツッコミどころが多いと思う
- 上記の通り、備忘録でもあるということ
- kintoneカスタマイズ初心者に向けた記事かも？
- たぶん既出のネタ

と、記事を書いている本人も思っていますので、ある程度手加減していただけると幸いです。

## フィールド値変更イベントとは

基本的なkintoneカスタマイズについてお話をしておくと、
サイボウズ様が用意した kintone JavaScript API を利用して、特定のイベントが動作した際に
カスタマイズ処理を動作させるのが、基本的なカスタマイズの方法だと思います。

その中でも今回は、

レコード追加画面のフィールド値変更時イベント
https://developer.cybozu.io/hc/ja/articles/201941984#step3

レコード編集画面のフィールド値変更時イベント
https://developer.cybozu.io/hc/ja/articles/202166270#step3

についての記事になります。

ざっくりと説明すると、特定のフィールドの値が変わった際に、カスタマイズを動かしたい時に使うイベントです。


## フィールド値変更イベントの使い方

例えば、ドロップダウンフィールドと文字列１行フィールドをアプリに配置し、前述のフィールド値変更時イベントのカスタマイズを記述します。
![スクリーンショット 2020-12-08 1.11.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/5418cc6d-4a03-726e-c5a6-4d84c1c8c21f.png)


```JavaScript
const watch = 'ドロップダウン';
const changeEvent = [
  `app.record.create.change.${watch}`,
  `app.record.edit.change.${watch}`
];

kintone.events.on(changeEvent, event => {
  event.record['文字列'].value = 'ヨシ！'
  return event;
});
```

そして、レコード追加（編集）画面でドロップダウンフィールドを変更すると・・
![スクリーンショット 2020-12-08 1.25.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/64ce6e3b-04aa-6386-a343-94c992443731.png)

文字列フィールドに、カスタマイズで文字が挿入されました。
基本的な使い方としてはこんな感じではないでしょうか。

それでは本題に入ります。

## 本題

基本的な使い方については先ほど説明した通りですが、ある程度カスタマイズに慣れてくると、もっと応用することもあるかと思います。

例えば、ルックアップの **「ほかのフィールドのコピー」でフィールドの値が変更された時に、フィールド値変更イベントを動作させる。** なんてことも可能ですよね。
ただし、この場合に注意しなければいけないことがある。というのが本題です。

実は、ルックアップの **「ほかのフィールドのコピー」でフィールド値変更イベントが動作した場合、ハンドラーの引数に渡されるオブジェクトの中身は、ルックアップのフィールドコピーが完了していない** ものが格納されています。

例えば、

- ルックアップフィールドを用意し「ほかのフィールドのコピー」を設定
- 「ほかのフィールドのコピー」で設定したフィールドに対して、フィールド値変更イベントのカスタマイズを用意

![本題1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/70d28ab5-9682-7a74-2208-df78f220ccf6.png)

```JavaScript
const watch = '担当者名';
const changeEvent = [
  `app.record.create.change.${watch}`,
  `app.record.edit.change.${watch}`
];

kintone.events.on(changeEvent, event => {
  console.log({ event })
  return event;
});
```


上記のカスタマイズが実行されると、コンソールに event が出力されます。
※ChromeであればF12キーを押すことで、コンソールが表示できます。

では、ルックアップの取得を行い、カスタマイズを動作させてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/5346f65c-a225-d643-71bb-bdf0d1d11717.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/a91810a6-3686-55e7-b11d-422ab669f06d.png)

**アプリの画面上**では、すべてのグレーアウトしているフィールドに値が入っていますが、
イベント発火時にコンソールに出力されたオブジェクトを見ると、ルックアップの「ほかのフィールドのコピー」が
**中途半端にしか動いていない（途中である）** ことがわかると思います。

画像の例で言うと、ルックアップの取得により**担当者名フィールドに値が入ったタイミング**で、**カスタマイズが動いている**ようで、その時点では **[郵便番号] ～ [住所] までのフィールドには、値のコピーが実施されていない** ということです。

これを知らずに、以下のようなカスタマイズを作ったとします。

- イベント発火は **[担当者名]フィールド** の**フィールド値変更イベント**
- **[日報]フィールド** に [担当者名] と **[住所]** の値を入れる

```JavaScript
const contact = '担当者名';
const address = '住所';
const changeEvent = [
  `app.record.create.change.${contact}`,
  `app.record.edit.change.${contact}`
];

kintone.events.on(changeEvent, event => {
  console.log({ event })
  event.record['日報'].value = ` 担当者名：${event.record[contact].value} 住所：${event.record[address].value}`;
  return event;
});
```

これを実行すると、前述の説明の通り、[住所]に値がコピーされる前に動作するため、正しく取得することができません。

![本題2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/33853d5a-1bbc-7d4f-12b2-253b0d31d2ef.png)

## じゃあどうするの？

どうやら、このルックアップの値コピーについては、画面上の上から順番に値のコピーを行っている（ような気がする）。
なので、カスタマイズソースを修正せずに対応する場合は [担当者名] を下に移動すること。

![本題3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/59a95147-5865-16ae-34d3-9541ef831955.png)

なので、[担当者名] を下に移動させると・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/98a9434b-3735-dbf1-85a1-c528b744a44f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/9be21787-56aa-33ef-1929-400454a0ad00.png)
引数に渡されたオブジェクトを見ると、ルックアップの**「ほかのフィールドのコピー」がすべて動いた後**に、
イベントが発火していることがわかります。（担当者名が一番最後にコピーされるはずなので。）
また、先ほどは "undefined" となってしまった [日報] に記載の住所、こちらも正しい値が格納できています。

これでほぼ解決かなと思いますが、

**[担当者] が当初の配置場所よりも、下のほうに配置することになった**

のが少し気になります。

この場合は、フィールドの配置は以前のままで、イベント発火のきっかけとなるフィールドを
[担当者名] → [住所]
に変更してあげることで解決するのですが、これはこれで
住所が入力されてないレコードをルックアップしたとき動かないじゃん。ということになりますので、どちらかの調整が必要
ということですね。

## ここまでをまとめると

- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成（配置）とイベント発火方法によっては、**値が空になっているケースがある**
**（ルックアップなどで値がコピーされてイベントが発火した場合）**
- 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する

と、いうことでしょうか。


## フィールド値変更イベントで、もう少し難しいこと（非同期処理）をやる場合は

恐らく、みんなこういうコーディングをしているんじゃないでしょうか。（思い込み）

```JavaScript
const contact = '担当者名';
const changeEvent = [
  `app.record.create.change.${contact}`,
  `app.record.edit.change.${contact}`
];

kintone.events.on(changeEvent, event => {
  setReport(); // 非同期の関数を呼び出す
  console.log({ event });
});

const setReport = async () => {
  
  /* なにかしらの非同期処理 */

  const event = kintone.app.record.get();
  event.record['日報'].value = "絶対ヨシ！";
  kintone.app.record.set(event);
}
```
フィールド値変更イベントでは、Promise が使えないので、ハンドラーでは 

```JavaScript 
return event;
```
を書かずに、関数を呼び出し、最終的に自分で kintone.app.record.set() を使ってレコードの値を変更する。

```JavaScript
kintone.app.record.set(event);
```


確かにこれで動きます。
動きますが、上記例で言うと、setReport関数がとても速く処理された場合、以下のようなエラーが出ます。
![image (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/47b68297-15bc-ac68-3b20-8c55363e0ba8.png)

エラーメッセージの通り、
> イベントハンドラの処理中は、"kintone.app.record.set()" は使えないよ。
> ※kintone.app.record.get()も同じ

とのこと。

要はこういう時にエラーが起きるんですね。
**イベントハンドラが終了する前に、setReport関数の kintone.app.record.set(event) が実行**されている。
setReport関数は 非同期関数（async）なので、イベントハンドラで呼び出した際に、setReport関数の終了を待たずに
処理が進んでいますが、イベントハンドラの終了の前に、setReport関数内の kintone.app.record.set(event) が
呼ばれると、上記のようなエラーが発生するようです。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851442/96b79b1a-8f8e-67dc-7e5c-581871a54108.png)

## ん？これって

フィールド値変更イベントは Promise に対応していない。
　↓
じゃあ、イベントハンドラで非同期関数呼び出して、イベントハンドラでは何も return しなきゃいいんじゃない？
　↓
概ねうまくいくけど、非同期関数が早く終わり過ぎると、エラーが出る。

・・・

**フィールド値変更イベントが Promise に対応するアップデートを待つ**
※サイボウズ様、よろしくお願いします。

**2022年04月05日 更新**
次項に解決できるソースコードを記載。


## 2022年04月05日追記
※すみません、いただいたコメントを記事に反映することを失念していました。遅くなり申し訳ございません。

@wv-sumichan さんからいただいたコメントの通り
以下のように呼び出した関数内で「await」記述することで、呼び出し元のイベントハンドラの終了を待ってから
後続の処理を実行させることができるようです。

```JavaScript
const contact = '担当者名';
const changeEvent = [
  `app.record.create.change.${contact}`,
  `app.record.edit.change.${contact}`
];

kintone.events.on(changeEvent, event => {
  setReport(); // 非同期の関数を呼び出す
  console.log({ event });
  return event;
});

const setReport = async () => {

  /* なにかしらの非同期処理 */
  await Promise.resolve(); // 既に完了している非同期処理の完了を待つ

  const event = kintone.app.record.get();
  event.record['日報'].value = "絶対ヨシ！";
  kintone.app.record.set(event);
}
```

または

```JavaScript
const setReport = () => {
  return new Promise((res, rej) => {
    res();
  }).then(() => {
    const event = kintone.app.record.get();
    event.record['日報'].value = "絶対ヨシ！";
    kintone.app.record.set(event);
  });
}
```

処理の順番としては、
1. イベントハンドラ内の setReport() が実行される。
1. setReport関数内の await Promise.resolve() が実行される。
1. イベントハンドラ内の console.log({ event }) が実行される。
1. イベントハンドラが終了する。
1. setReport関数内の const event = kintone.app.record.get(); 以降が実行される。

@wv-sumichan さんありがとうございました。


## まとめ


**イベントハンドラで同期処理しかないケース（先ほどの再掲）**

- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成（配置）とイベント発火方法によっては、**値が空になっているケースがある**
**（ルックアップなどで値がコピーされてイベントが発火した場合）**
- 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する

**イベントハンドラで非同期処理があるケース**

- イベントハンドラで非同期関数を呼び出す
- ~~イベントハンドラの終了よりも、呼び出した非同期関数が先に終わらないように意識する~~
- await や .then を使って、呼び出し元であるイベントハンドラの処理を先に終わらせる。
というところでしょうか。
間違っていたらごめんなさい。

## あとがき

記事を書くというのは難しいですね。
普段何気なく Qiita で自分の困りごとを解決してくれる記事を読んだりしますが
記事を投稿するのは結構大変ですね。感謝しかないです。

それと、実は私が所属する会社は CybozuDays 2020 東京に出展し、私自身もブースに立っておりました。
ブースに立ち寄ってくださったみなさま、ありがとうございました。