読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Knockout.jsを入門してみた

ASP.NET JavaScript

ちょっとJavaScriptのフレームワークを使おうかなと思ったのでどれを使おうか選んでたのですが、最近のreact.jsや、AngularJSや、Cycle.jsとかもいいですが以下の理由でKnockout.jsにしてみようと思いました。

  • 枯れてる
  • メンテナンスされ続けてる
  • 学習コストが低い
  • TypeScriptと相性がよさそうに見えた

特に半年前のフレームワークって何処行ったの??っていう状況になってるような気がするJavaScript界においてこれだけ長期間安定して提供され続けてるという点が個人的に評価ポイントとして高かったです。

Visual Studioでの使い方

ということでVSから使ってみようと思います。ASP.NETのプロジェクトを作ってMVCにだけチェック入れてEmptyのプロジェクトを作ります。

ライブラリの導入

NuGetからknockoutjsをインストールします。続けてknockout.typescriptで検索して型定義を入れます。

Viewの作成

HomeControllerを作ってIndexのViewをさくっと作ります。HomeControllerのIndexメソッドで右クリックしてViewの追加からやるとスムーズです。

Views/Shared/_Layout.cshtmlのJavaScriptに以下のようにknockoutを読み込むようにします。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <script src="~/Scripts/modernizr-2.6.2.js"></script>
    <script src="~/Scripts/knockout-3.3.0.js"></script>>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                </ul>
            </div>
        </div>
    </div>

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
</body>
</html>

Scriptの作成

Scriptsフォルダにindex.tsという名前でTypeScriptのファイルを作ります。ここにいろいろ書いていきます。

とりあえず、以下のページのサンプルをさくっと書いてみようと思います。

kojs.sukobuto.com

Hello world

TypeScriptはこんな感じで。

/// <reference path="typings/knockout/knockout.d.ts" />

module ViewModels {
    export class HelloWorldViewModel {
        public firstName: KnockoutObservable<string>;
        public lastName: KnockoutObservable<string>;
        public fullName: KnockoutComputed<string>;

        constructor(firstName: string, lastName: string) {
            this.firstName = ko.observable(firstName);
            this.lastName = ko.observable(lastName);
            this.fullName = ko.computed(() => this.firstName() + this.lastName())
        }
    }
}

window.onload = () => ko.applyBindings(new ViewModels.HelloWorldViewModel("Planet", "Earth"));

Index.cshtmlはこんな感じで。

@{
    ViewBag.Title = "Index";
}

<p>ファーストネーム:<input data-bind="value: firstName" /></p>
<p>ラストネーム:<input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"></span>!</h2>

<script src="~/Scripts/index.js"></script>

クリックカウンター

TypeScriptはこんな感じ。

/// <reference path="typings/knockout/knockout.d.ts" />

module ViewModels {
    export class ClickCounterViewModel {
        public numberOfClicks: KnockoutObservable<number> = ko.observable(0);

        public registerClick(): void {
            this.numberOfClicks(this.numberOfClicks() + 1);
        }

        public resetClicks(): void {
            this.numberOfClicks(0);
        }

        public hasClickedTooManyTimes: KnockoutComputed<boolean> = ko.computed(() => {
            return this.numberOfClicks() >= 5;
        }, this);
    }
}

window.onload = () => ko.applyBindings(new ViewModels.ClickCounterViewModel());

HTMLはこんな感じ。

@{
    ViewBag.Title = "Index";
}

<div>クリック回数 <span data-bind="text: numberOfClicks"></span></div>

<button data-bind="click: registerClick, disable: hasClickedTooManyTimes">クリック</button>

<div data-bind="visible: hasClickedTooManyTimes">
    クリックしすぎ。
    <button data-bind="click: resetClicks">リセット</button>
</div>

<script src="~/Scripts/index.js"></script>

シンプルなリスト

TypeScriptはこんな感じ。

/// <reference path="typings/knockout/knockout.d.ts" />

module ViewModels {
    export class SimpleListViewModel {
        public items: KnockoutObservableArray<string>;
        public itemToAdd: KnockoutObservable<string> = ko.observable("");
        constructor(items: string[]) {
            this.items = ko.observableArray(items);
        }
        public addItem(): void {
            this.items.push(this.itemToAdd());
            this.itemToAdd("");
        }
    }
}

window.onload = () => ko.applyBindings(new ViewModels.SimpleListViewModel(["Alpha", "Beta", "Gamma"]));

HTMLはこんな感じ。

@{
    ViewBag.Title = "Index";
}

<form data-bind="submit: addItem">
    新しいアイテム
    <input data-bind="value: itemToAdd" />
    <button data-bind="click: addItem">追加</button>

    <p>アイテム一覧</p>
    <select multiple="multiple" data-bind="options: items"></select>
</form>

<script src="~/Scripts/index.js"></script>

リストを改良する

ちょっとめんどくさくなってきたので手抜きでこんな感じに。

/// <reference path="typings/knockout/knockout.d.ts" />

module ViewModels {
    export class SimpleListViewModel {
        public items: KnockoutObservableArray<string>;
        public selectedItems: KnockoutObservableArray<string>;
        public itemToAdd: KnockoutObservable<string> = ko.observable("");
        constructor(items: string[]) {
            this.items = ko.observableArray(items);
            this.selectedItems = ko.observableArray(items.slice(0, 1));
        }
        public addItem(): void {
            this.items.push(this.itemToAdd());
            this.itemToAdd("");
        }

        public removeSelected(): void {
            this.items.removeAll(this.selectedItems());
            this.selectedItems([]);
        }

        public sort(): void {
            this.items.sort();
        }
    }
}

window.onload = () => ko.applyBindings(new ViewModels.SimpleListViewModel(["Alpha", "Beta", "Gamma"]));

HTMLのほうはこんな感じに。

@{
    ViewBag.Title = "Index";
}

<form data-bind="submit: addItem">
    新しいアイテム
    <input data-bind="value: itemToAdd" />
    <button data-bind="click: addItem">追加</button>

    <p>アイテム一覧</p>
    <select multiple="multiple" data-bind="options: items, selectedOptions: selectedItems"></select>
    <button data-bind="click: removeSelected, enable: selectedItems().length > 0">削除</button>
    <button data-bind="click: sort, enable: items().length > 0">ソート</button>
</form>

<script src="~/Scripts/index.js"></script>

コレクションを操る

段々めんどくさくなってきたので$rootとかは省略

/// <reference path="typings/knockout/knockout.d.ts" />

module ViewModels {
    export class Person {
        public children: KnockoutObservableArray<string>;

        constructor(public name: string, children: string[]) {
            this.children = ko.observableArray(children);
        }

        public addChild(): void {
            this.children.push("新しいお子様");
        }
    }

    export class ViewModel {
        public people: Person[];

        constructor() {
            this.people = [
                new Person("Annabella", ["Arnie", "Anders", "Apple"]),
                new Person("Bertie", ["Arnie", "Anders", "Apple"]),
                new Person("Charles", ["Arnie", "Anders", "Apple"])
            ];
        }
    }
}

window.onload = () => ko.applyBindings(new ViewModels.ViewModel());

HTML

@{
    ViewBag.Title = "Index";
}

<ul data-bind="foreach: people">
    <li>
        <div>
            <span data-bind="text: name"></span>さんの<span data-bind="text: children().length"></span>人のお子様
            <a href="#" data-bind="click: addChild">また産まれた!</a>
            <ul data-bind="foreach: children">
                <li><span data-bind="text: $data"></span></li>
            </ul>
        </div>
    </li>
</ul>

<script src="~/Scripts/index.js"></script>

力尽きた

とりあえず、眠たくなってきたので今日はここまで。