かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Knockout.jsを入門してみた

ちょっと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>

力尽きた

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