mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
feat(*): lazy one-time binding support
Expressions that start with `::` will be binded once. The rule
that binding follows is that the binding will take the first
not-undefined value at the end of a $digest cycle.
Watchers from $watch, $watchCollection and $watchGroup will
automatically stop watching when the expression(s) are bind-once
and fulfill.
Watchers from text and attributes interpolations will
automatically stop watching when the expressions are fulfill.
All directives that use $parse for expressions will automatically
work with bind-once expressions. E.g.
<div ng-bind="::foo"></div>
<li ng-repeat="item in ::items">{{::item.name}};</li>
Paired with: Caitlin and Igor
Design doc: https://docs.google.com/document/d/1fTqaaQYD2QE1rz-OywvRKFSpZirbWUPsnfaZaMq8fWI/edit#
Closes #7486
Closes #5408
This commit is contained in:
committed by
Igor Minar
parent
701ed5fdf6
commit
cee429f0aa
@@ -198,3 +198,122 @@ expose a `$event` object within the scope of that expression.
|
||||
|
||||
Note in the example above how we can pass in `$event` to `clickMe`, but how it does not show up
|
||||
in `{{$event}}`. This is because `$event` is outside the scope of that binding.
|
||||
|
||||
|
||||
## One-time binding
|
||||
|
||||
An expression that starts with `::` is considered a one-time expression. One-time expressions
|
||||
will stop recalculating once they are stable, which happens after the first digest if the expression
|
||||
result is a non-undefined value (see value stabilization algorithm below).
|
||||
|
||||
<example module="oneTimeBidingExampleApp">
|
||||
<file name="index.html">
|
||||
<div ng-controller="EventController">
|
||||
<button ng-click="clickMe($event)">Click Me</button>
|
||||
<p id="one-time-binding-example">One time binding: {{::name}}</p>
|
||||
<p id="normal-binding-example">Normal binding: {{name}}</p>
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('oneTimeBidingExampleApp', []).
|
||||
controller('EventController', ['$scope', function($scope) {
|
||||
var counter = 0;
|
||||
var names = ['Igor', 'Misko', 'Chirayu', 'Lucas'];
|
||||
/*
|
||||
* expose the event object to the scope
|
||||
*/
|
||||
$scope.clickMe = function(clickEvent) {
|
||||
$scope.name = names[counter % names.length];
|
||||
counter++;
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should freeze binding after its value has stabilized', function() {
|
||||
var oneTimeBiding = element(by.id('one-time-binding-example'));
|
||||
var normalBinding = element(by.id('normal-binding-example'));
|
||||
|
||||
expect(oneTimeBiding.getText()).toEqual('One time binding:');
|
||||
expect(normalBinding.getText()).toEqual('Normal binding:');
|
||||
element(by.buttonText('Click Me')).click();
|
||||
|
||||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||||
expect(normalBinding.getText()).toEqual('Normal binding: Igor');
|
||||
element(by.buttonText('Click Me')).click();
|
||||
|
||||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||||
expect(normalBinding.getText()).toEqual('Normal binding: Misko');
|
||||
|
||||
element(by.buttonText('Click Me')).click();
|
||||
element(by.buttonText('Click Me')).click();
|
||||
|
||||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||||
expect(normalBinding.getText()).toEqual('Normal binding: Lucas');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
|
||||
### Why this feature
|
||||
|
||||
The main purpose of one-time binding expression is to provide a way to create a binding
|
||||
that gets deregistered and frees up resources once the binding is stabilized.
|
||||
Reducing the number of expressions being watched makes the digest loop faster and allows more
|
||||
information to be displayed at the same time.
|
||||
|
||||
|
||||
### Value stabilization algorithm
|
||||
|
||||
One-time binding expressions will retain the value of the expression at the end of the
|
||||
digest cycle as long as that value is not undefined. If the value of the expression is set
|
||||
within the digest loop and later, within the same digest loop, it is set to undefined,
|
||||
then the expression is not fulfilled and will remain watched.
|
||||
|
||||
1. Given an expression that starts with `::` when a digest loop is entered and expression
|
||||
is dirty-checked store the value as V
|
||||
2. If V is not undefined mark the result of the expression as stable and schedule a task
|
||||
to deregister the watch for this expression when we exit the digest loop
|
||||
3. Process the digest loop as normal
|
||||
4. When digest loop is done and all the values have settled process the queue of watch
|
||||
deregistration tasks. For each watch to be deregistered check if it still evaluates
|
||||
to value that is not `undefined`. If that's the case, deregister the watch. Otherwise
|
||||
keep dirty-checking the watch in the future digest loops by following the same
|
||||
algorithm starting from step 1
|
||||
|
||||
|
||||
### How to benefit from one-time binding
|
||||
|
||||
When interpolating text or attributes. If the expression, once set, will not change
|
||||
then it is a candidate for one-time expression.
|
||||
|
||||
```html
|
||||
<div name="attr: {{::color}}">text: {{::name}}</div>
|
||||
```
|
||||
|
||||
When using a directive with bidirectional binding and the parameters will not change
|
||||
|
||||
```js
|
||||
someModule.directive('someDirective', function() {
|
||||
return {
|
||||
scope: {
|
||||
name: '=',
|
||||
color: '@'
|
||||
},
|
||||
template: '{{name}}: {{color}}'
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<div some-directive name=“::myName” color=“My color is {{::myColor}}”></div>
|
||||
```
|
||||
|
||||
|
||||
When using a directive that takes an expression
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li ng-repeat="item in ::items">{{item.name}};</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
|
||||
@@ -1485,6 +1485,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
parentSet(scope, parentValue = isolateScope[scopeName]);
|
||||
}
|
||||
}
|
||||
parentValueWatch.$$unwatch = parentGet.$$unwatch;
|
||||
return lastValue = parentValue;
|
||||
}, null, parentGet.literal);
|
||||
break;
|
||||
@@ -1813,6 +1814,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
compile: valueFn(function textInterpolateLinkFn(scope, node) {
|
||||
var parent = node.parent(),
|
||||
bindings = parent.data('$binding') || [];
|
||||
// Need to interpolate again in case this is using one-time bindings in multiple clones
|
||||
// of transcluded templates.
|
||||
interpolateFn = $interpolate(text);
|
||||
bindings.push(interpolateFn);
|
||||
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
|
||||
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
|
||||
|
||||
@@ -174,7 +174,11 @@ var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
|
||||
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
|
||||
|
||||
var parsed = $parse(attr.ngBindHtml);
|
||||
function getStringValue() { return (parsed(scope) || '').toString(); }
|
||||
function getStringValue() {
|
||||
var value = parsed(scope);
|
||||
getStringValue.$$unwatch = parsed.$$unwatch;
|
||||
return (value || '').toString();
|
||||
}
|
||||
|
||||
scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
|
||||
element.html($sce.getTrustedHtml(parsed(scope)) || '');
|
||||
|
||||
@@ -309,9 +309,11 @@ function $InterpolateProvider() {
|
||||
|
||||
|
||||
try {
|
||||
interpolationFn.$$unwatch = true;
|
||||
for (; i < ii; i++) {
|
||||
val = getValue(parseFns[i](context));
|
||||
if (allOrNothing && isUndefined(val)) {
|
||||
interpolationFn.$$unwatch = undefined;
|
||||
return;
|
||||
}
|
||||
val = stringify(val);
|
||||
@@ -319,6 +321,7 @@ function $InterpolateProvider() {
|
||||
inputsChanged = true;
|
||||
}
|
||||
values[i] = val;
|
||||
interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch;
|
||||
}
|
||||
|
||||
if (inputsChanged) {
|
||||
|
||||
@@ -1018,13 +1018,19 @@ function $ParseProvider() {
|
||||
$parseOptions.csp = $sniffer.csp;
|
||||
|
||||
return function(exp) {
|
||||
var parsedExpression;
|
||||
var parsedExpression,
|
||||
oneTime;
|
||||
|
||||
switch (typeof exp) {
|
||||
case 'string':
|
||||
|
||||
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
|
||||
oneTime = true;
|
||||
exp = exp.substring(2);
|
||||
}
|
||||
|
||||
if (cache.hasOwnProperty(exp)) {
|
||||
return cache[exp];
|
||||
return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp];
|
||||
}
|
||||
|
||||
var lexer = new Lexer($parseOptions);
|
||||
@@ -1037,7 +1043,11 @@ function $ParseProvider() {
|
||||
cache[exp] = parsedExpression;
|
||||
}
|
||||
|
||||
return parsedExpression;
|
||||
if (parsedExpression.constant) {
|
||||
parsedExpression.$$unwatch = true;
|
||||
}
|
||||
|
||||
return oneTime ? oneTimeWrapper(parsedExpression) : parsedExpression;
|
||||
|
||||
case 'function':
|
||||
return exp;
|
||||
@@ -1045,6 +1055,31 @@ function $ParseProvider() {
|
||||
default:
|
||||
return noop;
|
||||
}
|
||||
|
||||
function oneTimeWrapper(expression) {
|
||||
var stable = false,
|
||||
lastValue;
|
||||
oneTimeParseFn.literal = expression.literal;
|
||||
oneTimeParseFn.constant = expression.constant;
|
||||
oneTimeParseFn.assign = expression.assign;
|
||||
return oneTimeParseFn;
|
||||
|
||||
function oneTimeParseFn(self, locals) {
|
||||
if (!stable) {
|
||||
lastValue = expression(self, locals);
|
||||
oneTimeParseFn.$$unwatch = isDefined(lastValue);
|
||||
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) {
|
||||
self.$$postDigestQueue.push(function () {
|
||||
// create a copy if the value is defined and it is not a $sce value
|
||||
if ((stable = isDefined(lastValue)) && !lastValue.$$unwrapTrustedValue) {
|
||||
lastValue = copy(lastValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return lastValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -338,14 +338,6 @@ function $RootScopeProvider(){
|
||||
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
|
||||
}
|
||||
|
||||
if (typeof watchExp == 'string' && get.constant) {
|
||||
var originalFn = watcher.fn;
|
||||
watcher.fn = function(newVal, oldVal, scope) {
|
||||
originalFn.call(this, newVal, oldVal, scope);
|
||||
arrayRemove(array, watcher);
|
||||
};
|
||||
}
|
||||
|
||||
if (!array) {
|
||||
array = scope.$$watchers = [];
|
||||
}
|
||||
@@ -391,17 +383,28 @@ function $RootScopeProvider(){
|
||||
var deregisterFns = [];
|
||||
var changeCount = 0;
|
||||
var self = this;
|
||||
var unwatchFlags = new Array(watchExpressions.length);
|
||||
var unwatchCount = watchExpressions.length;
|
||||
|
||||
forEach(watchExpressions, function (expr, i) {
|
||||
deregisterFns.push(self.$watch(expr, function (value, oldValue) {
|
||||
var exprFn = $parse(expr);
|
||||
deregisterFns.push(self.$watch(exprFn, function (value, oldValue) {
|
||||
newValues[i] = value;
|
||||
oldValues[i] = oldValue;
|
||||
changeCount++;
|
||||
if (unwatchFlags[i] && !exprFn.$$unwatch) unwatchCount++;
|
||||
if (!unwatchFlags[i] && exprFn.$$unwatch) unwatchCount--;
|
||||
unwatchFlags[i] = exprFn.$$unwatch;
|
||||
}));
|
||||
}, this);
|
||||
|
||||
deregisterFns.push(self.$watch(function () {return changeCount;}, function () {
|
||||
deregisterFns.push(self.$watch(watchGroupFn, function () {
|
||||
listener(newValues, oldValues, self);
|
||||
if (unwatchCount === 0) {
|
||||
watchGroupFn.$$unwatch = true;
|
||||
} else {
|
||||
watchGroupFn.$$unwatch = false;
|
||||
}
|
||||
}));
|
||||
|
||||
return function deregisterWatchGroup() {
|
||||
@@ -409,6 +412,8 @@ function $RootScopeProvider(){
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
function watchGroupFn() {return changeCount;}
|
||||
},
|
||||
|
||||
|
||||
@@ -553,6 +558,7 @@ function $RootScopeProvider(){
|
||||
}
|
||||
}
|
||||
}
|
||||
$watchCollectionWatch.$$unwatch = objGetter.$$unwatch;
|
||||
return changeDetected;
|
||||
}
|
||||
|
||||
@@ -644,6 +650,7 @@ function $RootScopeProvider(){
|
||||
dirty, ttl = TTL,
|
||||
next, current, target = this,
|
||||
watchLog = [],
|
||||
stableWatchesCandidates = [],
|
||||
logIdx, logMsg, asyncTask;
|
||||
|
||||
beginPhase('$digest');
|
||||
@@ -694,6 +701,7 @@ function $RootScopeProvider(){
|
||||
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
|
||||
watchLog[logIdx].push(logMsg);
|
||||
}
|
||||
if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers});
|
||||
} else if (watch === lastDirtyWatch) {
|
||||
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
|
||||
// have already been tested.
|
||||
@@ -740,6 +748,13 @@ function $RootScopeProvider(){
|
||||
$exceptionHandler(e);
|
||||
}
|
||||
}
|
||||
|
||||
for (length = stableWatchesCandidates.length - 1; length >= 0; --length) {
|
||||
var candidate = stableWatchesCandidates[length];
|
||||
if (candidate.watch.get.$$unwatch) {
|
||||
arrayRemove(candidate.array, candidate.watch);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -787,7 +787,9 @@ function $SceProvider() {
|
||||
return parsed;
|
||||
} else {
|
||||
return function sceParseAsTrusted(self, locals) {
|
||||
return sce.getTrusted(type, parsed(self, locals));
|
||||
var result = sce.getTrusted(type, parsed(self, locals));
|
||||
sceParseAsTrusted.$$unwatch = parsed.$$unwatch;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2180,6 +2180,23 @@ describe('$compile', function() {
|
||||
);
|
||||
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(
|
||||
function($rootScope, $compile) {
|
||||
$rootScope.name = 'angular';
|
||||
element = $compile('<div name="attr: {{::name}}">text: {{::name}}</div>')($rootScope);
|
||||
expect($rootScope.$$watchers.length).toBe(2);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('text: angular');
|
||||
expect(element.attr('name')).toEqual('attr: angular');
|
||||
expect($rootScope.$$watchers.length).toBe(0);
|
||||
$rootScope.name = 'not-angular';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('text: angular');
|
||||
expect(element.attr('name')).toEqual('attr: angular');
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
it('should process attribute interpolation in pre-linking phase at priority 100', function() {
|
||||
module(function() {
|
||||
directive('attrLog', function(log) {
|
||||
@@ -2792,6 +2809,83 @@ describe('$compile', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should be possible to one-time bind a parameter on a component with a template', function() {
|
||||
module(function() {
|
||||
directive('otherTplDir', function() {
|
||||
return {
|
||||
scope: {param: '@', anotherParam: '=' },
|
||||
template: 'value: {{param}}, another value {{anotherParam}}'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function countWatches(scope) {
|
||||
var result = 0;
|
||||
while (scope !== null) {
|
||||
result += (scope.$$watchers && scope.$$watchers.length) || 0;
|
||||
result += countWatches(scope.$$childHead);
|
||||
scope = scope.$$nextSibling;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inject(function($rootScope) {
|
||||
compile('<div other-tpl-dir param="{{::foo}}" another-param="::bar"></div>');
|
||||
expect(countWatches($rootScope)).toEqual(3);
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: , another value ');
|
||||
expect(countWatches($rootScope)).toEqual(3);
|
||||
|
||||
$rootScope.foo = 'from-parent';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value ');
|
||||
expect(countWatches($rootScope)).toEqual(2);
|
||||
|
||||
$rootScope.foo = 'not-from-parent';
|
||||
$rootScope.bar = 'some value';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value some value');
|
||||
expect(countWatches($rootScope)).toEqual(1);
|
||||
|
||||
$rootScope.bar = 'some new value';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value some value');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should be possible to one-time bind a parameter on a component with a templateUrl', function() {
|
||||
module(function() {
|
||||
directive('otherTplDir', function() {
|
||||
return {
|
||||
scope: {param: '@', anotherParam: '=' },
|
||||
templateUrl: 'other.html'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $templateCache) {
|
||||
$templateCache.put('other.html', 'value: {{param}}, another value {{anotherParam}}');
|
||||
compile('<div other-tpl-dir param="{{::foo}}" another-param="::bar"></div>');
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: , another value ');
|
||||
|
||||
$rootScope.foo = 'from-parent';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value ');
|
||||
|
||||
$rootScope.foo = 'not-from-parent';
|
||||
$rootScope.bar = 'some value';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value some value');
|
||||
|
||||
$rootScope.bar = 'some new value';
|
||||
$rootScope.$digest();
|
||||
expect(element.html()).toBe('value: from-parent, another value some value');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('attribute', function() {
|
||||
it('should copy simple attribute', inject(function() {
|
||||
compile('<div><span my-component attr="some text">');
|
||||
|
||||
@@ -45,6 +45,43 @@ describe('ngBind*', function() {
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('-0false');
|
||||
}));
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind="::a"></div>')($rootScope);
|
||||
$rootScope.a = 'lucas';
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('lucas');
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
$rootScope.a = undefined;
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('lucas');
|
||||
}));
|
||||
|
||||
it('should be possible to bind to a new value within the same $digest', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind="::a"></div>')($rootScope);
|
||||
$rootScope.$watch('a', function(newVal) { if (newVal === 'foo') { $rootScope.a = 'bar'; } });
|
||||
$rootScope.a = 'foo';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('bar');
|
||||
$rootScope.a = undefined;
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('bar');
|
||||
}));
|
||||
|
||||
it('should remove the binding if the value is defined at the end of a $digest loop', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind="::a"></div>')($rootScope);
|
||||
$rootScope.$watch('a', function(newVal) { if (newVal === 'foo') { $rootScope.a = undefined; } });
|
||||
$rootScope.a = 'foo';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('');
|
||||
$rootScope.a = 'bar';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('bar');
|
||||
$rootScope.a = 'man';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('bar');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -59,6 +96,22 @@ describe('ngBind*', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should one-time bind the expressions that start with ::', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind-template="{{::hello}} {{::name}}!"></div>')($rootScope);
|
||||
$rootScope.name = 'Misko';
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-binding')).toEqual(true);
|
||||
expect(element.text()).toEqual(' Misko!');
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.hello = 'Hello';
|
||||
$rootScope.name = 'Lucas';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('Hello Misko!');
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
}));
|
||||
|
||||
|
||||
it('should render object as JSON ignore $$', inject(function($rootScope, $compile) {
|
||||
element = $compile('<pre>{{ {key:"value", $$key:"hide"} }}</pre>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
@@ -79,6 +132,18 @@ describe('ngBind*', function() {
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind-html="::html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,42 @@ describe('ngRepeat', function() {
|
||||
expect(element.text()).toEqual('shyam;');
|
||||
});
|
||||
|
||||
it('should be possible to use one-time bindings on the collection', function() {
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="item in ::items">{{item.name}};</li>' +
|
||||
'</ul>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
|
||||
scope.items = [{name: 'misko'}, {name:'shyam'}];
|
||||
scope.$digest();
|
||||
expect(element.find('li').length).toEqual(2);
|
||||
expect(element.text()).toEqual('misko;shyam;');
|
||||
scope.items.push({name: 'adam'});
|
||||
scope.$digest();
|
||||
expect(element.find('li').length).toEqual(2);
|
||||
expect(element.text()).toEqual('misko;shyam;');
|
||||
});
|
||||
|
||||
it('should be possible to use one-time bindings on the content', function() {
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="item in items">{{::item.name}};</li>' +
|
||||
'</ul>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
|
||||
scope.items = [{name: 'misko'}, {name:'shyam'}];
|
||||
scope.$digest();
|
||||
expect(element.find('li').length).toEqual(2);
|
||||
expect(element.text()).toEqual('misko;shyam;');
|
||||
scope.items.push({name: 'adam'});
|
||||
scope.$digest();
|
||||
expect(element.find('li').length).toEqual(3);
|
||||
expect(element.text()).toEqual('misko;shyam;adam;');
|
||||
});
|
||||
|
||||
|
||||
it('should iterate over an array-like object', function() {
|
||||
element = $compile(
|
||||
|
||||
@@ -760,9 +760,6 @@ describe('parser', function() {
|
||||
'disallowed! Expression: wrap["d"]');
|
||||
}));
|
||||
|
||||
it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) {
|
||||
scope.getWin = valueFn($window);
|
||||
scope.getDoc = valueFn($document);
|
||||
|
||||
expect(function() {
|
||||
scope.$eval('getWin()', scope);
|
||||
@@ -953,6 +950,69 @@ describe('parser', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('one-time binding', function() {
|
||||
it('should only use the cache when it is not a one-time binding', inject(function($parse) {
|
||||
expect($parse('foo')).toBe($parse('foo'));
|
||||
expect($parse('::foo')).not.toBe($parse('::foo'));
|
||||
}));
|
||||
|
||||
it('should stay stable once the value defined', inject(function($parse, $rootScope) {
|
||||
var fn = $parse('::foo');
|
||||
expect(fn.$$unwatch).not.toBe(true);
|
||||
$rootScope.$watch(fn);
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).not.toBe(true);
|
||||
|
||||
$rootScope.foo = 'bar';
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).toBe(true);
|
||||
expect(fn($rootScope)).toBe('bar');
|
||||
expect(fn()).toBe('bar');
|
||||
|
||||
$rootScope.foo = 'man';
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).toBe(true);
|
||||
expect(fn($rootScope)).toBe('bar');
|
||||
expect(fn()).toBe('bar');
|
||||
}));
|
||||
|
||||
it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope) {
|
||||
var fn = $parse('::foo');
|
||||
$rootScope.$watch(fn);
|
||||
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });
|
||||
|
||||
$rootScope.foo = 'bar';
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).toBe(false);
|
||||
|
||||
$rootScope.foo = 'man';
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).toBe(true);
|
||||
expect(fn($rootScope)).toBe('man');
|
||||
expect(fn()).toBe('man');
|
||||
|
||||
$rootScope.foo = 'shell';
|
||||
$rootScope.$digest();
|
||||
expect(fn.$$unwatch).toBe(true);
|
||||
expect(fn($rootScope)).toBe('man');
|
||||
expect(fn()).toBe('man');
|
||||
}));
|
||||
|
||||
it('should keep a copy of the stable element', inject(function($parse, $rootScope) {
|
||||
var fn = $parse('::foo'),
|
||||
value = {bar: 'bar'};
|
||||
$rootScope.$watch(fn);
|
||||
$rootScope.foo = value;
|
||||
$rootScope.$digest();
|
||||
|
||||
value.baz = 'baz';
|
||||
expect(fn()).toEqual({bar: 'bar'});
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('locals', function() {
|
||||
it('should expose local variables', inject(function($parse) {
|
||||
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
|
||||
|
||||
@@ -114,6 +114,45 @@ describe('Scope', function() {
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
}));
|
||||
|
||||
it('should clean up stable watches on the watch queue', inject(function($rootScope, $parse) {
|
||||
$rootScope.$watch($parse('::foo'), function() {});
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
|
||||
$rootScope.foo = 'foo';
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
}));
|
||||
|
||||
it('should claen up stable watches from $watchCollection', inject(function($rootScope, $parse) {
|
||||
$rootScope.$watchCollection('::foo', function() {});
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
|
||||
$rootScope.foo = [];
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
}));
|
||||
|
||||
it('should clean up stable watches from $watchGroup', inject(function($rootScope, $parse) {
|
||||
$rootScope.$watchGroup(['::foo', '::bar'], function() {});
|
||||
expect($rootScope.$$watchers.length).toEqual(3);
|
||||
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(3);
|
||||
|
||||
$rootScope.foo = 'foo';
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(2);
|
||||
|
||||
$rootScope.bar = 'bar';
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
}));
|
||||
|
||||
it('should delegate exceptions', function() {
|
||||
module(function($exceptionHandlerProvider) {
|
||||
|
||||
@@ -209,6 +209,15 @@ describe('SCE', function() {
|
||||
expect($sce.parseAsJs('"string"')()).toBe("string");
|
||||
}));
|
||||
|
||||
it('should be possible to do one-time binding', inject(function($sce, $rootScope) {
|
||||
var exprFn = $sce.parseAsHtml('::foo');
|
||||
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'trustedValue')})).toBe('trustedValue');
|
||||
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'anotherTrustedValue')})).toBe('anotherTrustedValue');
|
||||
$rootScope.$digest();
|
||||
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'somethingElse')})).toBe('anotherTrustedValue');
|
||||
expect(exprFn.$$unwatch).toBe(true);
|
||||
}));
|
||||
|
||||
it('should NOT parse constant non-literals', inject(function($sce) {
|
||||
// Until there's a real world use case for this, we're disallowing
|
||||
// constant non-literals. See $SceParseProvider.
|
||||
|
||||
Reference in New Issue
Block a user