自作directive内でng-modelをきちんと動作させる

  • 39
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

追記:require: '^ngModel'で解決しました。

自作directive内でng-modelを使ってみました。

デモ:Plunker - ng-model in a my directive.

作ってみるとわかりますが、自分で作ったdirectiveからng-modelを使おうとおもったらけっこう大変!
なんとisolated scope内では親スコープのngModelControllerを参照できないんですね。
ただ、isolated scopeにしないとせっかくのAngularJSでテンプレートが使えないという(複数個要素を作ると干渉するので)。

これを解決するためにscope.$new()で子スコープを作って、$compileを使ってテンプレートを手動でコンパイルした上で、さらに手動で親と子をバインドしてみました(以下コード例)。面倒くさいですが、現状わかっている範囲だと、こんなかんじで実装しないとng-changeが動かなかったり、ngModelControllerの便利なメソッドも使えないのでこれがベストかと思われます。

修正前

app.js
var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
  $scope.onChange = function () {
    console.log('name is changed!');
  };
  $scope.onChange2 = function () {
    console.log('name is changed!!');
  }
  $scope.onChange3 = function () {
    console.log('name is changed!!!');
  }
});

app.directive('hoge', function ($compile) {
  var template = '<label>{{label}}</label><input ng-model="model" type="text">';
  return {
    restrict: 'E',
    replace: true,
    require: '?ngModel',
    template: '<div></div>',
    link: function (scope, element, attrs, ngModelCtrl) {
      // 子スコープを作って
      var child = scope.$new();
      // テンプレートをコンパイルして
      element.append($compile(template)(child));
      attrs.$observe('label', function() {
        child.label = attrs.label;
      });
      // 親から子にバインドして
      scope.$watch(attrs.ngModel, function (value) {
        child.model = value;
      });
      // 子から親にバインドする
      child.$watch('model', function (value) {
        ngModelCtrl.$setViewValue(value);
      });
    }
  }
});
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.2.x" src="http://code.angularjs.org/1.2.3/angular.js" data-semver="1.2.3"></script>
    <script src="app.js"></script>
  </head>

  <body ng-app="plunker">
    <div ng-controller="MainCtrl">
      <input ng-model="name" ng-change="onChange()">
      <p>Hello {{name}}!</p>
      <hoge ng-model="name" label="{{name}}" ng-change="onChange2()"></hoge>
      <hoge ng-model="name" label="Bar" ng-change="onChange3()"></hoge>
    </div>
  </body>
</html>

追記

require: 'ngModel'のところをrequire: '^ngModel'にすることで親ディレクティブのコントローラーを参照できるようです。よくみたらチュートリアルに書いてあったという。

app.js
app.directive('hoge', function() {
  return {
    restrict: 'E',
    require: '^ngModel',
    scope: {
      label: '@'
    },
    template: '<div><label>{{label}}</label><input ng-model="model"></div>',
    replace: true,
    link: function(scope, element, attrs, ctrl) {
      scope.$watch('model', function (value) {
        ctrl.$setViewValue(value);
      });

      scope.$parent.$watch(attrs.ngModel, function (value) {
        scope.model = value;
      });
    }
  }
});

・・・ずいぶん簡単になりました。