[Knockout.js] ko.editables で ViewModel をロールバック/コミットしてみる


これは [KnockoutJS Advent Calendar 2014](http://qiita.com/advent-calendar/2014/knockoutjs) の９日目(12/9)の投稿です。

以前に [sukobuto](http://qiita.com/sukobuto) さんのこの記事（[KnockoutJS の便利なプラグイン](http://qiita.com/sukobuto/items/b4129190d496d0509bc3)）を拝見し、[ko.editables](https://github.com/romanych/ko.editables) オモシロそうやん！ と思ったので、この機に試してみました。導入手順から記載しますので、もし興味がある方は参考にしていただければと思います。

> ちなみに Knockout.js ですが、薄いし学習コストも低いしで、さくっとWEB画面に動きをつけるにはオススメです。同じ MVVM のフレームワーク(ていうかライブラリ？)として Vue.js がありますが、これはレガシーブラウザに対応していないので、業務で Vue.js はまだ導入できないかな、という感じです。（Knockout.js はレガシーブラウザに対応しているので安心。）

また自分の場合は、Knockout.js と Rails を組み合わせたりしています。Rails とわりと相性がいいんじゃなかろうか、と思っています。参考までに、Rails と組み合わせる方法についてはこちら。
⇒ [Rails⇔JavaScriptフレームワーク間でデータを受け渡す方法](http://qiita.com/hkusu/items/462d8bbe7c4dfe1877c8)

## まず Konockout.js の環境づくり

今回の方針として、[Yeoman](http://yeoman.io) の [webapp](https://github.com/yeoman/generator-webapp) ジェネレータでプロジェクトの雛形を作成して、その後に Bower で Knockout.js と `ko.editables` を導入することにします。

### 前提となる環境

OS：`Mac OS X 10.9.5`
yo：`1.3.2`
generator-webapp：`0.5.1`

また結果的に、Knockout.js のバージョンは `3.2.0`、ko.editables のバージョンは `0.9` になりました。

### 手順

webapp ジェネレータを起動します。(ちなみに CoffeeScript で書きます。)

```shell:
$ yo webapp --coffee
```

![スクリーンショット 2014-12-08 15.12.05.png](https://qiita-image-store.s3.amazonaws.com/0/35733/7f5b85be-57de-f8c4-4ef0-390b62dd24cf.png "スクリーンショット 2014-12-08 15.12.05.png")

今回は Bootstarap のみ選択しました。

雛形生成が終わったら、Bower で Knockout.js を導入します。

```shell:
$ bower install knockoutjs --save
```

このバージョンの webapp ジェネレータだと、下記のコマンドで js/css ライブラリを `index.html` へ自動で追記してくれます。

```shell:
$ grunt wiredep
````

ここで一旦、Knockout.js の動作確認をしてみます。

### ViewModel を書く

```coffeescript:app/scripts/main.coffee
$ ->

  HogeViewModel = (name, age) ->
    self =
      name: ko.observable(name)
      age: ko.observable(age)
      message: ko.observable("")
      say_hello: ->
        self.message("こんにちわ！ " + self.name() + " さん。")
        return
    self

  if $("#main")[0]
    ko.applyBindings HogeViewModel("山田", 32), $("#main")[0]

  return
```

### View を書く

```html:app/index.html
〜

<div id="main">
  <div class = "form-group">
    名前：<input class="form-control" data-bind="textInput: name">
    年齢：<input class="form-control" data-bind="textInput: age">
  </div>
  <p>
    <button class='btn btn-primary' data-bind='click: say_hello'>
      <span data-bind="text: name"></span>さん <span data-bind="text: age"></span> 歳に挨拶！
    </button>
  </p>
  <p style="color: goldenrod">
    <span data-bind="text: message"></span>
  </p>
</div>

〜
```

> Knockout.js バージョン`3.2`から `textInput` 属性が追加されたようです。入力をリアルタイムにキャッチしてくれます。

ファイルを保存したら、コマンドラインで `$ grunt serve` でローカルサーバを立ち上げて、データバインドが動作していればOKです。

![スクリーンショット 2014-12-08 15.28.46.png](https://qiita-image-store.s3.amazonaws.com/0/35733/49de7741-5626-13fb-0436-6518547ac9df.png "スクリーンショット 2014-12-08 15.28.46.png")

## ko.editables を導入する

次に [ko.editables](https://github.com/romanych/ko.editables) を Bower で導入します。

> 他にも Knockout.js のプラグインは、こちらにまとめられています。色々ありますね。
> ⇒ https://github.com/knockout/knockout/wiki/Plugins

```
$ bower install knockout-editables --save
```

`$ grunt wiredep` では自動で追記してくれなかったので、`index.html` に次のように追記します。

```diff:app/index.html
〜
<!-- build:js(.) scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/knockoutjs/dist/knockout.js"></script>
<!-- endbower -->
+<script src="bower_components/knockout-editables/ko.editables.js"></script>
<!-- endbuild -->
〜
```

## 本題：ViewModel をロールバックしてみる！

**デモをこちらに用意しました。**
⇒ http://hkusu.github.io/Knockout.js_ko.editables_demo/dist/

**イメージ：**

![スクリーンショット 2014-12-09 11.10.56.png](https://qiita-image-store.s3.amazonaws.com/0/35733/849750f9-ba83-9c51-2993-b109232b06fc.png "スクリーンショット 2014-12-09 11.10.56.png")

> [元に戻す]ボタンを押下すると、入力した内容がナシになります。[保存する]ボタンで一時セーブが出来ます。

ソースは次のようになっています。

### ViewModel

もう少しすっきりした書き方ができるような気がしつつ。

```coffeescipt:app/scripts/main.coffee
$ ->

  HogeViewModel = (name, power) ->

    # トランザクション開始用の内部関数
    begin_edit = ->
      self.name.beginEdit()
      self.power.beginEdit()
      return

    # コミット用の内部関数
    commit = ->
      self.name.commit()
      self.power.commit()
      return

    # ロールバック用の内部関数
    rollback = ->
      self.name.rollback()
      self.power.rollback()
      return

    # ViewModel のオブジェクト本体
    self =
      name: ko.observable(name) # 変数「name」を監視対象へ
      power: ko.observable(power)   # 変数「power」を監視対象へ
      cancel: -> # 「元に戻す」ボタンが押下された際のアクション
        rollback()   # ロールバック
        begin_edit() # 再度トランザクションを開始
        return
      save: ->   # 「保存する」ボタンが押下された際のアクション
        commit()     # コミット
        begin_edit() # 再度トランザクションを開始
        return

    # トランザクションの対象へ
    ko.editable(self)
    # 初期状態でトランザクション開始状態へ
    begin_edit()

    # リターン値としてオブジェクトを返す
    return self

  # View へ ViewModel をバインド
  if $("#main")[0]
    ko.applyBindings HogeViewModel("山田", 53), $("#main")[0]

  return
```

### View 

```html:app/index.html
<div id="main">
  <div class = "form-group" style="line-height: 40px">
    名前：<input class="form-control" data-bind="textInput: name">
    年齢：<input class="form-control" data-bind="textInput: power">
  </div>
  <p style="color: goldenrod">
    <span data-bind="text: name"></span> の戦闘力は <span data-bind="text: power"></span> 万です。
  </p>
  <button class='btn btn-primary' data-bind='click: cancel'>元に戻す</button>
  <button class='btn btn-primary' data-bind='click: save'>保存する</button>
</div>
```

使いこなせば、これは便利なんじゃないでしょうか。気になることとしては、`ko.editables` は数年メンテナンスされていないことですが、ソースは１ファイルのみのようなので、何かあれば自分で拡張しちゃうのも手かなと。
⇒ https://github.com/romanych/ko.editables/blob/master/ko.editables.js

## おわりに

今回のソースはこちらに置きました。
⇒ https://github.com/hkusu/Knockout.js_ko.editables_demo

もしサーバと組み合わせるなら、ViewModel を commit したら、サーバ側のデータも保存するようにすればいいのかなと。

> 書いてて思ったのですが、ViewModel を保存 ⇒ サーバ側のデータを更新 のアーキテクチャ良いかも。面白そうなので、今後 Rails と組み合わせる実験をしてみようかな。

明日(12/10)は [@tan_go238](https://twitter.com/tan_go238) さんによる ko.utils の記事です！よろしくお願いします。

> あと、Knockout.js を TypeScript で、というタイトルで [TypeScript Advent Calendar 2014](http://qiita.com/advent-calendar/2014/typescript) の 11日目に書く予定なので、もし興味があればそちらもご覧ください。