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.
This commit is contained in:
Misko Hevery 2012-06-06 13:58:10 -07:00 committed by Igor Minar
parent 5c95b8cccc
commit c3a41ff9fe
5 changed files with 292 additions and 217 deletions

View file

@ -321,34 +321,32 @@ compiler}. The attributes are:
parent scope. <br/> parent scope. <br/>
The 'isolate' scope takes an object hash which defines a set of local scope properties 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 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 templates. Locals definition is a hash of local scope property to its source:
corresponding binding strategy. Valid binding strategies are:
* `attribute` - one time read of element attribute value and save it to widget scope. <br/> * `@` or `@attr` - bind a local scope property to the DOM attribute. The result is always a
Given `<widget my-attr='abc'>` and widget definition of `scope: {myAttr:'attribute'}`, string since DOM attributes are strings. If no `attr` name is specified then the local name
then widget scope property `myAttr` will be `"abc"`. 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).
* `evaluate` - one time evaluation of expression stored in the attribute. <br/> Given * `=` or `=expression` - set up bi-directional binding between a local scope property and the
`<widget my-attr='name'>` and widget definition of `scope: {myAttr:'evaluate'}`, and parent scope property. If no `attr` name is specified then the local name and attribute
parent scope `{name:'angular'}` then widget scope property `myAttr` will be `"angular"`. 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`.
* `bind` - Set up one way binding from the element attribute to the widget scope. <br/> * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
Given `<widget my-attr='{{name}}'>` and widget definition of `scope: {myAttr:'bind'}`, If no `attr` name is specified then the local name and attribute name are same.
and parent scope `{name:'angular'}` then widget scope property `myAttr` will be Given `<widget my-attr="count = count + value">` and widget definition of
`"angular"`, but any changes in the parent scope will be reflected in the widget scope. `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
* `accessor` - Set up getter/setter function for the expression in the widget element the isolate scope via an expression and to the parent scope, this can be done by passing a
attribute to the widget scope. <br/> Given `<widget my-attr='name'>` and widget definition map of local variable names and values into the expression wrapper fn. For example if the
of `scope: {myAttr:'prop'}`, and parent scope `{name:'angular'}` then widget scope expression is `increment(amount)` then we can specify the amount value by calling the
property `myAttr` will be a function such that `myAttr()` will return `"angular"` and `localFn` as `localFn({amount: 22})`.
`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.
* `controller` - Controller constructor function. The controller is instantiated before the * `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 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. * `^` - 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 * `restrict` - String of subset of `EACM` which restricts the directive to a specific directive
declaration style. If omitted directives are allowed on attributes only. 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. // This HTML will replace the zippy directive.
replace: true, replace: true,
transclude: true, transclude: true,
scope: { zippyTitle:'bind' }, scope: { title:'@zippyTitle' },
template: '<div>' + template: '<div>' +
'<div class="title">{{zippyTitle}}</div>' + '<div class="title">{{title}}</div>' +
'<div class="body" ng-transclude></div>' + '<div class="body" ng-transclude></div>' +
'</div>', '</div>',
// The linking function will add behavior to the template // The linking function will add behavior to the template

View file

@ -18,6 +18,9 @@
*/ */
var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
/** /**
* @ngdoc function * @ngdoc function
* @name angular.module.ng.$compile * @name angular.module.ng.$compile
@ -225,47 +228,6 @@ function $CompileProvider($provide) {
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope) { $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) { var Attributes = function(element, attr) {
this.$$element = element; this.$$element = element;
this.$attr = attr || {}; this.$attr = attr || {};
@ -746,9 +708,67 @@ function $CompileProvider($provide) {
$element = attrs.$$element; $element = attrs.$$element;
if (newScopeDirective && isObject(newScopeDirective.scope)) { if (newScopeDirective && isObject(newScopeDirective.scope)) {
forEach(newScopeDirective.scope, function(mode, name) { var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
(LOCAL_MODE[mode] || wrongMode)(name, mode,
scope.$parent || scope, scope, attrs); 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 $transclude: boundTranscludeFn
}; };
forEach(directive.inject || {}, function(mode, name) {
(LOCAL_MODE[mode] || wrongMode)(name, mode,
newScopeDirective ? scope.$parent || scope : scope, locals, attrs);
});
controller = directive.controller; controller = directive.controller;
if (controller == '@') { if (controller == '@') {
controller = attrs[directive.name]; controller = attrs[directive.name];
@ -1007,9 +1021,10 @@ function $CompileProvider($provide) {
attr[name] = undefined; attr[name] = undefined;
($$observers[name] || ($$observers[name] = [])).$$inter = true; ($$observers[name] || ($$observers[name] = [])).$$inter = true;
scope.$watch(interpolateFn, function(value) { (attr.$$observers && attr.$$observers[name].$$scope || scope).
attr.$set(name, value); $watch(interpolateFn, function(value) {
}); attr.$set(name, value);
});
}) })
}); });
} }

View file

@ -857,8 +857,8 @@ var VALID_CLASS = 'ng-valid',
* </example> * </example>
* *
*/ */
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element', var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
function($scope, $exceptionHandler, $attr, ngModel, $element) { function($scope, $exceptionHandler, $attr, $element, $parse) {
this.$viewValue = Number.NaN; this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN; this.$modelValue = Number.NaN;
this.$parsers = []; this.$parsers = [];
@ -870,6 +870,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
this.$invalid = false; this.$invalid = false;
this.$name = $attr.name; 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 * @ngdoc function
* @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render * @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render
@ -974,7 +982,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
if (this.$modelValue !== value) { if (this.$modelValue !== value) {
this.$modelValue = value; this.$modelValue = value;
ngModel(value); ngModelSet($scope, value);
forEach(this.$viewChangeListeners, function(listener) { forEach(this.$viewChangeListeners, function(listener) {
try { try {
listener(); listener();
@ -987,9 +995,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
// model -> value // model -> value
var ctrl = this; var ctrl = this;
$scope.$watch(function() { $scope.$watch(ngModelGet, function(value) {
return ngModel();
}, function(value) {
// ignore change from view // ignore change from view
if (ctrl.$modelValue === value) return; if (ctrl.$modelValue === value) return;
@ -1044,9 +1050,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
*/ */
var ngModelDirective = function() { var ngModelDirective = function() {
return { return {
inject: {
ngModel: 'accessor'
},
require: ['ngModel', '^?form'], require: ['ngModel', '^?form'],
controller: NgModelController, controller: NgModelController,
link: function(scope, element, attr, ctrls) { link: function(scope, element, attr, ctrls) {

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
describe('$compile', function() { describe('$compile', function() {
var element, directive; var element, directive, $compile, $rootScope;
beforeEach(module(provideLog, function($provide, $compileProvider){ beforeEach(module(provideLog, function($provide, $compileProvider){
element = null; element = null;
@ -54,8 +54,17 @@ describe('$compile', function() {
priority: -100, // even with negative priority we still should be able to stop descend priority: -100, // even with negative priority we still should be able to stop descend
terminal: true terminal: true
})); }));
return function(_$compile_, _$rootScope_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
};
})); }));
function compile(html) {
element = angular.element(html);
$compile(element)($rootScope);
}
afterEach(function(){ afterEach(function(){
dealoc(element); dealoc(element);
@ -1633,105 +1642,166 @@ describe('$compile', function() {
}); });
describe('locals', function() { describe('isolated locals', function() {
it('should marshal to locals', function() { var componentScope;
module(function() {
directive('widget', function(log) { beforeEach(module(function() {
return { directive('myComponent', function() {
scope: { return {
attr: 'attribute', scope: {
prop: 'evaluate', attr: '@',
bind: 'bind', attrAlias: '@attr',
assign: 'accessor', ref: '=',
read: 'accessor', refAlias: '= ref',
exp: 'expression', expr: '&',
nonExist: 'accessor', exprAlias: '&expr'
nonExistExpr: 'expression' },
}, link: function(scope) {
link: function(scope, element, attrs) { componentScope = scope;
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);
}
};
});
}); });
inject(function(log, $compile, $rootScope) { directive('badDeclaration', function() {
$rootScope.myProp = 'B'; return {
$rootScope.bi = {nd: 'C'}; scope: { attr: 'xxx' }
$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();
$rootScope.$apply();
expect(element.text()).toEqual('C');
expect(log).toEqual('C');
$rootScope.bi.nd = 'c';
$rootScope.$apply();
expect(log).toEqual('C; c');
}); });
}));
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(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(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('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() { 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');
});
});
it('should get required controller', function() { it('should get required controller', function() {
module(function() { module(function() {
directive('main', function(log) { directive('main', function(log) {
@ -1986,11 +2056,11 @@ describe('$compile', function() {
module(function() { module(function() {
directive('box', valueFn({ directive('box', valueFn({
transclude: 'content', transclude: 'content',
scope: { name: 'evaluate', show: 'accessor' }, scope: { name: '=', show: '=' },
template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>', template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>',
link: function(scope, element) { link: function(scope, element) {
scope.$watch( scope.$watch(
function() { return scope.show(); }, 'show',
function(show) { function(show) {
if (!show) { if (!show) {
element.find('div').find('div').remove(); element.find('div').find('div').remove();

View file

@ -4,7 +4,7 @@ describe('NgModelController', function() {
var ctrl, scope, ngModelAccessor, element, parentFormCtrl; var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
beforeEach(inject(function($rootScope, $controller) { beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias'}; var attrs = {name: 'testAlias', ngModel: 'value'};
parentFormCtrl = { parentFormCtrl = {
$setValidity: jasmine.createSpy('$setValidity'), $setValidity: jasmine.createSpy('$setValidity'),
@ -17,12 +17,7 @@ describe('NgModelController', function() {
scope = $rootScope; scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor'); ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, { ctrl = $controller(NgModelController, {
$scope: scope, $element: element.find('input'), ngModel: ngModelAccessor, $attrs: attrs $scope: scope, $element: element.find('input'), $attrs: attrs
});
// mock accessor (locals)
ngModelAccessor.andCallFake(function(val) {
if (isDefined(val)) scope.value = val;
return scope.value;
}); });
})); }));
@ -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() { it('should init the properties', function() {
expect(ctrl.$dirty).toBe(false); expect(ctrl.$dirty).toBe(false);
expect(ctrl.$pristine).toBe(true); expect(ctrl.$pristine).toBe(true);