OutSystemsでカウントダウンタイマーを作成する

# やりたいこと
OutSystemsでスクリーン上にカウントダウンタイマーを表示します。
こんなの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/9ec645b1-bec5-5892-54db-dcd58d5caaa4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/8b1b7570-d607-33c1-d306-5e91b70ff42d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/f2a0ff50-3f3e-ab0e-d285-0fe77cc79c10.png)

# 概要
JavaScriptの`SetInterval`を利用して、1秒おきにスクリーンのexpressionに設定してあるLocal Variableを更新します。
仕組みはこれだけ。単純です。

# プログラムの構成
## Screen
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/d3657d71-c69d-0fda-49d5-d15f036c9f3d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/933f7015-c9da-2513-dca6-5451176e3ab5.png)

### Local Variable
* `InputSec`
    * Integer
    * ユーザが入力するカウントダウン秒数。
    * InputウィジェットのVariableに指定。
* `CountdownSec`
    * Integer
    * 実際のカウントダウンで更新される秒数。`InputSec`がコピーされて1秒おきに1ずつ減らされる。
    * Expressionに指定。
    * Default Value: `-1`

### Expression
* カウントダウン数値表示
    * `Substr("0" + Max(CountdownSec, 0), Length("0" + Max(CountdownSec, 0)) - 2, 2)`
    * `CountdownSec`は初期表示時`-1`なので、マイナス値を`0`に変換してから利用。
    * 何となく"00"表示にしたかったので、左に0パディングしています。
* "Finished!"表示
    * `If(CountdownSec = 0, "Finished!", "")`
    * 実行中は表示したくないため、`CountdownSec`が`0`の場合のみ表示。この条件だけだと初回表示時も「***Finished!***」などとアホくさい表示がドヤ顔でされてしまうため、`CountdownSec`を初期値`-1`にし、`0`になるのは実行完了した時だけになるようにしました。

### その他
* 細かいことですが、[Start]ボタンのEnabledに`CountdownSec <= 0`を指定することでカウントダウン実行中はボタンが使用不可となり、多重起動を防止することができます。

## Client Action
### StartOnClick
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/a16a7941-f64e-9b0a-80cf-817b34d11f04.png)
1. `InputSec`のバリデーション(1以上であること)
2. `CountdownSec`に`InputSec`をAssign
3. JavaScript呼び出し

#### JavaScript
```Countdown.js
console.log("[START]");
console.log($parameters.CountdownSec);

const timerId = setInterval(() => {
    if ($parameters.CountdownSec > 0) {
        $parameters.CountdownSec--;
        $actions.CountdownExec();
        console.log($parameters.CountdownSec);
    } else {
        clearInterval(timerId);
        console.log("[END]");
    }
}, 1000);
```
##### JavaScrip Input Parameter
* `CountdownSec`
    * Integer
    * Local Variable `InputSec`または`CountdownSec`を指定。どちらも直前のAssignで同値になっているのでどっちでもいいです。

`SetInterval`を引数`1000ms`で呼び出すことで1秒おきに処理を発生させ、その中で受け取ったパラメータ値をデクリメントしつつClient Actionを呼び出します。実際の表示用Local Variable `CountdownSec`は呼び出したClient Action `CountdownExec`の中で更新します。
パラメータ値が`0`になったら処理は終了。この時、`SetInterval`呼び出しで作成される`timer id`を使って`clearInterval`を実行します。
仕組みはこれだけ。単純です。

### CountdownExec
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189679/776a2189-e2f7-b446-6fed-216130fcd6a5.png)

Local Variable `CountdownSec`を-1するだけです。超絶単純です。

が、何故わざわざこいつをJavaScriptから呼び出すなどという面倒なことをしているのかというと、JavaScript内でアクセスできるOutSystemsの要素が限定されているためです。変数として扱えるのはInput Parameterとして与えられたもののみで直接Local Variableなどをいじることはできません。
このため、今回は不要ですが処理結果を後続処理で使いたい場合はOutput Parameterに詰めて送ってやる必要があります。

# 注意点
`SetInterval`を実行したままで画面遷移やブラウザを閉じるなどしてスクリーンが失われると、以下のエラーが発生します。

> Invalid call of the '#ClientActionName' client action of the '#ScreenName' since the latter is not currently active. This is likely due to a platform's client action being used as an event handler or in a setTimeout function. Consider removing this call by using the 'On Destroy' event of the screen/block or moving your logic to a global client action.


完全な対応策はないようですが、以下のようにForumでは何度も話題に上がっています。
[Invalid call of the 'xyz' client action of the 'Screen'. error Log](https://www.outsystems.com/forums/discussion/62678/invalid-call-of-the-xyz-client-action-of-the-screen-error-log/)
今回の場合はエラーログの言う通り`OnDestroy`で`clearInterval(timerId);`を実行してやれば解決！……すればいいのですが、この場合もブラウザやタブを閉じてしまった場合はService Centerにエラーが記録されてしまいます(`OnDestroy`は画面遷移の最後に発生するイベントなので)。こちらの対応策としてはJavaScriptの`beforeUnload`イベントなどが考えられますが、これも完全な対応策とは言い切れません。現状は無視するしか方法はないのかな、と思います。

ちなみに、これに一時停止/再開機能を付けようとすると割とかなり相当めんどくさいと思われます。悪しからずご了承ください。