ご紹介する Visual State Binding と Sequencer を使えばとってもカンタンですよ。
材料
下記の順に読み込んで下さい。
Visual State Binding
まずご紹介するのはこちら、Visual State Binding というバインディングハンドラです。
Knockout で jQuery のアニメーション効果を使う例が公式ドキュメントにありますが、使い勝手が良くないのでできれば忘れて下さい(^_^;)
Visual State Binding は次のように使います。
<p class="message" data-bind="visualState: { shown: displayMessage }">
メッセージ
</p>
$(function() {
$('.message')
.on('jqvs-init', function(e, state) {
// プロパティがバインドされた時に実行される
ko.unwrap(state.shown) ? $(this).show() : $(this).hide();
})
.on('jqvs-changed', function(e, state) {
// プロパティが変更された時に実行される
ko.unwrap(state.shown) ? $(this).stop().fadeIn() : $(this).stop().fadeOut();
// CSS-class が付与されるため次のように書いても判定できます
// (つまり CSS アニメーションを使うことも可能です)
//$(this).is('.shown') ? $(this).stop().fadeIn() : $(this).stop().fadeOut();
};
});
function ViewModel() {
var self = this;
self.displayMessage = ko.observable(true);
self.toggle = function() {
self.displayMessage(!self.displayMessage());
}
}
プロパティがバインドされたとき(初期化時)には jqvs-init
という jQuery イベントが、プロパティが変更されたときは jqvs-changed
という jQuery イベントがそれぞれ発火します。
これらを View側で ハンドリングしてアニメーション効果などで視覚的な状態を反映させます。
使うバインディングはいつも同じ。
アニメーション効果は 自由 に、その部品専用に書く。
という考え方です。
これで jQuery アニメーションは、Knockout の世界から独立していくらでも自由に書くことができるようになりました。
ただ一つ、Knockout によって動的に追加・削除されるエレメントに関しては注意が必要です。場合によっては、エフェクトロジックを次のように記述するべきです。
$(function() {
$(document)
.on('jqvs-init', '.foo', function(e, state) {
// プロパティがバインドされた時に実行される
ko.unwrap(state.shown) ? $('.foo').show() : $('.foo').hide();
})
.on('jqvs-changed', '.foo', function(e, state) {
// プロパティが変更された時に実行される
ko.unwrap(state.shown) ? $('.foo').stop().fadeIn() : $('.foo').stop().fadeOut();
};
});
これで後から追加されるエレメントも処理できるようになります。
Sequencer
jQuery のアニメーションを、いくつも連続で実行する場合に便利な方法です。冒頭の デモ のコードがこちらです。
<label>
<input type="checkbox" data-bind="checked: opened"/>
Open/Close
</label>
<div class="wrapper" id="a" data-bind="visualState: { opened: opened }">
<div class="container" id="b">
<div class="item" id="c"></div>
<div class="item" id="d"></div>
<div class="item" id="e"></div>
</div>
</div>
var $a = $('#a')
, $b = $('#b')
, $c = $('#c')
, $d = $('#d')
, $e = $('#e')
, base_duration = 'fast'
, wrapper_style = {
shown: {
width: 240,
height: 240,
padding: 20
},
hidden: {
width: 0,
height: 0,
padding: 0
}
}
, seq_open = new ko.jqvs.Sequencer([ // オープン用のアニメーションシーケンス
function(next) { $a.stop().animate(wrapper_style.shown, {complete: next}) },
function(next) { $b.stop().slideDown(base_duration, next) },
function(next) { $c.stop().fadeIn(base_duration, next) },
function(next) { $d.stop().animate({ height: 40 }, {complete: next}) },
function(next) { $d.stop().animate({ width: 160 }, {complete: next}) },
function(next) { $e.stop().slideDown(base_duration, next) }
], 'seq_group_a')
, seq_close = new ko.jqvs.Sequencer([ // クローズ用のアニメーションシーケンス
function(next) { $e.stop().slideUp(base_duration, next) },
function(next) { $d.stop().animate({ width: 0 }, next) },
function(next) { $d.animate({ height: 0 }, {completed: next}) },
function(next) { $c.stop().fadeOut(base_duration, next) },
function(next) { $b.stop().slideUp(base_duration, next) },
function(next) { $a.stop().animate(wrapper_style.hidden, {complete: next}) }
], 'seq_group_a')
;
$('.wrapper')
.on('jqvs-init', function(e, state) {
if (ko.unwrap(state.opened)) {
$a.css(wrapper_style.shown);
} else {
$a.css(wrapper_style.hidden);
$b.hide();
$c.hide();
$d.css({ width: 0, height: 0 });
$e.hide();
}
})
.on('jqvs-changed', function(e, state) {
ko.unwrap(state.opened)
? seq_open.start() // オープン実行
: seq_close.start(); // クローズ実行
});
function ViewModel() {
this.opened = ko.observable(false);
}
ko.applyBindings(new ViewModel());
書式
// 生成
var seq = new ko.jqvs.Sequencer( func_array, [group_id] );
// 最初から開始
seq.start();
// 停止
seq.stop();
// 再開
seq.resume();
第一引数 : func_array
実行したいアニメーションを関数として、配列に順番に格納して ko.jqvs.Sequencer を生成します。
逐次実行関数の引数 next
ここで受け取っている next
という引数が重要です。
next
はコールバック関数で、これを呼び出すことで次の処理が実行される仕組みになっています。
Node.js + Express.js のミドルウェアと同じような考え方です。
アニメーションの完了時コールバックに next を指定する
この例では、各アニメーション効果の終了時に次の処理を実行するため、fadeIn
や animate
の完了時に呼び出されるコールバックとして指定しています。
// 配列に挿入する関数の形式
function (next) {
// 引数 next は次のアニメーションを実行するための関数
// next(); のように呼び出すことで次の処理を実行できる。
// アニメーションの完了時に呼び出されるようにするには
// 次のようにコールバックに指定する。
$a.animate(
{ width: 100 },
{ complete: next }
);
}
第二引数 : group_id
第二引数として渡している文字列はグループIDと言って、同じグループIDを指定したシーケンサ同士をグループ化するための引数です。
グループ化
グループ化することによって、同じエレメントに対するアニメーション効果の処理を干渉させないようにすることができます。
例えば上記コードでは、オープン時のシーケンスが実行されている最中にクローズのシーケンスが開始されてしまうと、アニメーションが破綻してしまいます。
グループ化することによって、何れかのシーケンス開始時にグループ化された全てのシーケンスの実行が停止され、アニメーションが破綻しないようにできます。
当初は jQuery Promise でやろうと思ったのですが、Promise はエレメントに対してフックするタイプのためイベントによっていくつも実行するタイプのアニメーション効果では使い勝手が悪く、Sequencer を用意することにしました。通信などの非同期処理では失敗したときのフロー制御も必要となるため Promise や Deferred
が必要となりますが、直線的なアニメーションの場合は単純に羅列できたほうが書きやすいと思ったためこのような実装になりました。車輪の再開発かもしれませんが、それはそれでいいです(^_^)
以上、ご紹介した2つの機能で、Knockout でも自由なアニメーション効果を実装してみてください!