みこむらめもむらむらむら

なんかHTML5とかJS勉強とかやりながらめもを綴るブログ

【KnouckoutJS】リストの操作

はいはいKnouckoutJSお勉強なうです

はじめに

Working with Lists and Collections
Building a dynamic UI where elements are added and removed

ということでリストの操作です、利用頻度高そう
要素の追加、削除の動的なUI構築とあるくらいなので
変更が即時反映のリストが作れるんだろうなという期待をしつつ
実際にチュートリアルをやってみるよ


今回は座席や食事を予約ツールが例題でございまする

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([ //★1
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);
}

ko.applyBindings(new ReservationsViewModel());

まずはViewModel(javascript)の方のあれそれ
とくにReservationsViewModelのseatsに注目(★1)
SeatReservationの初期値を保持する配列であると同時に
ko.observableArrayであることが大事

KnouckoutJSではおなじみのko.observableなわけで
項目が追加、削除されるたびに自動的にUIの更新が可能になる
うむーなんともこのko.observableにまだ慣れないのだが
これめっちゃ使うみたいなのよな‥慣れるまで時間かかりそう‥

お次はView(html)‥こっちもdata-bindに慣れねばな‥

<h1>Your seat reservations</h1>
<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
    </tr></thead>
    <tbody data-bind="foreach: seats"><!--★2-->
        <tr>
            <td data-bind="text: name"></td>
            <td data-bind="text: meal().mealName"></td>
            <td data-bind="text: meal().price"></td><!--★3-->

        </tr>    
    </tbody>
</table>

こちらで注意すべきはmealプロパティの値を参照する際(★3)
関数のように

meal()

と記述する必要があるということ
故に食事の値段は

meal().price

となるわけです
なんで関数呼び出しなんだ?ってところが曖昧なんだよな‥代入処理じゃないからってどういうこと?
理解できてなすぎて死にたい、聞いてみよう‥

ちなみにforeachバインディングはおなじみの繰り返し処理で(★2)
tbody内のコンテンツを繰り返して表示するみたい
他にもifとかifnotとかもつかえるとかつかえないとかごにょごにょ

席を追加する

お次は先ほどのソースにそれぞれ下記を追加、実行!

<!--ソースの一番最後に追加-->
<button data-bind="click: addSeat">Reserve another seat</button>
function ReservationsViewModel() {
    // ※他はそのままで下記だけ追加

    // Operations
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
}

ボタンクリックで配列が追加できるようになりました!わー!

ReservationsViewModelのseats(★3)が
ko.observableArrayがゆえに常に値を監視されていて
先ほども述べたように追加、削除の処理が行われると
自動的にUIが更新されまする

ちなみに私は、あれ.push()ってなんだっけってなりました
Arrayオブジェクトの.push()は配列末尾に要素を追加でごわす

そうだね追加処理だね!←

ここでとくに注目すべきは
行を追加しても再生にUI全体リロードされるわけではないこと!
効率化のためKnouckoutJSは更新のあった最小限の処理を反映するように
できているそうな‥利口!

編集可能なデータを作成する

お次は既存のデータや追加したデータを編集できすようになるそうな
まずはView(html)の下記部分を

<td data-bind="text: name"></td>
<td data-bind="text: meal().mealName"></td>

↓こんな感じに書き換えてみまする

<td><input data-bind="value: name" /></td>
<td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td><!--★4-->

ドロップダウンのリスト新たに2つのバインディングを使用しているとな、ふむふむ
使用しているバイディングは下記の2つ

利用可能な項目の設定にoptionsバインディング
optionsバインディングはselect要素のみに使用するもので
割り当てる値は配列(または観察配列)
配列内の各項目ごとにoption要素が表示されるらしい

利用可能な項目のプロパティを表示するためにoptionsTextバインディング
リストのテキストとして表示されるべき
プロパティを設定するためのバインディング
ふむふむ、ここではmealNameだけどこれをpriceにすれば値段が表示されるのかー
にゃるほど!

ここでは何も触れてないのだけれど$rootのとこがなんで必要なのかわかりますん
わ、わかるのがあたりまえなの‥(((;゚Д゚)))ガクブル

価格の書式設定(データフォーマット機能を追加する)

なにやらお次は価格表示をデータの中身によって整形し直して
表示する機能を追加するようです

なんか翻訳が上手いこといってなくてわかりにくいんだけれど
KnouckoutJSはデータをきれいにオブジェクト指向で書いているから
オブジェクトグラフ内の任意の場所に簡単に機能を追加できるそうな
オブジェクトグラフってなんだよと思ったのは内緒
ぐぐってもベストアンサーがなかった‥

さて、ソース見てみましょう

今回はViewModel(javascript)から

function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() { //★5
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}

formattedPriceを追加します(★5)
価格が0だったらNone、1以上だったら$マークをつけるフォーマット機能を追加

とくに注目すべきはko.computedの部分
監視している値をもとに処理を実行し
結果のデータを自動で返している、みたい←

ちなみにこのフォーマットされたデータをView(html)に表示する場合は

<td data-bind="text: meal().price"></td>

↓こんな感じに書き換えまする

<td data-bind="text: formattedPrice"></td>

うむうむ、自動更新される、すごい

項目を削除する

追加もできれば削除もできるよね!ということで★6の行をView(html)に追加
削除リンクですです

<tr>
    <td><input data-bind="value: name" /></td>
    <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
    <td data-bind="text: formattedPrice"></td>
    <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td><!--★6-->
</tr> 

さてここでも出てきました$root
翻訳ツールで翻訳すると

$rootの接頭辞は、トップレベルのViewModelの代わりにSeatReservationインスタンスがバインドされている上にremoveSeatハンドラを探すためにノックアウトを引き起こすことに注意してください

はて、どういうことですかね‥?w
どうしよう全然わからない
そもそも$rootが全然わかってないのよね‥これは質問します‥

function ReservationsViewModel() {
    // ※そのまま

    // Operations
    self.addSeat = function() {
        // ※そのままで下記を追加
    }
    self.removeSeat = function(seat) {
        self.seats.remove(seat);
    }
} 

ViewModel(javascript)の方にはView(html)でdata-bindしたremoveSeatを追加

するとリンククリックで予約データ削除!おおーできました

合計金額を表示する

お次は食事の合計金額の計算処理を追加です
ko.computedですな

function ReservationsViewModel() {
   // ※他はそのままで下記だけ追加

   // Computed data
   self.totalSurcharge = ko.computed(function() {
      var total = 0;
      for (var i = 0; i < self.seats().length; i++) {
          total += self.seats()[i].meal().price;
      }
      return total;
   });
} 

互いに監視関係にあるので
更新があるたびに自動でUIにも更新がされまする、うむ

さて今度は計算処理した合計値をView(html)に表示するためにこちらも追記

<p data-bind="visible: totalSurcharge() > 0"><!--★7-->
    Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</p>

ここではvisibleバインディングを使用してますな(★7)
これは感覚でわかるー
CSSのdisplayプロパティをごにょごにょしているわけですな

あとここでもう一つ、バインディングの内側に
任意のJavaScript式を使用しているところも注目
簡単な構文はdata-bind内に書けますよ、と
うーんこれはいいのか悪いのか‥
処理は極力ViewModel(javascript)に寄せたいと思ったり思わなかったり‥どうなんだろな

最後に

View(html)にちょっと記述を追加するだけで
座席数の合計を表示したり
座席の上限をしてしたりすることが可能だそうな

まずは合計

<h1>Your seat reservations</h1>

↓こんな感じに追記

<h1>Your seat reservations(<span data-bind="text: seats().length"></span>)</h1>

これだけで増減も自動で反映されまする
おおすごい

上限指定は一定数座席が埋まったらボタンを無効にすることで実現できるとか

<button data-bind="click: addSeat">Reserve another seat</button>

↓こんな感じに追記

<button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button>

enableバインディングは指定されたパラメーター値がtrueの場合のみ
関連付けられたDOM要素を有効にするもの
この場合 seats().length < 5 がtrueの場合はボタンが有効だけど
falseになるとその時点で無効化されるようになっていまする
inputとかselectとかtextareaとかのform要素に有用とのこと、ふむー

ということでまとめ

最終的にこんな感じになりました

<h1>Your seat reservations(<span data-bind="text: seats().length"></span>)</h1>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
    </tr></thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
            <td data-bind="text: formattedPrice"></td>
            <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
        </tr>    
    </tbody>
</table>

<button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button>

<p data-bind="visible: totalSurcharge() > 0">
    Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</p>
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });    
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);

    // Computed data
    self.totalSurcharge = ko.computed(function() {
       var total = 0;
       for (var i = 0; i < self.seats().length; i++) {
           total += self.seats()[i].meal().price;
       }
       return total;
    });    

    // Operations
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
    self.removeSeat = function(seat) { self.seats.remove(seat) }
}

ko.applyBindings(new ReservationsViewModel());

まとめてみたらなんかちょっとわかってきた気がする
気がするだけかもしれないけれど

互いに監視し合う関係とかその辺はなんとなくつかめてきました
ふむふむ