パート 5: Knockout.js を使用して動的 UI を作成する

作成者: Rick Anderson

完成したプロジェクトをダウンロードする

Knockout.js で動的 UI を作成する

このセクションでは、Knockout.jsを使用して、Admin ビューに機能を追加します。

Knockout.js は、HTML コントロールをデータに簡単にバインドできる Javascript ライブラリです。 Knockout.js では Model-View-ViewModel (MVVM) パターンを使用します。

  • Model は、ビジネス ドメインのサーバー側のデータ表現です (ここでは製品と注文)。
  • View は、プレゼンテーション レイヤー (HTML) です。
  • View-Model は、Model データを保持する Javascript オブジェクトです。 View-Model は、UI のコード抽象化です。 HTML 表現には関知しません。 代わりに、"アイテムの一覧" など、View の抽象的な特徴を表します。

View は、View-Model にデータ バインドされます。 View-Model に対する更新は、View に自動的に反映されます。 また、View-Model は、ボタン クリックなどのイベントを View から取得し、Model に対して操作 (注文の作成など) を実行します。

Diagram of interaction between H T M L data, the view-model, j son, and the Web A P I controller.

HTML データ、View-Model、json、Web API コントローラー間の相互作用を示す図。 HTML データ ボックスは、ラベル付きの View です。 データ バインディングというラベルが付いた双方向矢印は、HTML データ ボックスを View-Model ボックスにリンクします。 HTTP リクエストというラベルが付いた双方向矢印と、サーバーからの json Model は、View-Model を Web API コントローラーにリンクします。

まず、View-Model を定義します。 その後、HTML マークアップを View-Model にバインドします。

次の Razor セクションを Admin.cshtml に追加します。

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
  <script type="text/javascript" src="@Url.Content("~/Scripts/knockout-2.1.0.js")"></script> 
  <script type="text/javascript">
  // View-model will go here
  </script>
}

このセクションは、ファイル内の任意の場所に追加できます。 View がレンダリングされると、そのセクションが HTML ページの下部 (終了 </body> タグの直前) に表示されます。

このページのすべてのスクリプトは、コメントで示される、スクリプト タグ内に配置されます。

<script type="text/javascript">
  // View-model will go here
  </script>

まず、View-Model クラスを定義します。

function ProductsViewModel() {
    var self = this;
    self.products = ko.observableArray();
}

ko.observableArray は、observable と呼ばれる、Knockout の特殊なタイプのオブジェクトです。 Knockout.js のドキュメントによると、observable は、"変更についてサブスクライバーに通知できる JavaScript オブジェクト" です。observable のコンテンツが変わると、それに合わせて View が自動的に更新されます。

products 配列に入力するには、Web API に対して AJAX 要求を行います。 API のベース URI をビュー バッグに格納したことを思い出してください (チュートリアルのパート 4 を参照)。

function ProductsViewModel() {
    var self = this;
    self.products = ko.observableArray();

    // New code
    var baseUri = '@ViewBag.ApiUrl';
    $.getJSON(baseUri, self.products);
}

次に、View-Model に関数を追加して、製品を作成、更新、削除します。 これらの関数は、Web API に AJAX 呼び出しを送信し、その結果を使用して View-Model を更新します。

function ProductsViewModel() {
    var self = this;
    self.products = ko.observableArray();

    var baseUri = '@ViewBag.ApiUrl';

    // New code
    self.create = function (formElement) {
        // If the form data is valid, post the serialized form data to the web API.
        $(formElement).validate();
        if ($(formElement).valid()) {
            $.post(baseUri, $(formElement).serialize(), null, "json")
                .done(function (o) { 
                    // Add the new product to the view-model.
                    self.products.push(o); 
                });
        }
    }

    self.update = function (product) {
        $.ajax({ type: "PUT", url: baseUri + '/' + product.Id, data: product });
    }

    self.remove = function (product) {
        // First remove from the server, then from the view-model.
        $.ajax({ type: "DELETE", url: baseUri + '/' + product.Id })
            .done(function () { self.products.remove(product); });
    }

    $.getJSON(baseUri, self.products);
}

最も重要な部分として、DOM がいっぱいになったら、ko.applyBindings 関数を呼び出し、ProductsViewModel の新しいインスタンスを渡します。

$(document).ready(function () {
    ko.applyBindings(new ProductsViewModel());
})

ko.applyBindings メソッドにより Knockout がアクティブになり、View-Mode を View に接続します。

これで View-Model が作成されました。次はバインディングを作成します。 Knockout.js では、HTML 要素に data-bind 属性を追加することでこれを行います。 たとえば、HTML リストを配列にバインドするには、foreach バインディングを使用します。

<ul id="update-products" data-bind="foreach: products">

foreach バインディングによって配列が反復処理され、配列内の各オブジェクトの子要素が作成されます。 子要素のバインディングは、配列オブジェクトのプロパティを参照できます。

"update-products" リストに次のバインディングを追加します。

<ul id="update-products" data-bind="foreach: products">
    <li>
        <div>
            <div class="item">Product ID</div> <span data-bind="text: $data.Id"></span>
        </div>
        <div>
            <div class="item">Name</div> 
            <input type="text" data-bind="value: $data.Name"/>
        </div> 
        <div>
            <div class="item">Price ($)</div> 
            <input type="text" data-bind="value: $data.Price"/>
        </div>
        <div>
            <div class="item">Actual Cost ($)</div> 
            <input type="text" data-bind="value: $data.ActualCost"/>
        </div>
        <div>
            <input type="button" value="Update" data-bind="click: $root.update"/>
            <input type="button" value="Delete Item" data-bind="click: $root.remove"/>
        </div>
    </li>
</ul>

<li> 要素は、foreach バインディングのスコープ内で発生します。 つまり、Knockout は products 配列内の各製品に対してその要素を 1 回レンダリングします。 <li> 要素内のすべてのバインディングは、その製品インスタンスを参照します。 たとえば、$data.Name は製品の Name プロパティを参照します。

テキスト入力の値を設定するには、value バインディングを使用します。 ボタンは、click バインディングを使用して、Model-View の関数にバインドされます。 製品インスタンスは、各関数にパラメーターとして渡されます。 詳細については、Knockout.js のドキュメント でさまざまなバインディングについて細かく説明しています。

次に、[製品の追加] フォームで submit イベントのバインディングを追加します。

<form id="addProduct" data-bind="submit: create">

このバインディングにより、View-Model の create 関数が呼び出され、新しい製品が作成されます。

Admin ビューの完全なコードを次に示します。

@model ProductStore.Models.Product

@{
    ViewBag.Title = "Admin";
}

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
  <script type="text/javascript" src="@Url.Content("~/Scripts/knockout-2.0.0.js")"></script> 
  <script type="text/javascript">
      function ProductsViewModel() {
          var self = this;
          self.products = ko.observableArray();

          var baseUri = '@ViewBag.ApiUrl';

          self.create = function (formElement) {
              // If valid, post the serialized form data to the web api
              $(formElement).validate();
              if ($(formElement).valid()) {
                  $.post(baseUri, $(formElement).serialize(), null, "json")
                      .done(function (o) { self.products.push(o); });
              }
          }

          self.update = function (product) {
              $.ajax({ type: "PUT", url: baseUri + '/' + product.Id, data: product });
          }

          self.remove = function (product) {
              // First remove from the server, then from the UI
              $.ajax({ type: "DELETE", url: baseUri + '/' + product.Id })
                  .done(function () { self.products.remove(product); });
          }

          $.getJSON(baseUri, self.products);
      }

      $(document).ready(function () {
          ko.applyBindings(new ProductsViewModel());
      })
  </script>
}

<h2>Admin</h2>
<div class="content">
    <div class="float-left">
    <ul id="update-products" data-bind="foreach: products">
        <li>
            <div>
                <div class="item">Product ID</div> <span data-bind="text: $data.Id"></span>
            </div>
            <div>
                <div class="item">Name</div> 
                <input type="text" data-bind="value: $data.Name"/>
            </div> 
            <div>
                <div class="item">Price ($)</div> 
                <input type="text" data-bind="value: $data.Price"/>
            </div>
            <div>
                <div class="item">Actual Cost ($)</div> 
                <input type="text" data-bind="value: $data.ActualCost"/>
            </div>
            <div>
                <input type="button" value="Update" data-bind="click: $root.update"/>
                <input type="button" value="Delete Item" data-bind="click: $root.remove"/>
            </div>
        </li>
    </ul>
    </div>

    <div class="float-right">
    <h2>Add New Product</h2>
    <form id="addProduct" data-bind="submit: create">
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>Contact</legend>
            @Html.EditorForModel()
            <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    </form>
    </div>
</div>

アプリケーションを実行し、管理者アカウントでログインして、"Admin" リンクをクリックします。 製品の一覧が表示され、製品を作成、更新、削除できます。