今年もあっという間にアドベントカレンダーの季節がやってまいりましたね。
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
についての記事になります。
ざっくりと説明すると、特定のフィールドの値が変わった際に、カスタマイズを動かしたい時に使うイベントです。
フィールド値変更イベントの使い方
例えば、ドロップダウンフィールドと文字列1行フィールドをアプリに配置し、前述のフィールド値変更時イベントのカスタマイズを記述します。
const watch = 'ドロップダウン';
const changeEvent = [
`app.record.create.change.${watch}`,
`app.record.edit.change.${watch}`
];
kintone.events.on(changeEvent, event => {
event.record['文字列'].value = 'ヨシ!'
return event;
});
そして、レコード追加(編集)画面でドロップダウンフィールドを変更すると・・
文字列フィールドに、カスタマイズで文字が挿入されました。
基本的な使い方としてはこんな感じではないでしょうか。
それでは本題に入ります。
本題
基本的な使い方については先ほど説明した通りですが、ある程度カスタマイズに慣れてくると、もっと応用することもあるかと思います。
例えば、ルックアップの 「ほかのフィールドのコピー」でフィールドの値が変更された時に、フィールド値変更イベントを動作させる。 なんてことも可能ですよね。
ただし、この場合に注意しなければいけないことがある。というのが本題です。
実は、ルックアップの 「ほかのフィールドのコピー」でフィールド値変更イベントが動作した場合、ハンドラーの引数に渡されるオブジェクトの中身は、ルックアップのフィールドコピーが完了していない ものが格納されています。
例えば、
- ルックアップフィールドを用意し「ほかのフィールドのコピー」を設定
- 「ほかのフィールドのコピー」で設定したフィールドに対して、フィールド値変更イベントのカスタマイズを用意
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キーを押すことで、コンソールが表示できます。
では、ルックアップの取得を行い、カスタマイズを動作させてみましょう。
アプリの画面上では、すべてのグレーアウトしているフィールドに値が入っていますが、
イベント発火時にコンソールに出力されたオブジェクトを見ると、ルックアップの「ほかのフィールドのコピー」が
中途半端にしか動いていない(途中である) ことがわかると思います。
画像の例で言うと、ルックアップの取得により担当者名フィールドに値が入ったタイミングで、カスタマイズが動いているようで、その時点では [郵便番号] ~ [住所] までのフィールドには、値のコピーが実施されていない ということです。
これを知らずに、以下のようなカスタマイズを作ったとします。
- イベント発火は [担当者名]フィールド のフィールド値変更イベント
- [日報]フィールド に [担当者名] と [住所] の値を入れる
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;
});
これを実行すると、前述の説明の通り、[住所]に値がコピーされる前に動作するため、正しく取得することができません。
じゃあどうするの?
どうやら、このルックアップの値コピーについては、画面上の上から順番に値のコピーを行っている(ような気がする)。
なので、カスタマイズソースを修正せずに対応する場合は [担当者名] を下に移動すること。
なので、[担当者名] を下に移動させると・・
引数に渡されたオブジェクトを見ると、ルックアップの**「ほかのフィールドのコピー」がすべて動いた後**に、
イベントが発火していることがわかります。(担当者名が一番最後にコピーされるはずなので。)
また、先ほどは "undefined" となってしまった [日報] に記載の住所、こちらも正しい値が格納できています。
これでほぼ解決かなと思いますが、
[担当者] が当初の配置場所よりも、下のほうに配置することになった
のが少し気になります。
この場合は、フィールドの配置は以前のままで、イベント発火のきっかけとなるフィールドを
[担当者名] → [住所]
に変更してあげることで解決するのですが、これはこれで
住所が入力されてないレコードをルックアップしたとき動かないじゃん。ということになりますので、どちらかの調整が必要
ということですね。
ここまでをまとめると
- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成(配置)とイベント発火方法によっては、値が空になっているケースがある
(ルックアップなどで値がコピーされてイベントが発火した場合) - 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する
と、いうことでしょうか。
フィールド値変更イベントで、もう少し難しいこと(非同期処理)をやる場合は
恐らく、みんなこういうコーディングをしているんじゃないでしょうか。(思い込み)
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 が使えないので、ハンドラーでは
return event;
を書かずに、関数を呼び出し、最終的に自分で kintone.app.record.set() を使ってレコードの値を変更する。
kintone.app.record.set(event);
確かにこれで動きます。
動きますが、上記例で言うと、setReport関数がとても速く処理された場合、以下のようなエラーが出ます。
エラーメッセージの通り、
イベントハンドラの処理中は、"kintone.app.record.set()" は使えないよ。
※kintone.app.record.get()も同じ
とのこと。
要はこういう時にエラーが起きるんですね。
イベントハンドラが終了する前に、setReport関数の kintone.app.record.set(event) が実行されている。
setReport関数は 非同期関数(async)なので、イベントハンドラで呼び出した際に、setReport関数の終了を待たずに
処理が進んでいますが、イベントハンドラの終了の前に、setReport関数内の kintone.app.record.set(event) が
呼ばれると、上記のようなエラーが発生するようです。
ん?これって
フィールド値変更イベントは Promise に対応していない。
↓
じゃあ、イベントハンドラで非同期関数呼び出して、イベントハンドラでは何も return しなきゃいいんじゃない?
↓
概ねうまくいくけど、非同期関数が早く終わり過ぎると、エラーが出る。
・・・
フィールド値変更イベントが Promise に対応するアップデートを待つ
※サイボウズ様、よろしくお願いします。
2022年04月05日 更新
次項に解決できるソースコードを記載。
2022年04月05日追記
※すみません、いただいたコメントを記事に反映することを失念していました。遅くなり申し訳ございません。
@wv-sumichan さんからいただいたコメントの通り
以下のように呼び出した関数内で「await」記述することで、呼び出し元のイベントハンドラの終了を待ってから
後続の処理を実行させることができるようです。
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);
}
または
const setReport = () => {
return new Promise((res, rej) => {
res();
}).then(() => {
const event = kintone.app.record.get();
event.record['日報'].value = "絶対ヨシ!";
kintone.app.record.set(event);
});
}
処理の順番としては、
- イベントハンドラ内の setReport() が実行される。
- setReport関数内の await Promise.resolve() が実行される。
- イベントハンドラ内の console.log({ event }) が実行される。
- イベントハンドラが終了する。
- setReport関数内の const event = kintone.app.record.get(); 以降が実行される。
@wv-sumichan さんありがとうございました。
まとめ
イベントハンドラで同期処理しかないケース(先ほどの再掲)
- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成(配置)とイベント発火方法によっては、値が空になっているケースがある
(ルックアップなどで値がコピーされてイベントが発火した場合) - 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する
イベントハンドラで非同期処理があるケース
- イベントハンドラで非同期関数を呼び出す
イベントハンドラの終了よりも、呼び出した非同期関数が先に終わらないように意識する- await や .then を使って、呼び出し元であるイベントハンドラの処理を先に終わらせる。
というところでしょうか。
間違っていたらごめんなさい。
あとがき
記事を書くというのは難しいですね。
普段何気なく Qiita で自分の困りごとを解決してくれる記事を読んだりしますが
記事を投稿するのは結構大変ですね。感謝しかないです。
それと、実は私が所属する会社は CybozuDays 2020 東京に出展し、私自身もブースに立っておりました。
ブースに立ち寄ってくださったみなさま、ありがとうございました。