mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
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:
parent
5c95b8cccc
commit
c3a41ff9fe
5 changed files with 292 additions and 217 deletions
|
|
@ -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:
|
||||
templates. Locals definition is a hash of local scope property to its source:
|
||||
|
||||
* `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"`.
|
||||
* `@` 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).
|
||||
|
||||
* `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"`.
|
||||
* `=` 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`.
|
||||
|
||||
* `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.
|
||||
* `&` 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toEqual('C');
|
||||
expect(log).toEqual('C');
|
||||
$rootScope.bi.nd = 'c';
|
||||
$rootScope.$apply();
|
||||
expect(log).toEqual('C; c');
|
||||
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(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() {
|
||||
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() {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue