勉強がてら 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 モデルの値は変更されているため・・・
- span の内容を変更
- フェードアウト
- フェードイン
という順番で処理されているためです。
次のように要素の内部で 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.appFadeDuration
で app-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
というプロパティが追加されます。
なんとなく気持ち悪いのでこの方法は止めました。