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

AngularJS1.3で追加されたngModelOptionsで遊ぶ

はじめに

最近出版されたAngularJSリファレンス

AngularJSリファレンス

AngularJSリファレンス

これはすごく良い本。分かりやすいし、なんと言っても1.3に対応しているのが素晴らしい。

Angularの公式チュートリアルをやって、さかな本を読んで、ぐらいで1.3を一生懸命追っかけてない人(まさに僕)には特にいいと思う。

そういう意味では、この本の価値は出たての今がまさにピークだと思うので、AngularJSに興味がある人はいつ買うか?いまでしょ!って感じ。

その本の中で1.3で追加されてるdirectiveで、ngModelOptionsっていうのが紹介されてた。

これが便利そうなのでちょっといじってみる。

書いたソースは↓に置いた。

joe-re/ng-model-options-sample · GitHub

公式ドキュメント

https://docs.angularjs.org/api/ng/directive/ngModelOptions

できること

ngModelに指定した変数の値を(viewで)変更した時には、リアルタイムにmodelに通知される。

model側で変更した時も同様に、viewに通知される。

つまりviewとmodelでは常に同じ値を保持する。

これはAngular様の双方向バインディングのお力のおかげだ。

これは非常にありがたくて便利で、まさにAngular様々なんだけど、時々困る。

それがこのngModelOptionsを使うと、ng-modelの変更タイミングをカスタマイズすることができる。

実例

こんな例を考えてみる。

f:id:joe-re:20140817002725p:plain

saveを押下した時に入力されている値を保存したい。

これを愚直に全てngModelで紐付けすると、save押すか押さないかに関わらず全て共有されてしまう。

途中まで入力して、別のアイテムを選択して、また戻ったら入力途中のものが再現される。

左の選択リストにはtitle:の値を出しているのだけど、こいつも入力値に合わせてリアルタイムで変わってしまう。

ngModelOptionsを使わない実装

  • views/main.html
<div class="list-group col-lg-4">
  <a ng-repeat='item in items' href='' class="list-group-item"
    ng-click='select(item)' ng-class='{active: isSelect(item)}'>{{item.title}}</a>
</div>

<div class="list-group col-lg-8">
  <form class="well" ng-submit="save()">
    <div class="form-group">
      <label for="title">title:</label>
      <input id="title" type="text" class="form-control" placeholder="title" ng-model="selectedItem.title">
    </div>
    <div class="form-group">
      <label for="content">content:</label>
      <textarea id="content" style="resize: none" rows="10" class="form-control" placeholder="content" ng-model="selectedItem.content"></textarea>
    </div>
    <input type="submit" class="btn btn-info" value="Save" />
  </form>
</div>
  • scripts/controllers/main.js
'use strict';

angular.module('angularTestApp')
.controller('MainCtrl', function ($scope) {
  $scope.items = [
    { id: 1, title: 'test1', content: 'content1' },
    { id: 2, title: 'test2', content: 'content2' },
    { id: 3, title: 'test3', content: 'content3' },
  ];

  $scope.select = function(item) { $scope.selectedItem = angular.copy(item);  };
  $scope.isSelect = function(item) { return $scope.selectedItem.id === item.id; };
  $scope.save = function() {
    angular.forEach($scope.items, function(item, i) {
      if (item.id === $scope.selectedItem.id) {
        $scope.items[i] = angular.copy($scope.selectedItem);
      }
    });
  };

  $scope.select($scope.items[0]);
});

かなり小細工してる。

selectedItemというmodelを作って、選択した値はangular.copyを使ってディープコピーする。

右の入力エリアにはselectedItemの値を出しておく。

saveが押下されたら、selectedItemをディープコピーしてitemsを更新する。

ngModelOptionsを使った実装

  • views/main.html
<div class="list-group col-lg-4">
  <a ng-repeat='item in items' href='' class="list-group-item" ng-click='select(item)'
    ng-class='{active: isSelect(item)}'>{{item.title}}</a>
</div>

<div class="list-group col-lg-8">
  <form class="well" ng-submit>
    <div class="form-group">
      <label for="title">title:</label>
      <input id="title" type="text" class="form-control" placeholder="title"
        ng-model="selectedItem.title" ng-model-options="{ updateOn: 'submit' }">
    </div>
    <div class="form-group">
      <label for="content">content:</label>
      <textarea id="content" style="resize: none" rows="10" class="form-control" placeholder="content"
        ng-model="selectedItem.content" ng-model-options="{ updateOn: 'submit' }"></textarea>
    </div>
    <input type="submit" class="btn btn-info" value="Save" />
  </form>
</div>
  • scripts/controllers/main.js
'use strict';

angular.module('angularTestApp')
.controller('MainCtrl', function ($scope) {
  $scope.items = [
    { id: 1, title: 'test1', content: 'content1' },
    { id: 2, title: 'test2', content: 'content2' },
    { id: 3, title: 'test3', content: 'content3' },
  ];

  $scope.select = function(item) { $scope.selectedItem = item;  };
  $scope.isSelect = function(item) { return $scope.selectedItem === item; };
  $scope.select($scope.items[0]);
});

ng-model-options="{ updateOn: 'submit' }をngModelの更新タイミングに設定した。

htmlはそんなに変わってないけど、jsは小細工がなくなってすっきり!

saveメソッドももはや必要なくなったので、削除できた。

ただsaveメソッド的なやつは、あくまで今回のコンテキストが、左の選択リストと右の入力途中のものを分離したいだけだから不要というだけで、実際にはサーバの保存APIを呼んだりする必要がある。

今回は簡略化のために意識してないけど、item自身にsaveメソッドを用意してあげて、呼んだら自分自身をサーバ側に保存しにいくようにするとより良い感じ。

ngModelOptionsを使ってngModelの更新タイミングを調整していれば、コントローラやディレクトリに追加のコードを書く必要はなくて、ng-submit="selectedItem.save()"みたいにすれば良いのですごく奇麗。

おわりに

振り返ってみれば、遊ぶってほど遊んでない。。

また今度遊んだら、その2とか書く。