しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Knockout.js の paging extender を使ってページングを実装する

Knockout.js で項目のページングを行う方法として、公式サンプルにある simpleGrid を使う方法があります。

f:id:shiba-yan:20141104014702p:plain

Knockout : Paged grid

名前からわかるように、ページング用と言うよりも簡単に GridView を作るための ViewModel になります。*1

もうちょっと汎用的に使えるページング用の実装が欲しかったので調べていたら、JSFiddle で paging extender と言うものを見つけました。

extender を使って observableArray にページング用のメソッドやプロパティを追加するので、結構スマートに書けているし実装が分離出来ていていい感じです。

ちょっとごちゃっとしてるので、最低限のコードだけ引っ張ってきました。

<ul data-bind="foreach: tweets.currentPageData">
  <li data-bind="text: status"></li>
</ul>

<button data-bind="click: tweets.movePrevious">prev</button>
<button data-bind="click: tweets.moveNext">next</button>

<script>
var viewModel = {
  tweets: ko.observableArray([...]).extend({ paging: 10 });
};

ko.applyBindings(viewModel);
<script>

疑似的なコードですが、こんな感じでページングの処理を簡単に書けます。

元になるコレクションには全てのデータを読み込んでおく必要がありますが、Ajax を使って非同期に読み込めば問題ないかと思ってます。ViewModel より extender の方が良い感じに分離出来て便利そうです。

*1:3.2 では Component を使った方が上手く書けるかも

Knockout.js 3.2.0 がリリースされたので新機能について一通り調べてみた

最近はあまり Knockout.js についてブログを書いてませんが、超特大なアップデートを含む 3.2.0 がリリースされたので、新機能について調べておきました。

Knockout 3.2.0 released - Steve Sanderson’s blog - As seen on YouTube™

それぞれの新機能について紹介していきます。

Components

Components は個人的には ASP.NET のサーバーコントロールとか MVC の部分ビューが近い気がしてます。HTML と ViewModel というペアを 1 つのコンポーネントとして扱えるようになるので、今まで以上に簡単に再利用可能なパーツとして定義できます。

サンプルとして、はてなブックマークのエントリ情報を読み込むようなコンポーネントを作ってみました。

レンダリングするコンポーネントは、新しく追加された component バインディングを使って指定できます。

params の url に表示したい URL を指定すると、それに関連付いたはてなブックマークのエントリ情報とブックマークコメントを表示するようになっています。サイドバーなどのウィジェットやソーシャルパーツの共通化に力を発揮すると思います。

Custom elements

前の Components では component バインディングを使ってコンポーネントをレンダリングするようにしましたが、独自の要素を使ってレンダリングを行うことも出来るようになりました。

書き方的にはより ASP.NET のサーバーコントロールっぽくなったと思います。コンポーネントに渡すパラメーターは params 属性に書けるので、component バインディングを使うよりもシンプルです。

Pure computeds

多分、新機能の中で一番分かりにくいと思うのが、この Pure computeds だと思います。とりあえず、簡単なサンプルを用意してみたので試してみると分かりやすいかもしれません。

具体的な挙動の違いとしては ko.pureComputed を使った場合は値の更新が行われていませんが、ko.computed を使った場合は値の更新が行われています。

そして、ko.computed を使った部分は以下のようなコードになります。

self.tempFullName = ko.computed(function () {
    var value = self.firstName() + " " + self.lastName();
    
    self.fullName(value);
    
    return value;
});
self.tempFullNamePure = ko.pureComputed(function () {
    var value = self.firstName() + " " + self.lastName();
        
    self.fullNamePure(value);
        
    return value;
});

今回のコードは computed の内部で別のプロパティに対して値の更新を行うようにしています。あえて tempFullNamePure は DOM にバインドしないようにしていますが、バインドしていない場合には引数で指定した関数が実行されません。

関数が呼び出される回数が削減されるのと、依存関係の解決がシンプルになるので CPU 使用率を削減し、メモリリークを改善できるというからくりのようです。

textInput binding

これまでテキストボックスやテキストエリアに対しては value バインディングを使っていましたが、value バインディングはデフォルトではフォーカスが失われたタイミングでのみ更新されていました。

しかし、テキストボックスではキーを押したタイミングやコピー&ペーストをした時でも、リアルタイムに値を反映したいことが割とあります。そういったケースには value バインディングではなく textInput バインディングを使うことで簡単に解決できるようになりました。

Twitter のような入力可能文字数を表示するインターフェースなども、textInput バインディングと ko.computed を使うことで簡単に実装出来ますね。

今回の目玉はやはり Components だと思っていましたが、地味に textInput バインディングも使い勝手が良い機能だと思いました。

Knockout.js と ASP.NET MVC の組み合わせ時にモデルバインディングが腐る件

今日も Knockout.js を使って ASP.NET MVC に POST するようなコードを書いていたのですが、モデルバインダが意図したとおりに値をバインドしてくれない時がありました。

まずは ASP.NET MVC 側のコードを見ていきましょう。難しいことはしてません。

public class ChildFormModel
{
    public string Name { get; set; }
}

public class FormModel
{
    public string Name { get; set; }
    public List<ChildFormModel> Items { get; set; }
}

[HttpPost]
public ActionResult Post(FormModel model)
{
    return Json(true);
}

簡単にコードを紹介すると、親のモデルがさらにクラスのリストを持っている形です。

それでは次は Knockout.js 側のコードを見ていきます。

var viewModel = {
    Name: ko.observable("foo"),
    Items: ko.observableArray([
        { Name: ko.observable("bar") }
    ])
};

function saveChange() {
    $.ajax({
        type: "POST",
        url: "/Home/Post",
        data: ko.toJS(viewModel)
    });
}

めんどくさかったので初期データを横着しました。ちなみに Knockout.js の Observable なモデルを普通の JavaScript オブジェクトにするためには ko.toJS メソッドを使います。

これで実際に saveChange メソッドを実行すると、Visual Studio 側でネストされた ChildFormModel の Name プロパティに値がセットされていないことがわかります。

f:id:shiba-yan:20140625211118p:plain

何故動作しないかを調べたら、POST されたデータが以下のようになっていました。

Name:foo
Items[0][Name]:bar

ASP.NET MVC のモデルバインダは Items[0].Name という形式でないと正しくバインディング出来ないようになっているので、jQuery が内部的に変換するために使っているメソッドが原因といえます。

どうやって回避するかですが、わざわざ application/x-www-form-urlencoded 形式でエンコードしなくても、ASP.NET MVC は JSON からもバインディング出来るので、難しく考えずに JSON で送ってしまいます。

var viewModel = {
    Name: ko.observable("foo"),
    Items: ko.observableArray([
        { Name: ko.observable("bar") }
    ])
};

function saveChange() {
    $.ajax({
        type: "POST",
        url: "/Home/Post",
        contentType: "application/json",
        data: ko.toJSON(viewModel)
    });
}

Knockout.js には ko.toJSON という Observable なモデルを JSON にしてくれるメソッドがあるので、簡単に JSON を送るように変更できます。

f:id:shiba-yan:20140625212624p:plain

実際に動かしてみると、今度はちゃんと値がセットされていることがわかります。モデルが複雑な場合には JSON を使うのが無難ですね。

Knockout.js の if と visible は似ているようで違うので注意

Knockout.js の if と visible は見た目的には要素の表示・非表示を切り替えるので大差ないですが、DOM 的には大きく仕組みが異なっています。

簡単な例で挙動の差を見ていきます。

<span data-bind="if: flag">ほげほげ</span>
↓ タグの中身が空っぽの状態になる
<span></span>

if の場合は flag が true になったタイミングで、Knockout.js が初回読み込み時に複製しておいたノードを span の下に追加するようになっています。

<span data-bind="visible: flag">ほげほげ</span>
↓ 要素はそのままだけど、スタイルで非表示にしている
<span style="display: none;">ほげほげ</span>

visible の場合は flag が true になったタイミングで、Knockout.js が display: none の指定を削除します。

まとめると実行時にノードを追加・削除するのか、それとも表示・非表示を CSS で切り替えるかという差があるということになります。if を使うと DOM ツリーに存在しないタイミングが発生するのと、ノードを複製して保持しているのでコンテンツ次第ではメモリ使用量に差が出そうです。

<!-- ノードを複製しているのでメモリを少し食いそう -->
<div data-bind="if: flag">
<!-- とても長い HTML -->
</div>

<!-- こっちのが省メモリ、多分 -->
<div data-bind="visible: flag">
<!-- とても長い HTML -->
</div>

割と現実的な問題として、foreach の afterAdd を使っている場合にちょっとした差異が発生します。

ありがちな項目を追加したタイミングでアニメーションを行う実装を例に見ていきます。

<div data-bind="if: items().length > 0">
    <ul data-bind="foreach:{ data: items, afterAdd: fadeIn }">
        <li data-bind="text: $data"></li>
    </ul>
</div>

<div data-bind="visible: items().length > 0">
    <ul data-bind="foreach: { data: items, afterAdd: fadeIn }">
        <li data-bind="text: $data"></li>
    </ul>
</div>

HTML 的には if か visible を使っているという差しかありませんが、実行してみると初回に差が発生します。

if を使った方では初回にフェードインアニメーションが動作しませんでした。どうやら if を使った場合には初回に afterAdd が発火されないようなので、afterAdd を使う場合には visible で制御するようにしましょう。

ちなみに F12 開発ツールで見た時の DOM ツリーは以下のような感じです。

f:id:shiba-yan:20140624182938p:plain

しかし、if の場合には仮想要素を使ってフローを書くことが出来るので、使い分けが重要かなと思います。

<!-- ko if: flag -->
<p>ほげほげ</p>
<!-- /ko -->
↓ コメントのみ残る
<!-- ko if: flag -->
<!-- /ko -->

この場合 p タグにマージンやパディングなどのスタイルが指定されていても、要素自体が消えるので変な空白などが残らなくなります。

Knockout.js の Mapping プラグインを使ったら凄く捗った件

めっちゃ久しぶりに Knockout.js の話です。最近、仕事で再び使い始めたので Tips 的なものが貯まってきたら、こうやってちょいちょい書いていきます。

Knockout : Home

ASP.NET MVC 側で持っているモデルをそのまま JSON にして Knockout.js に流し込みたかったのですが、いちいち JavaScript 側でも同じ構造を持ったモデルを作るのは非常に面倒なので、今回は Knockout.js に用意されている Mapping プラグインを使ってみました。

Mapping プラグインのドキュメントは公式サイトで提供されています。

Knockout : Mapping

プラグイン本体は GitHub からダウンロードできます。Knockout.js の最新バージョンは 3.1.0 なので互換性を心配しましたが、今のところは特に問題なく使えています。

knockout.mapping/build/output at master · SteveSanderson/knockout.mapping · GitHub

ダウンロードしてきたプラグインへの参照を HTML に追加するだけで準備完了です。*1

基本的な使い方

恐らく一番簡単な使い方としては、ko.mapping.fromJS メソッドを使うことです。

// 元になる JavaScript オブジェクト
var obj = {
    foo: 12345,
    bar: 'string',
    baz: [ 1, 2, 3, 4, 5 ]
};

var viewModel = ko.mapping.fromJS(obj);

たったこれだけのコードで以下のようなトラッキング可能なモデルが生成されます。

// ko.mapping.fromJS を使うと以下のような VM が自動生成される
var viewModel = {
    foo: ko.observable(12345),
    bar: ko.observable('string'),
    baz: ko.observableArray([ 1, 2, 3, 4, 5 ])
};

スカラー型の場合には ko.observable が、配列の場合には ko.observableArray が使われるので、多くの場合で手直しなどが必要ありません。

この例では JavaScript オブジェクトを渡していますが、fromJSON メソッドを使うと JSON をそのまま渡すことも出来ます。

// JSON 化したオブジェクト
var json = '{"foo":12345,"bar":"string","baz":[1,2,3,4,5]}';

// fromJS メソッドを使った結果と同じ
var viewModel = ko.mapping.fromJSON(json);

JSON から直接モデルを生成させることが出来るので、ASP.NET MVC から使う場合には以下のように JSON 化して渡しました。

// Json ヘルパーを使って JSON にエンコード
var json = '@Html.Raw(Json.Encode(model))';

var viewModel = ko.mapping.fromJSON(json);

注意点としては Html.Raw ヘルパーを使わないと HTML エンコードされてエラーになるという点ぐらいでしょうか。これで自由に操作可能なモデルを、わざわざ手動で定義することなく作成できました。

ちなみに Observable を使っているモデルから、普通の JavaScript オブジェクトや JSON に変換したい場合には、toJS / toJSON メソッドを使えば簡単に実現できます。

// observable を外して普通のオブジェクトに変換
var obj = ko.mapping.toJS(viewModel);

// 同じく JSON に変換
var json = ko.mapping.toJSON(viewModel);

クライアントサイドでリストの項目を動的に追加、削除したりするケースで非常に力を発揮してくれました。

マッピング設定を追加

JavaScript のオブジェクトや JSON をそのまま Observable なモデルに変換したいだけであれば問題無いのですが、マッピング時にモデルの値を操作したい場合がちょいちょい出てきます。

具体的な例を挙げると日付の扱いです。JSON は日付が定義されていないので、そのままでは文字列表現になってしまったり、ASP.NET というか JSON.NET をそのまま使った場合では "/Date(1198908717056)/" という形式になったりとバラバラです。

そのあたりの経緯は以下の記事を参照してください。

Tales from the Evil Empire - Dates and JSON
James Newton-King - Good (Date)Times with Json.NET

JSON 上は文字列であっても、モデルとしては日付として扱いたいので、Mapping プラグインのカスタマイズ機能を使い、変換を定義して解決します。

ko.mapping.fromJS / fromJSON の 2 つ目の引数には、プロパティ名でマッピング時の挙動を定義したオブジェクトを指定できるので、これを使って Date クラスに変換する処理を追加します。

var mapping = {
    created_at: {
        update: function (options) {
            return new Date(parseInt(options.data.substr(6)));
        }
    }
};

var obj = {
    created_at: "/Date(1198908717056)/"
};

var viewModel = ko.mapping.fromJS(obj, mapping);

作成した定義は非常にシンプルなものになりますが、実際に実行してみるとちゃんと Date クラスのインスタンスに変換されていることが確認できます。

f:id:shiba-yan:20140613172356p:plain

さらに特殊な変換が必要であれば、変換の関数内に組み込んでしまえば良いですね。

モデルの更新

Mapping プラグインではオブジェクトや JSON から新しくモデルを作成する以外にも、既存のモデルに対して値の更新を行うことが出来ます。使い方は fromJS / fromJSON の引数として既存のモデルを渡すだけです。

// 元になるモデルを作成する
var viewModel = ko.mapping.fromJS({
    name: 'kamebuchi'
});

// 新しいオブジェクトで値を更新
ko.mapping.fromJS({
    name: '抱かれたい男 No.1',
}, {}, viewModel);

// "抱かれたい男 No.1" が出力される
console.log(viewModel.name());

ちゃんと Observable なプロパティを壊すことなく更新してくれるので、定期的にサーバーからデータを更新するようなアプリケーションで便利だと思います。

いろいろ見てみると機能的には AutoMapper とかに似てますね。細かいカスタマイズや、別ソースを使って既存のモデルへの更新も出来るのが非常に便利でした。

*1:当然ながら Knockout.js の参照も必要

knockout.js 2.3.0 と 3.0.0 ベータがリリースされていました

最近、動きがなかった knockout.js ですが、昨日にバージョン 2.3.0 と 3.0.0 ベータが公開されました。

Knockout : Downloads

f:id:shiba-yan:20130710230813p:plain

公式サイトのダウンロードページが分かりやすくなった気がします。作者である Steven Sanderson 氏のブログに新機能や更新内容がいろいろと紹介されていました。

Knockout v2.3.0 released; v3.0.0 beta available - Steve Sanderson’s blog - As seen on YouTube™

そしてリリースノートに 2.3.0 と 3.0.0 ベータ の詳細な更新内容が記載されているので確認しておきます。

Releases · knockout/knockout · GitHub

2.3.0 では名前が変更されたメソッドやバインディングがあるので注意ですね。

具体的には hasfocus が hasFocus に、ユーティリティ扱いだった ko.utils.unwrapObservable メソッドが ko.unwrap になったようです。今のバージョンでは互換性が維持されていますが、将来的には古い名前では動作しなくなる可能性もあるので注意が必要です。

個人的に便利だと思うのが template バインディングで、テンプレート名に observable を指定できるようになった点です。テスト用のコードを載せておきます。

<input type="button" data-bind="click: change" value="change" />
<div data-bind="template: { name: type }"></div>

<script type="text/html" id="single">
    <input type="text" data-bind="value: message" />
</script>
<script type="text/html" id="multiple">
    <textarea data-bind="value: message" />
</script>

<script>
    var viewModel = {
        type: ko.observable("single"),
        message: ko.observable(),
        change: function () {
            this.type(this.type() == "single" ? "multiple": "single");
        }
    };

    ko.applyBindings(viewModel);
</script>

このコードではテンプレートを type プロパティの値で切り替えていますが、type の値 == テンプレート名なので if バインディングなどを使わずにスッキリ書けました。実に便利ですね。

応用として img/audio/video タグをテンプレート使って切り替えて出力なども出来ると思います。

おっと、そうそう 2.3.0 から ko.observable や computed などの DOM が関係しない機能であれば、node.js 上でも使えるようになったみたいですよ。NPM からもインストール出来るみたいです。

https://npmjs.org/package/knockout

最後に 3.0.0 についてですが、バインディング周りを書き直して拡張可能な仕組みになったようです。Mutation Observers とか使うかな?と少し期待しましたが、特にそのあたりは導入されていないようです。

インテリセンスの新しい knockout.js サポート

今日、Visual Studio 2012 を使って久しぶりに knockout.js のバインディングを書いていたら、data-bind 属性の中でインテリセンスが効くことに気が付きました。

例えばバインディングを定義しようとして、data-bind 属性を入力したら以下のような感じになりました。

f:id:shiba-yan:20130217002621p:plain

更に、バインディングを入力した後に ViewModel のプロパティ名も表示される親切な仕様です。

f:id:shiba-yan:20130217002711p:plain

おまけに data-bind 属性を使って knockout.js のバインディングを定義した部分は背景色が変わります。

f:id:shiba-yan:20130217003322p:plain

調べてみたら ASP.NET and Web Tools 2012.2 で knockout.js のインテリセンスが付いていたんですね。てっきり、knockout.js の JavaScript 側のインテリセンスかと思っていました。

詳しくは以下のブログで説明がされていたので参照してください。

Knockout Intellisense in Visual Studio 2012 | John Papa

何だかんだで knockout.js はバインディングの種類が多く、ViewModel のプロパティも多くなりがちなので、インテリセンスが機能するようになったのは非常にありがたいですね。

knockout.js の最近の動向

Knockout : Home

気が付いたら 2.2.1 までバージョンが上がってました。2.2.1 に関してはリリースノートが上がっていないようなので、2.2.0 の情報を追いかけます。

Knockout 2.2.0 released - Steve Sanderson’s blog - As seen on YouTube™

大きなところでは foreach バインディングを使っている場合、ソートで順番を入れ替えた場合に DOM の再生成を行わないようになっているようです。他にも仮想要素に対して text バインディングを設定可能になったようです。

<!-- 今までは親タグが必要だった -->
<span data-bind="text: fullName"></span>

<!-- ko 2.2.0 から仮想要素にバインド可能になった -->
<!-- ko text: fullName --><!-- /ko -->

個人的には仮想要素が好きなので、この実装は嬉しいですね。

最近は ASP.NET MVC でも Single Page Application テンプレートが復活したので、しっかりと内部で knockout.js が使われていますし、ここ最近一番注目している ASP.NET SignalR と knockout.js の相性は最高だと思っているので、何かこの二つを組み合わせたサービスを作ってみたいですね。

とりあえず knockout.js の基本的な部分は、昔に自分が書いたスライドを見てもらえれば大体 OK だと思います。

参考にされている方がそれなりにいるようで嬉しい限りです。

knockout.js 2.1.0 がリリースされました

Knockout 2.1.0 released

Knockout 2.1 is out - Knock Me Out

さっそく仕事で使ってるやつも 2.1.0 にアップデートしておきましたが特に不具合などは出ていません。

更新内容としては AMD 対応やパフォーマンスの改善がありますが、注目したいのが foreach でのインデックスでしょうか。$ から始まる変数としては $parent や $root などがありますが、foreach の中でだけ使える $index というのが追加されました。

<div data-bind="foreach: items">
  <span data-bind="text: $index"></span> - <span data-bind="text: name"></span>
</div>

とか書くと 0 から始まる数値が自動的にセットされるわけです。このあたりのサンプルは Steven Sandarson 氏がブログで用意してくれているので、そっちを確認してください(投げやり

今までは結構大きめのテンプレートを書いていた場合、要素の追加が目に見えて遅くなっていたのですが、DOM の clone 周り改善したらしいので速くなってたらいいですね。

まあベンチマークで確認しろという感じですが、最近忙しいので誰か調べてくれないかなぁ…。

knockout.js でカスタムバインディングを作成する

バインディングは text とか value みたいに data-bind 属性の内部で指定する奴です。knockout.js ではこのバインディングを自由に拡張できるようになっています。

バインディングは ko.bindingHandlers に内蔵のものもすべて含まれているので、ここに追加してあげればいいです。以下にテンプレを置いておきます。

ko.bindingHandlers.myBinding = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // 初期化、イベントハンドラの登録など
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // 値が変化した時に行う処理
    }
};

init は最初に一回だけ呼ばれるので、このタイミングでイベントハンドラの登録や初期値の設定を行っておきます。そして実際にバインディングしたプロパティの値が変化した時に update が呼ばれるので UI 要素への反映などを行います。

コールバックの引数は element に data-bind 属性を指定した DOM 要素、valueAccessor にバインディングされた値のアクセッサ、allBindingsAccessor に data-bind 属性に指定されたすべてのバインディング定義のアクセッサ、viewModel には指定したビューモデルが入ってます。

<span data-bind="myBinding: name"></span>

のように指定すると valueAccessor では name プロパティの値が取得できます。

アクセッサは関数なので呼び出すと実際の値が取れます。

// 値を取得
var value = valueAccessor();

// 全てのバインディング定義を取得
var allBindings = allBindingsAccessor();

valueAccessor で取れる値は大抵の場合は ko.observable なので、実際に値を取りたい場合には ko.utils.unwrapObservable を使えば楽です。

// 値を取得
var value = valueAccessor();

// observable から実際の値を取得
var valueUnwrapped = ko.utils.unwrapObservable(value);

後は何を実装するかなので、アイディアしだいですね。DOM 要素であればバインディング可能なので、audio/video 要素や canvas 要素向けのバインディングも作成することが出来るので、ある程度の処理をまとめたバインディングを作っておけば便利かもしれませんね。