diff options
| author | Misko Hevery | 2012-06-06 13:58:10 -0700 | 
|---|---|---|
| committer | Igor Minar | 2012-06-08 15:50:13 -0700 | 
| commit | c3a41ff9fefe894663c4d4f40a83794521deb14f (patch) | |
| tree | b44037cfb0089cfea42f253b6ad1a09ccb7e2d86 | |
| parent | 5c95b8cccc0d72f7ca3afb1162b9528c1222eb3c (diff) | |
| download | angular.js-c3a41ff9fefe894663c4d4f40a83794521deb14f.tar.bz2 | |
feat($compile): simplify isolate scope bindings
Changed the isolate scope binding options to:
  - @attr - attribute binding (including interpolation)
  - =model - by-directional model binding
  - &expr - expression execution binding
This change simplifies the terminology as well as
number of choices available to the developer. It
also supports local name aliasing from the parent.
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.
To migrate the code follow the example below:
Before:
scope: {
  myAttr: 'attribute',
  myBind: 'bind',
  myExpression: 'expression',
  myEval: 'evaluate',
  myAccessor: 'accessor'
}
After:
scope: {
  myAttr: '@',
  myBind: '@',
  myExpression: '&',
  // myEval - usually not useful, but in cases where the expression is assignable, you can use '='
  myAccessor: '=' // in directive's template change myAccessor() to myAccessor
}
The removed `inject` wasn't generaly useful for directives so there should be no code using it.
| -rw-r--r-- | docs/content/guide/directive.ngdoc | 84 | ||||
| -rw-r--r-- | src/ng/compile.js | 121 | ||||
| -rw-r--r-- | src/ng/directive/input.js | 21 | ||||
| -rw-r--r-- | test/ng/compileSpec.js | 256 | ||||
| -rw-r--r-- | test/ng/directive/inputSpec.js | 29 | 
5 files changed, 293 insertions, 218 deletions
| diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 26df46df..8657a5d4 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -321,34 +321,32 @@ compiler}. The attributes are:        parent scope. <br/>        The 'isolate' scope takes an object hash which defines a set of local scope properties        derived from the parent scope. These local properties are useful for aliasing values for -      templates. Locals definition is a hash of normalized element attribute name to their -      corresponding binding strategy. Valid binding strategies are: - -      * `attribute` - one time read of element attribute value and save it to widget scope. <br/> -        Given `<widget my-attr='abc'>` and widget definition of `scope: {myAttr:'attribute'}`, -        then widget scope property `myAttr` will be `"abc"`. - -      * `evaluate` - one time evaluation of expression stored in the attribute. <br/> Given -        `<widget my-attr='name'>` and widget definition of `scope: {myAttr:'evaluate'}`, and -        parent scope `{name:'angular'}` then widget scope property `myAttr` will be `"angular"`. - -      * `bind` - Set up one way binding from the element attribute to the widget scope. <br/> -        Given `<widget my-attr='{{name}}'>` and widget definition of `scope: {myAttr:'bind'}`, -        and parent scope `{name:'angular'}` then widget scope property `myAttr` will be -        `"angular"`, but any changes in the parent scope will be reflected in the widget scope. - -      * `accessor` - Set up getter/setter function for the expression in the widget element -        attribute to the widget scope. <br/> Given `<widget my-attr='name'>` and widget definition -        of `scope: {myAttr:'prop'}`, and parent scope `{name:'angular'}` then widget scope -        property `myAttr` will be a function such that `myAttr()` will return `"angular"` and -        `myAttr('new value')` will update the parent scope `name` property. This is useful for -        treating the element as a data-model for reading/writing. - -      * `expression` - Treat element attribute as an expression to be executed on the parent scope. -        <br/> -        Given `<widget my-attr='doSomething()'>` and widget definition of `scope: -        {myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then calling the -        widget scope function `myAttr` will execute the expression against the parent scope. +      templates. Locals definition is a hash of local scope property to its source: + +      * `@` or `@attr` - bind a local scope property to the DOM attribute. The result is always a +        string since DOM attributes are strings. If no `attr` name is specified then the local name +        and attribute name are same. Given `<widget my-attr="hello {{name}}">` and widget definition +        of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect +        the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the +        `localName` property on the widget scope. The `name` is read from the parent scope (not +        component scope). + +      * `=` or `=expression` - set up bi-directional binding between a local scope property and the +        parent scope property. If no `attr` name is specified then the local name and attribute +        name are same. Given `<widget my-attr="parentModel">` and widget definition of +        `scope: { localModel:'=myAttr' }`, then widget scope property `localName` will reflect the +        value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected +        in `localModel` and any changes in `localModel` will reflect in `parentModel`. + +      * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. +        If no `attr` name is specified then the local name and attribute name are same. +        Given `<widget my-attr="count = count + value">` and widget definition of +        `scope: { localFn:'increment()' }`, then isolate scope property `localFn` will point to +        a function wrapper for the `increment()` expression. Often it's desirable to pass data from +        the isolate scope via an expression and to the parent scope, this can be done by passing a +        map of local variable names and values into the expression wrapper fn. For example if the +        expression is `increment(amount)` then we can specify the amount value by calling the +        `localFn` as `localFn({amount: 22})`.    * `controller` - Controller constructor function. The controller is instantiated before the      pre-linking phase and it is shared with other directives if they request it by name (see @@ -369,32 +367,6 @@ compiler}. The attributes are:      * `^` - Look for the controller on parent elements as well. -  * `inject` (object hash) -  Specifies a way to inject bindings into a controller. Injection -    definition is a hash of normalized element attribute names to their corresponding binding -    strategy. Valid binding strategies are: - -    * `attribute` - inject attribute value. <br/> -      Given `<widget my-attr='abc'>` and widget definition of `inject: {myAttr:'attribute'}`, then -      `myAttr` will inject `"abc"`. - -    * `evaluate` - inject one time evaluation of expression stored in the attribute. <br/> -      Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'evaluate'}`, and -      parent scope `{name:'angular'}` then `myAttr` will inject `"angular"`. - -    * `accessor` - inject a getter/setter function for the expression in the widget element -      attribute to the widget scope. <br/> -      Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'prop'}`, and -      parent scope `{name:'angular'}` then injecting `myAttr` will inject a function such -      that `myAttr()` will return `"angular"` and `myAttr('new value')` will update the parent -      scope `name` property. This is usefull for treating the element as a data-model for -      reading/writing. - -    * `expression` - Inject expression function. <br/> -      Given `<widget my-attr='doSomething()'>` and widget definition of -      `inject: {myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then -      injecting `myAttr` will inject a function which when called will execute the expression -      against the parent scope. -    * `restrict` - String of subset of `EACM` which restricts the directive to a specific directive      declaration style. If omitted directives are allowed on attributes only. @@ -649,9 +621,9 @@ Following is an example of building a reusable widget.             // This HTML will replace the zippy directive.             replace: true,             transclude: true, -           scope: { zippyTitle:'bind' }, +           scope: { title:'@zippyTitle' },             template: '<div>' + -                       '<div class="title">{{zippyTitle}}</div>' + +                       '<div class="title">{{title}}</div>' +                         '<div class="body" ng-transclude></div>' +                       '</div>',             // The linking function will add behavior to the template diff --git a/src/ng/compile.js b/src/ng/compile.js index ee120263..e1aba35b 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -18,6 +18,9 @@   */ +var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; + +  /**   * @ngdoc function   * @name angular.module.ng.$compile @@ -225,47 +228,6 @@ function $CompileProvider($provide) {      function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache,   $parse,               $controller,   $rootScope) { -    var LOCAL_MODE = { -      attribute: function(localName, mode, parentScope, scope, attr) { -        scope[localName] = attr[localName]; -      }, - -      evaluate: function(localName, mode, parentScope, scope, attr) { -        scope[localName] = parentScope.$eval(attr[localName]); -      }, - -      bind: function(localName, mode, parentScope, scope, attr) { -        var getter = $interpolate(attr[localName]); -        scope.$watch( -          function() { return getter(parentScope); }, -          function(v) { scope[localName] = v; } -        ); -      }, - -      accessor: function(localName, mode, parentScope, scope, attr) { -        var getter = noop, -            setter = noop, -            exp = attr[localName]; - -        if (exp) { -          getter = $parse(exp); -          setter = getter.assign || function() { -            throw Error("Expression '" + exp + "' not assignable."); -          }; -        } - -        scope[localName] = function(value) { -          return arguments.length ? setter(parentScope, value) : getter(parentScope); -        }; -      }, - -      expression: function(localName, mode, parentScope, scope, attr) { -        scope[localName] = function(locals) { -          $parse(attr[localName])(parentScope, locals); -        }; -      } -    }; -      var Attributes = function(element, attr) {        this.$$element = element;        this.$attr = attr || {}; @@ -746,9 +708,67 @@ function $CompileProvider($provide) {          $element = attrs.$$element;          if (newScopeDirective && isObject(newScopeDirective.scope)) { -          forEach(newScopeDirective.scope, function(mode, name) { -            (LOCAL_MODE[mode] || wrongMode)(name, mode, -                scope.$parent || scope, scope, attrs); +          var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/; + +          var parentScope = scope.$parent || scope; + +          forEach(newScopeDirective.scope, function(definiton, scopeName) { +            var match = definiton.match(LOCAL_REGEXP) || [], +                attrName = match[2]|| scopeName, +                mode = match[1], // @, =, or & +                lastValue, +                parentGet, parentSet; + +            switch (mode) { + +              case '@': { +                attrs.$observe(attrName, function(value) { +                  scope[scopeName] = value; +                }); +                attrs.$$observers[attrName].$$scope = parentScope; +                break; +              } + +              case '=': { +                parentGet = $parse(attrs[attrName]); +                parentSet = parentGet.assign || function() { +                  // reset the change, or we will throw this exception on every $digest +                  lastValue = scope[scopeName] = parentGet(parentScope); +                  throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] + +                      ' (directive: ' + newScopeDirective.name + ')'); +                }; +                lastValue = scope[scopeName] = parentGet(parentScope); +                scope.$watch(function() { +                  var parentValue = parentGet(parentScope); + +                  if (parentValue !== scope[scopeName]) { +                    // we are out of sync and need to copy +                    if (parentValue !== lastValue) { +                      // parent changed and it has precedence +                      lastValue = scope[scopeName] = parentValue; +                    } else { +                      // if the parent can be assigned then do so +                      parentSet(parentScope, lastValue = scope[scopeName]); +                    } +                  } +                  return parentValue; +                }); +                break; +              } + +              case '&': { +                parentGet = $parse(attrs[attrName]); +                scope[scopeName] = function(locals) { +                  return parentGet(parentScope, locals); +                } +                break; +              } + +              default: { +                throw Error('Invalid isolate scope definition for directive ' + +                    newScopeDirective.name + ': ' + definiton); +              } +            }            });          } @@ -761,12 +781,6 @@ function $CompileProvider($provide) {                $transclude: boundTranscludeFn              }; - -            forEach(directive.inject || {}, function(mode, name) { -              (LOCAL_MODE[mode] || wrongMode)(name, mode, -                  newScopeDirective ? scope.$parent || scope : scope, locals, attrs); -            }); -              controller = directive.controller;              if (controller == '@') {                controller = attrs[directive.name]; @@ -1007,9 +1021,10 @@ function $CompileProvider($provide) {            attr[name] = undefined;            ($$observers[name] || ($$observers[name] = [])).$$inter = true; -          scope.$watch(interpolateFn, function(value) { -            attr.$set(name, value); -          }); +          (attr.$$observers && attr.$$observers[name].$$scope || scope). +            $watch(interpolateFn, function(value) { +              attr.$set(name, value); +            });          })        });      } diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index aa79082b..04af4c2a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -857,8 +857,8 @@ var VALID_CLASS = 'ng-valid',   * </example>   *   */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element', -    function($scope, $exceptionHandler, $attr, ngModel, $element) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', +    function($scope, $exceptionHandler, $attr, $element, $parse) {    this.$viewValue = Number.NaN;    this.$modelValue = Number.NaN;    this.$parsers = []; @@ -870,6 +870,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e    this.$invalid = false;    this.$name = $attr.name; +  var ngModelGet = $parse($attr.ngModel), +      ngModelSet = ngModelGet.assign; + +  if (!ngModelSet) { +    throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel + +        ' (' + startingTag($element) + ')'); +  } +    /**     * @ngdoc function     * @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render @@ -974,7 +982,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e      if (this.$modelValue !== value) {        this.$modelValue = value; -      ngModel(value); +      ngModelSet($scope, value);        forEach(this.$viewChangeListeners, function(listener) {          try {            listener(); @@ -987,9 +995,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e    // model -> value    var ctrl = this; -  $scope.$watch(function() { -    return ngModel(); -  }, function(value) { +  $scope.$watch(ngModelGet, function(value) {      // ignore change from view      if (ctrl.$modelValue === value) return; @@ -1044,9 +1050,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e   */  var ngModelDirective = function() {    return { -    inject: { -      ngModel: 'accessor' -    },      require: ['ngModel', '^?form'],      controller: NgModelController,      link: function(scope, element, attr, ctrls) { diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index d2b0360c..93183b93 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1,7 +1,7 @@  'use strict';  describe('$compile', function() { -  var element, directive; +  var element, directive, $compile, $rootScope;    beforeEach(module(provideLog, function($provide, $compileProvider){      element = null; @@ -54,8 +54,17 @@ describe('$compile', function() {        priority: -100, // even with negative priority we still should be able to stop descend        terminal: true      })); + +    return function(_$compile_, _$rootScope_) { +      $rootScope = _$rootScope_; +      $compile = _$compile_; +    };    })); +  function compile(html) { +    element = angular.element(html); +    $compile(element)($rootScope); +  }    afterEach(function(){      dealoc(element); @@ -1633,105 +1642,166 @@ describe('$compile', function() {    }); -  describe('locals', function() { -    it('should marshal to locals', function() { -      module(function() { -        directive('widget', function(log) { -          return { -            scope: { -              attr: 'attribute', -              prop: 'evaluate', -              bind: 'bind', -              assign: 'accessor', -              read: 'accessor', -              exp: 'expression', -              nonExist: 'accessor', -              nonExistExpr: 'expression' -            }, -            link: function(scope, element, attrs) { -              scope.nonExist(); // noop -              scope.nonExist(123); // noop -              scope.nonExistExpr(); // noop -              scope.nonExistExpr(123); // noop -              log(scope.attr); -              log(scope.prop); -              log(scope.assign()); -              log(scope.read()); -              log(scope.assign('ng')); -              scope.exp({myState:'OK'}); -              expect(function() { scope.read(undefined); }). -                  toThrow("Expression ''D'' not assignable."); -              scope.$watch('bind', log); -            } -          }; -        }); +  describe('isolated locals', function() { +    var componentScope; + +    beforeEach(module(function() { +      directive('myComponent', function() { +        return { +          scope: { +            attr: '@', +            attrAlias: '@attr', +            ref: '=', +            refAlias: '= ref', +            expr: '&', +            exprAlias: '&expr' +          }, +          link: function(scope) { +            componentScope = scope; +          } +        };        }); -      inject(function(log, $compile, $rootScope) { -        $rootScope.myProp = 'B'; -        $rootScope.bi = {nd: 'C'}; -        $rootScope.name = 'C'; -        element = $compile( -            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + -                'exp="state=myState">{{bind}}</div></div>') -            ($rootScope); -        expect(log).toEqual('A; B; C; D; ng'); -        expect($rootScope.name).toEqual('ng'); -        expect($rootScope.state).toEqual('OK'); -        log.reset(); +      directive('badDeclaration', function() { +        return { +          scope: { attr: 'xxx' } +        }; +      }); +    })); + +    describe('attribute', function() { +      it('should copy simple attribute', inject(function() { +        compile('<div><span my-component attr="some text">'); +        expect(componentScope.attr).toEqual(undefined); +        expect(componentScope.attrAlias).toEqual(undefined); +          $rootScope.$apply(); -        expect(element.text()).toEqual('C'); -        expect(log).toEqual('C'); -        $rootScope.bi.nd = 'c'; + +        expect(componentScope.attr).toEqual('some text'); +        expect(componentScope.attrAlias).toEqual('some text'); +        expect(componentScope.attrAlias).toEqual(componentScope.attr); +      })); + + +      it('should update when interpolated attribute updates', inject(function() { +        compile('<div><span my-component attr="hello {{name}}">'); +        expect(componentScope.attr).toEqual(undefined); +        expect(componentScope.attrAlias).toEqual(undefined); + +        $rootScope.name = 'misko';          $rootScope.$apply(); -        expect(log).toEqual('C; c'); -      }); + +        expect(componentScope.attr).toEqual('hello misko'); +        expect(componentScope.attrAlias).toEqual('hello misko'); + +        $rootScope.name = 'igor'; +        $rootScope.$apply(); + +        expect(componentScope.attr).toEqual('hello igor'); +        expect(componentScope.attrAlias).toEqual('hello igor'); +      }));      }); -  }); -  describe('controller', function() { -    it('should inject locals to controller', function() { -      module(function() { -        directive('widget', function(log) { -          return { -            controller: function(attr, prop, assign, read, exp){ -              log(attr); -              log(prop); -              log(assign()); -              log(read()); -              log(assign('ng')); -              exp(); -              expect(function() { read(undefined); }). -                  toThrow("Expression ''D'' not assignable."); -              this.result = 'OK'; -            }, -            inject: { -              attr: 'attribute', -              prop: 'evaluate', -              assign: 'accessor', -              read: 'accessor', -              exp: 'expression' -            }, -            link: function(scope, element, attrs, controller) { -              log(controller.result); -            } -          }; -        }); -      }); -      inject(function(log, $compile, $rootScope) { -        $rootScope.myProp = 'B'; -        $rootScope.bi = {nd: 'C'}; -        $rootScope.name = 'C'; -        element = $compile( -            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + -                'exp="state=\'OK\'">{{bind}}</div></div>') -            ($rootScope); -        expect(log).toEqual('A; B; C; D; ng; OK'); -        expect($rootScope.name).toEqual('ng'); -      }); +    describe('object reference', function() { +      it('should update local when origin changes', inject(function() { +        compile('<div><span my-component ref="name">'); +        expect(componentScope.ref).toBe(undefined); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        $rootScope.name = 'misko'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); + +        $rootScope.name = {}; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); +      })); + + +      it('should update local when origin changes', inject(function() { +        compile('<div><span my-component ref="name">'); +        expect(componentScope.ref).toBe(undefined); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        componentScope.ref = 'misko'; +        $rootScope.$apply(); +        expect($rootScope.name).toBe('misko'); +        expect(componentScope.ref).toBe('misko'); +        expect($rootScope.name).toBe(componentScope.ref); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        componentScope.name = {}; +        $rootScope.$apply(); +        expect($rootScope.name).toBe(componentScope.ref); +        expect(componentScope.refAlias).toBe(componentScope.ref); +      })); + + +      it('should update local when both change', inject(function() { +        compile('<div><span my-component ref="name">'); +        $rootScope.name = {mark:123}; +        componentScope.ref = 'misko'; + +        $rootScope.$apply(); +        expect($rootScope.name).toEqual({mark:123}) +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); + +        $rootScope.name = 'igor'; +        componentScope.ref = {}; +        $rootScope.$apply(); +        expect($rootScope.name).toEqual('igor') +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); +      })); + +      it('should complain on non assignable changes', inject(function() { +        compile('<div><span my-component ref="\'hello \' + name">'); +        $rootScope.name = 'world'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe('hello world'); + +        componentScope.ref = 'ignore me'; +        expect($rootScope.$apply). +            toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)"); +        expect(componentScope.ref).toBe('hello world'); +        // reset since the exception was rethrown which prevented phase clearing +        $rootScope.$$phase = null; + +        $rootScope.name = 'misko'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe('hello misko'); +      })); +    }); + + +    describe('executable expression', function() { +      it('should allow expression execution with locals', inject(function() { +        compile('<div><span my-component expr="count = count + offset">'); +        $rootScope.count = 2; + +        expect(typeof componentScope.expr).toBe('function'); +        expect(typeof componentScope.exprAlias).toBe('function'); + +        expect(componentScope.expr({offset: 1})).toEqual(3); +        expect($rootScope.count).toEqual(3); + +        expect(componentScope.exprAlias({offset: 10})).toEqual(13); +        expect($rootScope.count).toEqual(13); +      }));      }); +    it('should throw on unknown definition', inject(function() { +      expect(function() { +        compile('<div><span bad-declaration>'); +      }).toThrow('Invalid isolate scope definition for directive badDeclaration: xxx'); +    })); +  }); + +  describe('controller', function() {      it('should get required controller', function() {        module(function() {          directive('main', function(log) { @@ -1986,11 +2056,11 @@ describe('$compile', function() {        module(function() {          directive('box', valueFn({            transclude: 'content', -          scope: { name: 'evaluate', show: 'accessor' }, +          scope: { name: '=', show: '=' },            template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>',            link: function(scope, element) {              scope.$watch( -                function() { return scope.show(); }, +                'show',                  function(show) {                    if (!show) {                      element.find('div').find('div').remove(); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index d7ca7aea..3b511011 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -4,7 +4,7 @@ describe('NgModelController', function() {    var ctrl, scope, ngModelAccessor, element, parentFormCtrl;    beforeEach(inject(function($rootScope, $controller) { -    var attrs = {name: 'testAlias'}; +    var attrs = {name: 'testAlias', ngModel: 'value'};      parentFormCtrl = {        $setValidity: jasmine.createSpy('$setValidity'), @@ -17,12 +17,7 @@ describe('NgModelController', function() {      scope = $rootScope;      ngModelAccessor = jasmine.createSpy('ngModel accessor');      ctrl = $controller(NgModelController, { -      $scope: scope, $element: element.find('input'), ngModel: ngModelAccessor, $attrs: attrs -    }); -    // mock accessor (locals) -    ngModelAccessor.andCallFake(function(val) { -      if (isDefined(val)) scope.value = val; -      return scope.value; +      $scope: scope, $element: element.find('input'), $attrs: attrs      });    })); @@ -32,6 +27,26 @@ describe('NgModelController', function() {    }); +  it('should fail on non-assignable model binding', inject(function($controller) { +    var exception; + +    try { +      $controller(NgModelController, { +        $scope: null, +        $element: jqLite('<input ng-model="1+2">'), +        $attrs: { +          ngModel: '1+2' +        } +      }); +    } catch (e) { +      exception = e; +    } + +    expect(exception.message). +        toMatch(/Non-assignable model expression: 1\+2 \(<input( value="")? ng-model="1\+2">\)/); +  })); + +    it('should init the properties', function() {      expect(ctrl.$dirty).toBe(false);      expect(ctrl.$pristine).toBe(true); | 
