LoginSignup
32
36

More than 5 years have passed since last update.

ng-modelのやや踏み込んだ話

Last updated at Posted at 2015-09-07

ng-modelを使うようなdirectiveを作るときのパターンを何個か紹介します。

対象

Angularのversion

1.4系

対象な人

  • ng-modelが使えるdirectiveを作りたい
  • NgModelControllerについて知りたい

対象じゃない人

  • ng-model自体の使い方を知りたい

NgModelControllerのプロパティ、メソッド一覧

割りと説明なしで使うのでAngularJS: API: ngModel.NgModelControllerを参照しながらみてください。

Validators

form.$error.fooでエラーを受け取りたいなら、ngModel.$validators.fooにbooleanを返す関数を書きます。引数はそれぞれngModel.$modelValue, ngModel.$viewValueと同じものになります。formの詳しい情報はformとの連携を参照して下さい。

<form name="form">
  <input type="text" ng-model="hoge" name="hoge" foo>
  {{form.$error.foo}}
</form>
app.directive('foo', () => {
  return {
    restrict: 'A',
    require: 'ngModel',
    priority: -1,
    link: (scope, element, attr, ngModel) => {
      ngModel.$validators.foo = (modelValue, viewValue) => (modelValue | viewValue) === 'foo';
    }
  };
});

$validators.fooに登録される関数は厳密にbooleanを返すべきです。例えば横着してundefinedはfalsyだろと思ってそのまま返すとformがvalidでもinvalidでもない状態になってしまいます。その状態でformの送信をform.$invalidを見てガードしていたりするとすり抜けます。

装飾目的で標準のinputをラップする

イケてるUIを要素一つで表現したい等。

<!-- コード上はこう書きたい -->
<awesome-checkbox ng-model="foo" ng-true-value="'TRUE'"></awesome-checkbox>
<!-- こう展開されて欲しい -->
<div class="awesome-checkbox">
  <input type="" ng-model="foo" ng-true-value="'TRUE'">
  ...
</div>

やり方は単純で、compileするときにng-modelなど、使用するinput要素の属性にコピーするだけです。

例: ionic/checkbox.js at master · driftyco/ionicより

/**
 * @ngdoc directive
 * @name ionCheckbox
 * @module ionic
 * @restrict E
 * @codepen hqcju
 * @description
 * The checkbox is no different than the HTML checkbox input, except it's styled differently.
 *
 * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
 *
 * @usage
 * ```html
 * <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
 * ```
 */

IonicModule
.directive('ionCheckbox', ['$ionicConfig', function($ionicConfig) {
  return {
    restrict: 'E',
    replace: true,
    require: '?ngModel',
    transclude: true,
    template:
      '<label class="item item-checkbox">' +
        '<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
          '<input type="checkbox">' +
          '<i class="checkbox-icon"></i>' +
        '</div>' +
        '<div class="item-content disable-pointer-events" ng-transclude></div>' +
      '</label>',
    compile: function(element, attr) {
      var input = element.find('input');
      // compileでng-modelなどのattributeを追加する
      forEach({
        'name': attr.name,
        'ng-value': attr.ngValue,
        'ng-model': attr.ngModel,
        'ng-checked': attr.ngChecked,
        'ng-disabled': attr.ngDisabled,
        'ng-true-value': attr.ngTrueValue,
        'ng-false-value': attr.ngFalseValue,
        'ng-change': attr.ngChange,
        'ng-required': attr.ngRequired,
        'required': attr.required
      }, function(value, name) {
        if (isDefined(value)) {
          input.attr(name, value);
        }
      });
      var checkboxWrapper = element[0].querySelector('.checkbox');
      checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
    }
  };
}]);

見た目($viewValue)とモデル($modelValue)を分離する

input要素上では文字列で入力するが、コード上はオブジェクト等にしたい等。

実装すべきもの

  • ngModel.$formattersngModel.$modelValueからngModel.$viewValueへ変換する関数を追加する。
  • ngModel.$parsersngModel.$viewValueからngModel.$modelValueへ変換する関数を追加する。

あると嬉しいもの

  • ngModel.$validators.hoge

例:見た目'YYYY/MM/DD'、値がmoment Plunkerでみる

<input type="text" ng-model="date" moment-input>
app.directive('momentInput', () => {
  return {
    priority: -1, // ngModelのpriorityが0なので-1以下にする
    restrict: 'A',
    require: '?ngModel',
    controller: function($element) {
      var ngModel = $element.controller('ngModel');
      if (!ngModel) return;
      // $modelValue to $viewValue
      ngModel.$formatters.push(value => value && moment(value).format('YYYY/MM/DD'));
      // $viewValue to $modelValue
      ngModel.$parsers.push(value => value && moment(value, 'YYYY/MM/DD', true));
      // ついでにvalidate
      ngModel.$validators.date = modelValue => {
        if (!modelValue) return true;
        return modelValue.isValid();
      };
    }
  }
})

中身が複雑なdirectiveを隠ぺいする

とにかくデータのやりとりはng-modelでやりたい、他のライブラリとかをラップしたい等。インターフェースがAngular標準のdirectiveと同じになるので使い方を共有しやすい。

実装すべきもの

  • directive内部で値が更新したときにngModel.$setViewValueでngModelの値を明示的に更新する処理を書く。
  • ngModel.$render、(ngModel.$formattersでもいいかも?)に外部から値が変更されたときの処理を書く。

あると嬉しいもの

  • ngModel.$isEmptyに空判定を書けばrequiredが(lengthプロパティを持っている場合はmaxlengthminlengthも)効く。
  • ngModel.$validators.hoge

例: チェックボックスが複数あって、値を配列で扱う Plunkerでみる(上のURLと同じものです)

<checkbox-group ng-model="arr" items="items" required minlength="2" maxlength="3"></checkbox-group>
app.directive('checkboxGroup', () => {
  return {
    scope: {},
    priority: -1,
    restrict: 'E',
    require: '?ngModel',
    bindToController: {
      items: '='
    },
    template: `
    <div>
      <label for="checkbox-{{$id}}" ng-repeat="item in ctrl.items">
        <input id="checkbox-{{$id}}" type="checkbox" ng-model="item.selected" ng-change="ctrl.update()">{{item.label}}
      </label>
    </div>
    `,
    controller: function($element) {
      var ngModel = $element.controller('ngModel');
      if (!ngModel) return;

      // 空判定
      ngModel.$isEmpty = value => {
        return !(angular.isArray(value) && value.length);
      };
      // 外から更新された時の処理
      ngModel.$render = () => {
        var value = ngModel.$viewValue;
        if (!angular.isArray(value) || !angular.isArray(this.items)) return value;
        this.items.forEach(item => item.selected = false);
        value.forEach(v => this.items.some(item => item.selected = v === item.value));
        return value;
      };
      // 内部で更新された時の処理
      this.update = () => {
        ngModel.$setViewValue(this.items.filter(item => item.selected).map(item => item.value));
      };
    },
    controllerAs: 'ctrl'
  }
})

ng-modelにオブジェクトを設定したときの注意点

ng-modelに設定された値(ng-model="ctrl.foo"とします)がinvalidとなる場合、ctrl.fooundefinedになります。この挙動は割と困る場合が多いかと思われます。ng-model-optionsallowInvalidtrueとすることで防ぐことができます。

<div ng-model-options="{allowInvalid: true}">
  <hoge ng-model="ctrl.foo"></hoge>
  <div>{{ctrl.foo.firstName}} {{ctrl.foo.lastName}}</div>
</div>

formとの連携

name属性を付けるだけ。FormControllerから辿れるようになります。下の例の場合はhoge.foohoge.bar。form全体のvalidationはnameがなくても適用されます。ngModel.$validatorsをしっかり作っておくと、controllerでのvalidationや、viewにエラーを表示する部分で楽できます。

<form name="hoge">
  <checkbox-group name="foo" ng-model="foo" items="items" required></checkbox-group>
  <input type="text" name="bar" ng-model="moment" moment-input>
</form>

ng-changeとの連携

ng-modelを使うともれなくng-changeを使えるようになります。ただし、ng-modelにオブジェクトが設定されているときはプロパティだけ変えても何も起こりません(!==で判定してる)。

32
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
36