AngularJSでクリックする度に一定時間だけ表示される要素を作る

f:id:watass:20150722224307p:plain

最近、AngularJSが楽しすぎてフロントエンドばかり触っています。Yeoman便利すぎgrunt最強すぎ。ストレスフリーに色々試せるので、つい遊びすぎてます。
今回は小ネタですが、意外と情報がなくて困惑したのでメモとして残しておきます。
JavaScript初学者なので、他にこんな方法あるよ!とかこれは間違ってるよ!というのがあれば教えて下さい。お願いします。

やりたいこと

タイトル通りですが、デモはこんな感じ。
https://jsfiddle.net/wata727/tocuL2yd/1/

見栄えとかは気にしていないので最小限です。右下に表示されているボタンをクリックすると一秒だけ「表示中。。」と表示されます。ポイントはCSSなどで表示を制御しているわけではなく、要素そのものを非表示にしているということです。詳しくは後ほど説明します。

バージョン情報

デモはjsfiddleを使っていますが、AngularJSのバージョンがあっていればどんな環境でも同じように動かせるはずです。バージョンは以下の通り。

名前 バージョン
AngularJS 1.2.1

ng-showを使う

AngularJSの詳細な解説は他サイトにいくらでもあるはずなので譲るとして、ng-showを使うことで、$scopeにバインドされた値を元に要素の表示、非表示を切り替えることができますので、基本的にはこれを使用します。
コントローラー側でボタンがクリックされる度に$scopeの値をtrueにし、一定時間経過した時点でfalseにします。

index.html

<!-- 省略 -->
<body ng-app="constTimeElemApp">
  <div ng-controller="MainCtrl">
    <button ng-click="Click()">一定時間だけ表示する</div>
    <div ng-show="clicked">表示中。。</div>
  </div>
</body>
<!-- 省略 -->

app.js

angular.module('constTimeElemApp',[])
.controller('MainCtrl', ['$scope', function ($scope){
    $scope.Click = function () {
        $scope.clicked = true;
        var time = new Date().getTime();
        while (new Date().getTime() < time + 1000) {}
        $scope.clicked = false;
    };
}]);

確認のため、ビジーウェイトになりますが、whileで1秒間ウェイトをかけておきます。これで1秒だけ表示される要素ができた・・・と思いますが、試してみるとわかるように、実際には何も表示されません。

なぜ表示されないのか

ポイントはAngularJSが画面を更新するタイミングにあります。
AngularJSはjQueryと異なり、JavaScript側でHTML側のDOMを意識せずとも、勝手に必要なタイミングでビューを更新してくれることで、見通しのよいアプリケーション開発がしやすくなるというメリットを持っています。それにより、昨今注目を受けていますが、内部的には$scope.$apply()を実行することで画面の更新を行っています。

これは実際内部処理を確認したわけではないので推測ですが、ng-clickをトリガにClick関数が実行された後、初めて$scope.$apply()が呼び出されて画面が更新されるので、いくらClick関数内で$scope.clickedの値を更新するなり、ウェイトかけるなりしても、最終的には$scope.clicked = falseから$scope.clicked = falseで変化なし、として画面更新されるため、表示されない模様です。

ng-clickをトリガにふたつの関数を実行できれば、この問題も解決できるかもしれませんが、現状確認する限りではng-clickに対して2つの関数を指定できない模様です。まぁ普段は必要性感じませんしね・・・

$timeoutを使う

そこでAngularJSで用意されている$timeoutを使用します。app.jsを以下のように変更します。

app.js

angular.module('constTimeElemApp',[])
.controller('MainCtrl', ['$scope', '$timeout', function ($scope, $timeout){
    $scope.Click = function () {
        $scope.clicked = true;
        $timeout(function (){
            $scope.clicked = false;
        },1000);
    };
}]);

$timeoutは時間差で処理を実行する、ウェイト機能を提供するサービスです。第一引数に実行する関数を、第二引数に時間をミリ秒単位で指定します。今回は1000ミリ秒なので1秒です。
$timeoutは実行完了時、個別に$scope.$apply()を実行するので、まず$scope.clicked = trueになって表示され、画面が更新、1秒後に$timeoutによって$scope.clicked = falseになって画面が更新されるといった具合です。

これで冒頭のデモで紹介した通り、一定時間だけ表示される要素ができました。

まとめ

  • AngularJSで画面更新されるタイミングは決まっているので、関数内で$scopeの値を変更しても即座にng-showには反映されない
  • $timeoutを使用すると、時間経過後に画面更新されるのでコールバック関数を登録して実行するとよい

CSSアニメーションを何度も実行するみたいなケースに使える

今回のようなケースはある程度javascriptを触っている人であれば、いまさら感強い内容だと思いますが、個人的に割りとハマったのでメモ書きでした。
AngularJSの便利さに安心しきって、画面更新の仕組みとタイミングを把握していないとこういった過ちを犯しますね。

ちなみに、今回これが必要になったのは、ボタンクリック時にCSSアニメーションを表示する、みたいな処理で、CSSアニメーションでフェードアウトしておけば、表示自体は残らないけど、二度目のクリックをしたときに要素が残っているのでアニメーションが実行されないみたいな問題にぶちあたったためでした。
jQueryを使った方法は結構あったのですが、せっかくAngularJSを使っているのにそれはないよということで。
今回の記事のように$timeoutで更新するようにすれば、10ミリ秒後とかに値をリセットするようにすることで、クリックする度にアニメーションを描画できるようになります。

初歩的かつ小ネタ的でしたが、どなたかの参考になれば幸いです。

参考

$timeout | AngularJS 1.2 日本語リファレンス | js STUDIO
# AngularJSの日本語リファレンス。読み込むことを推奨します。
AngularJS: 自分で画面を更新したいときは $timeout サービスを使うのが良さげ | CUBE SUGAR STORAGE
# 色々探しているうちに辿り着き、$timeoutの存在を知ることができました。ありがとうございます。