13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularJS でモデルの変化時にフェードイン・アウトするディレクティブを作る

Posted at

勉強がてら AngularJS の簡単なディレクティブを作ってみました。

デモ

使い方

要素に app-fade 属性でモデルを指定すると、モデルの変化に連動してフェードイン・フェードアウトします。
次の例では、チェックボックスの ON/OFF に連動して span がフェードイン・フェードアウトします。

<label><input type="checkbox" ng-model="check" ng-init="check=true"> Check</label>
<span app-fade="check">Checked</span>

app-fade 属性には式も記述できます。

<label><input type="checkbox" ng-model="check2"> Check</label>
<span app-fade="!check2">Unchecked</span>

app-fade-duration 属性でフェードイン・フェードアウトのスピードを指定できます。
次の例ではゆっくりフェードイン・フェードアウトします。

<label><input type="checkbox" ng-model="slowly"> Slowly</label>
<span app-fade="slowly" app-fade-duration="1000">Checked</span>

app-fade 属性に指定したモデルを要素の内部で使用するときは注意が必要です。
次の例ではチェックボックスの value を要素の内部で表示していますが、とても不自然な動きになります。

<label><input type="checkbox" ng-model="yesno" ng-true-value="Yes" ng-false-value="No" ng-init="yesno='No'"> Yes/No</label>
<span app-fade="yesno">{{yesno}}</span>

これはフェードアウトの開始時点で既に yesno モデルの値は変更されているため・・・

  1. span の内容を変更
  2. フェードアウト
  3. フェードイン

という順番で処理されているためです。

次のように要素の内部で appFade.value を使うと自然にフェードアウト・フェードインされます。
appFade.value には app-fade 属性で指定されたモデルの値が入っています。

<label><input type="checkbox" ng-model="onoff" ng-true-value="On" ng-false-value="Off"  ng-init="onoff='Off'"> On/Off</label>
<span app-fade="onoff">{{appFade.value}}</span>

次のようにボタンのクリックでモデルに値をセットし、メッセージをフェードインさせることができます。
これだけなら ngShow と ngAnimation でもできますね。

<button class="btn btn-default" ng-click="message='Pushed Button'">Button</button>
<span app-fade="message">{{appFade.value}}</span>

上の例だと、ボタンを何回クリックしてもモデルの値は変化しないため、フェードインするのは最初の1回だけです。なのでクリックしている感がありません。

次のように app-fade にオブジェクトを指定し、ボタンのクリックでモデルをオブジェクトごと上書きすれば、ボタンをクリックする度にメッセージがフェードインします。

<button class="btn btn-default" ng-click="msg={message:'Pushed Button'}">Button</button>
<span app-fade="msg">{{appFade.value.message}}</span>

作り方

ディレクティブの全文は次の通りです。

angular.module('app', []).directive('appFade', function($timeout){

    function appFadeLink(scope, elem, attr)
    {
        var duration = attr.appFadeDuration - 0 || 200;

        scope.appFade = {
            value: scope.$eval(attr.appFade)
        };

        if (scope.appFade.value)
        {
            elem.show();
        }
        else
        {
            elem.hide();
        }

        var initializing = true;

        scope.$watch(attr.appFade, function(value){
            if (initializing)
            {
                $timeout(function(){ initializing = false });
            }
            else
            {
                elem.stop(true).fadeOut(duration).queue(function(){
                    scope.appFade = { value: value };
                    scope.$$phase || scope.$apply();
                    angular.element(this).dequeue();
                });

                if (value)
                {
                    elem.fadeIn(duration);
                }
            }
        });
    }

    return {
        restrict: 'A',
        scope: true,
        link: appFadeLink
    };
});

要点を説明します。

var duration = attr.appFadeDuration - 0 || 200;

attrs.appFadeDurationapp-fade-duration 属性の値を取り出しています。

scope.appFade = {
    value: scope.$eval(attr.appFade)
};

要素の内部で appFade.value が使えるようにスコープに追加します。

if (scope.appFade.value)
{
    elem.show();
}
else
{
    elem.hide();
}

ページの最初の表示にフェードインするのは不自然なため、最初は普通に表示させます。

var initializing = true;

scope.$watch(attr.appFade, function(value){
    if (initializing)
    {
        $timeout(function(){ initializing = false });
    }
    else
    {
        // ...
    }
});

app-fade 属性に指定された式を監視してフェードイン・フェードアウトさせているのですが、最初の1回目は無視します。

scope.$watch(attr.appFade, function(value){
    // ...
    else
    {
        elem.stop(true).fadeOut(duration).queue(function(){
            scope.appFade = { value: value };
            scope.$$phase || scope.$apply();
            angular.element(this).dequeue();
        });

        if (value)
        {
            elem.fadeIn(duration);
        }
    }
});

jQuery でフェードアウト・フェードインのアニメーションをします。

フェードアウトの完了時にスコープの appFade を更新しますが、このタイニングは AngularJS に補足されないので scope.$apply() を呼びます。

が、たまに $digest already in progress というエラーになるので scope.$$phase が空かどうかで呼び分けます。

    return {
        restrict: 'A',
        scope: true,
        link: appFadeLink
    };

ディレクティブの定義を返します。スコープに appFade プロパティを追加しているので scope: false だと親のスコープが更新されてしまいます。
scope: true なら親スコープを継承したディレクティブのスコープが作成されるので親スコープには影響しません。

別の実装

要素の内部で app-fade 属性に指定したモデルを表示させると処理順が不自然になる問題について、次のようにディレクティブのスコープで親のスコープのモデルを上書きし、親のスコープを直接監視する方法を考えましたが・・・

angular.module('app', []).directive('appFade', function($timeout){

    function appFadeLink(scope, elem, attr)
    {
        var duration = attr.appFadeDuration - 0 || 200;
        var name = attr.appFade;

        function watch()
        {
            return scope.$parent.$eval(name);
        }

        scope[name] = watch();

        if (scope[name])
        {
            elem.show();
        }
        else
        {
            elem.hide();
        }

        var initializing = true;

        scope.$watch(watch, function(value){
            if (initializing)
            {
                $timeout(function() { initializing = false });
            }
            else
            {
                elem.stop(true).fadeOut(duration).queue(function(){
                    scope[name] = value;
                    scope.$$phase || scope.$apply();
                    angular.element(this).dequeue();
                });

                if (value)
                {
                    elem.fadeIn(duration);
                }
            }
        });
    }

    return {
        restrict: 'A',
        scope: true,
        link: appFadeLink
    };
});

app-fade 属性で式を指定すると、その式の文字列でディレクティブのスコープにプロパティが作成されます。

例えば app-fade="!check2" などとすると !check2 というプロパティが追加されます。

なんとなく気持ち悪いのでこの方法は止めました。

13
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?