mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-18 11:31:07 +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/>
|
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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue