diff --git a/src/ng/compile.js b/src/ng/compile.js
index ccf8aba56253..eb7b84018217 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -1505,8 +1505,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrs[attrName], newIsolateScopeDirective.name);
};
lastValue = isolateScope[scopeName] = parentGet(scope);
- isolateScope.$watch(function parentValueWatch() {
- var parentValue = parentGet(scope);
+ var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) {
if (!compare(parentValue, isolateScope[scopeName])) {
// we are out of sync and need to copy
if (!compare(parentValue, lastValue)) {
@@ -1517,9 +1516,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentSet(scope, parentValue = isolateScope[scopeName]);
}
}
- parentValueWatch.$$unwatch = parentGet.$$unwatch;
return lastValue = parentValue;
- }, null, parentGet.literal);
+ }), null, parentGet.literal);
+ isolateScope.$on('$destroy', unwatch);
break;
case '&':
@@ -1855,9 +1854,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return 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);
parent.data('$binding', bindings);
if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js
index 794d84df8ecb..88db9ef1ca61 100644
--- a/src/ng/directive/ngBind.js
+++ b/src/ng/directive/ngBind.js
@@ -181,13 +181,13 @@ var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
var parsed = $parse(attr.ngBindHtml);
- function getStringValue() {
- var value = parsed(scope);
- getStringValue.$$unwatch = parsed.$$unwatch;
- return (value || '').toString();
- }
+ var changeDetector = $parse(attr.ngBindHtml, function getStringValue(value) {
+ return (value || '').toString();
+ });
- scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
+ scope.$watch(changeDetector, function ngBindHtmlWatchAction() {
+ // we re-evaluate the expr because we want a TrustedValueHolderType
+ // for $sce, not a string
element.html($sce.getTrustedHtml(parsed(scope)) || '');
});
};
diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js
index 830c548277c2..2f0a949f6d81 100644
--- a/src/ng/interpolate.js
+++ b/src/ng/interpolate.js
@@ -195,8 +195,7 @@ function $InterpolateProvider() {
hasInterpolation = false,
hasText = false,
exp,
- concat = [],
- lastValuesCache = { values: {}, results: {}};
+ concat = [];
while(index < textLength) {
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
@@ -205,7 +204,7 @@ function $InterpolateProvider() {
separators.push(text.substring(index, startIndex));
exp = text.substring(startIndex + startSymbolLength, endIndex);
expressions.push(exp);
- parseFns.push($parse(exp));
+ parseFns.push($parse(exp, parseStringifyInterceptor));
index = endIndex + endSymbolLength;
hasInterpolation = true;
} else {
@@ -246,6 +245,7 @@ function $InterpolateProvider() {
var compute = function(values) {
for(var i = 0, ii = expressions.length; i < ii; i++) {
+ if (allOrNothing && isUndefined(values[i])) return;
concat[2*i] = separators[i];
concat[(2*i)+1] = values[i];
}
@@ -254,13 +254,9 @@ function $InterpolateProvider() {
};
var getValue = function (value) {
- if (trustedContext) {
- value = $sce.getTrusted(trustedContext, value);
- } else {
- value = $sce.valueOf(value);
- }
-
- return value;
+ return trustedContext ?
+ $sce.getTrusted(trustedContext, value) :
+ $sce.valueOf(value);
};
var stringify = function (value) {
@@ -284,64 +280,49 @@ function $InterpolateProvider() {
};
return extend(function interpolationFn(context) {
- var scopeId = (context && context.$id) || 'notAScope';
- var lastValues = lastValuesCache.values[scopeId];
- var lastResult = lastValuesCache.results[scopeId];
var i = 0;
var ii = expressions.length;
var values = new Array(ii);
- var val;
- var inputsChanged = lastResult === undefined ? true: false;
-
-
- // if we haven't seen this context before, initialize the cache and try to setup
- // a cleanup routine that purges the cache when the scope goes away.
- if (!lastValues) {
- lastValues = [];
- inputsChanged = true;
- if (context && context.$on) {
- context.$on('$destroy', function() {
- lastValuesCache.values[scopeId] = null;
- lastValuesCache.results[scopeId] = null;
- });
- }
- }
-
try {
- interpolationFn.$$unwatch = true;
for (; i < ii; i++) {
- val = getValue(parseFns[i](context));
- if (allOrNothing && isUndefined(val)) {
- interpolationFn.$$unwatch = undefined;
- return;
- }
- val = stringify(val);
- if (val !== lastValues[i]) {
- inputsChanged = true;
- }
- values[i] = val;
- interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch;
+ values[i] = parseFns[i](context);
}
- if (inputsChanged) {
- lastValuesCache.values[scopeId] = values;
- lastValuesCache.results[scopeId] = lastResult = compute(values);
- }
+ return compute(values);
} catch(err) {
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
err.toString());
$exceptionHandler(newErr);
}
- return lastResult;
}, {
// all of these properties are undocumented for now
exp: text, //just for compatibility with regular watchers created via $watch
separators: separators,
- expressions: expressions
+ expressions: expressions,
+ $$watchDelegate: function (scope, listener, objectEquality, deregisterNotifier) {
+ var lastValue;
+ return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
+ var currValue = compute(values);
+ if (isFunction(listener)) {
+ listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
+ }
+ lastValue = currValue;
+ }, objectEquality, deregisterNotifier);
+ }
});
}
+
+ function parseStringifyInterceptor(value) {
+ try {
+ return stringify(getValue(value));
+ } catch(err) {
+ var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
+ err.toString());
+ $exceptionHandler(newErr);
+ }
+ }
}
diff --git a/src/ng/parse.js b/src/ng/parse.js
index ba854dc1798d..dbebc5b43ea9 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -991,68 +991,88 @@ function $ParseProvider() {
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
$parseOptions.csp = $sniffer.csp;
- return function(exp) {
- var parsedExpression,
- oneTime;
+ return function(exp, interceptorFn) {
+ var parsedExpression, oneTime,
+ cacheKey = (exp = trim(exp));
switch (typeof exp) {
case 'string':
+ if (cache.hasOwnProperty(cacheKey)) {
+ parsedExpression = cache[cacheKey];
+ } else {
+ if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
+ oneTime = true;
+ exp = exp.substring(2);
+ }
- exp = trim(exp);
-
- if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
- oneTime = true;
- exp = exp.substring(2);
- }
-
- if (cache.hasOwnProperty(exp)) {
- return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp];
- }
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
+ parsedExpression = parser.parse(exp);
- var lexer = new Lexer($parseOptions);
- var parser = new Parser(lexer, $filter, $parseOptions);
- parsedExpression = parser.parse(exp);
+ if (parsedExpression.constant) parsedExpression.$$watchDelegate = constantWatch;
+ else if (oneTime) parsedExpression.$$watchDelegate = oneTimeWatch;
- if (exp !== 'hasOwnProperty') {
- // Only cache the value if it's not going to mess up the cache object
- // This is more performant that using Object.prototype.hasOwnProperty.call
- cache[exp] = parsedExpression;
+ if (cacheKey !== 'hasOwnProperty') {
+ // Only cache the value if it's not going to mess up the cache object
+ // This is more performant that using Object.prototype.hasOwnProperty.call
+ cache[cacheKey] = parsedExpression;
+ }
}
-
- return oneTime || parsedExpression.constant ? oneTimeWrapper(parsedExpression) : parsedExpression;
+ return addInterceptor(parsedExpression, interceptorFn);
case 'function':
- return exp;
+ return addInterceptor(exp, interceptorFn);
default:
- return noop;
+ return addInterceptor(noop, interceptorFn);
}
+ };
- 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.constant && lastValue ? 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 === null || !lastValue.$$unwrapTrustedValue)) {
- lastValue = copy(lastValue, null);
- }
- });
+ function oneTimeWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) {
+ var unwatch, lastValue;
+ return unwatch = scope.$watch(function oneTimeWatch(scope) {
+ return parsedExpression(scope);
+ }, function oneTimeListener(value, old, scope) {
+ lastValue = value;
+ if (isFunction(listener)) {
+ listener.apply(this, arguments);
+ }
+ if (isDefined(value)) {
+ scope.$$postDigest(function () {
+ if (isDefined(lastValue)) {
+ unwatch();
}
- }
- return lastValue;
+ });
+ }
+ }, objectEquality, deregisterNotifier);
+ }
+
+ function constantWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) {
+ var unwatch;
+ return unwatch = scope.$watch(function constantWatch(scope) {
+ return parsedExpression(scope);
+ }, function constantListener(value, old, scope) {
+ if (isFunction(listener)) {
+ listener.apply(this, arguments);
}
+ unwatch();
+ }, objectEquality, deregisterNotifier);
+ }
+
+ function addInterceptor(parsedExpression, interceptorFn) {
+ if (isFunction(interceptorFn)) {
+ var fn = function interceptedExpression(scope, locals) {
+ var value = parsedExpression(scope, locals);
+ var result = interceptorFn(value, scope, locals);
+ // we only return the interceptor's result if the
+ // initial value is defined (for bind-once)
+ return isDefined(value) ? result : value;
+ };
+ fn.$$watchDelegate = parsedExpression.$$watchDelegate;
+ return fn;
+ } else {
+ return parsedExpression;
}
- };
+ }
}];
}
diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js
index 23990427a63b..044a37cbbbfe 100644
--- a/src/ng/rootScope.js
+++ b/src/ng/rootScope.js
@@ -324,11 +324,17 @@ function $RootScopeProvider(){
*
* @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of
* comparing for reference equality.
+ * @param {function()=} deregisterNotifier Function to call when the deregistration function
+ * get called.
* @returns {function()} Returns a deregistration function for this listener.
*/
- $watch: function(watchExp, listener, objectEquality) {
+ $watch: function(watchExp, listener, objectEquality, deregisterNotifier) {
+ var get = compileToFn(watchExp, 'watch');
+
+ if (get.$$watchDelegate) {
+ return get.$$watchDelegate(this, listener, objectEquality, deregisterNotifier, get);
+ }
var scope = this,
- get = compileToFn(watchExp, 'watch'),
array = scope.$$watchers,
watcher = {
fn: listener,
@@ -354,6 +360,9 @@ function $RootScopeProvider(){
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
+ if (isFunction(deregisterNotifier)) {
+ deregisterNotifier();
+ }
};
},
@@ -380,7 +389,6 @@ function $RootScopeProvider(){
* and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
* those of `watchExpression`
* The `scope` refers to the current scope.
- *
* @returns {function()} Returns a de-registration function for all listeners.
*/
$watchGroup: function(watchExpressions, listener) {
@@ -389,37 +397,43 @@ function $RootScopeProvider(){
var deregisterFns = [];
var changeCount = 0;
var self = this;
- var unwatchFlags = new Array(watchExpressions.length);
- var unwatchCount = watchExpressions.length;
+ var masterUnwatch;
+
+ if (watchExpressions.length === 1) {
+ // Special case size of one
+ return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) {
+ newValues[0] = value;
+ oldValues[0] = oldValue;
+ listener.call(this, newValues, (value === oldValue) ? newValues : oldValues, scope);
+ });
+ }
forEach(watchExpressions, function (expr, i) {
- var exprFn = $parse(expr);
- deregisterFns.push(self.$watch(exprFn, function (value, oldValue) {
+ var unwatch = self.$watch(expr, function watchGroupSubAction(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;
- }));
+ }, false, function watchGroupDeregNotifier() {
+ arrayRemove(deregisterFns, unwatch);
+ if (!deregisterFns.length) {
+ masterUnwatch();
+ }
+ });
+
+ deregisterFns.push(unwatch);
}, this);
- deregisterFns.push(self.$watch(watchGroupFn, function () {
- listener(newValues, oldValues, self);
- if (unwatchCount === 0) {
- watchGroupFn.$$unwatch = true;
- } else {
- watchGroupFn.$$unwatch = false;
- }
- }));
+ masterUnwatch = self.$watch(function watchGroupChangeWatch() {
+ return changeCount;
+ }, function watchGroupChangeAction(value, oldValue) {
+ listener(newValues, (value === oldValue) ? newValues : oldValues, self);
+ });
return function deregisterWatchGroup() {
- forEach(deregisterFns, function (fn) {
- fn();
- });
+ while (deregisterFns.length) {
+ deregisterFns[0]();
+ }
};
-
- function watchGroupFn() {return changeCount;}
},
@@ -490,14 +504,14 @@ function $RootScopeProvider(){
// only track veryOldValue if the listener is asking for it
var trackVeryOldValue = (listener.length > 1);
var changeDetected = 0;
- var objGetter = $parse(obj);
+ var changeDetector = $parse(obj, $watchCollectionInterceptor);
var internalArray = [];
var internalObject = {};
var initRun = true;
var oldLength = 0;
- function $watchCollectionWatch() {
- newValue = objGetter(self);
+ function $watchCollectionInterceptor(_value) {
+ newValue = _value;
var newLength, key, bothNaN;
if (!isObject(newValue)) { // if primitive
@@ -566,7 +580,6 @@ function $RootScopeProvider(){
}
}
}
- $watchCollectionWatch.$$unwatch = objGetter.$$unwatch;
return changeDetected;
}
@@ -599,7 +612,7 @@ function $RootScopeProvider(){
}
}
- return this.$watch($watchCollectionWatch, $watchCollectionAction);
+ return this.$watch(changeDetector, $watchCollectionAction);
},
/**
@@ -662,7 +675,6 @@ function $RootScopeProvider(){
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
- stableWatchesCandidates = [],
logIdx, logMsg, asyncTask;
beginPhase('$digest');
@@ -713,7 +725,6 @@ 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.
@@ -760,13 +771,6 @@ 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);
- }
- }
},
diff --git a/src/ng/sce.js b/src/ng/sce.js
index 3b1e8ae72802..4a002f941740 100644
--- a/src/ng/sce.js
+++ b/src/ng/sce.js
@@ -787,11 +787,9 @@ function $SceProvider() {
if (parsed.literal && parsed.constant) {
return parsed;
} else {
- return function sceParseAsTrusted(self, locals) {
- var result = sce.getTrusted(type, parsed(self, locals));
- sceParseAsTrusted.$$unwatch = parsed.$$unwatch;
- return result;
- };
+ return $parse(expr, function (value) {
+ return sce.getTrusted(type, value);
+ });
}
};
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index b0de0f63a831..d22b52f075be 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -2218,20 +2218,20 @@ describe('$compile', function() {
);
it('should one-time bind if the expression starts with a space and two colons', inject(
- function($rootScope, $compile) {
- $rootScope.name = 'angular';
- element = $compile('
text: {{ ::name }}
')($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');
- })
- );
+ function($rootScope, $compile) {
+ $rootScope.name = 'angular';
+ element = $compile('text: {{ ::name }}
')($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() {
@@ -2846,15 +2846,7 @@ 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}}'
- };
- });
- });
+ describe('bind-once', function () {
function countWatches(scope) {
var result = 0;
@@ -2866,60 +2858,142 @@ describe('$compile', function() {
return result;
}
- inject(function($rootScope) {
- compile('');
- expect(countWatches($rootScope)).toEqual(3);
- $rootScope.$digest();
- expect(element.html()).toBe('value: , another value ');
- expect(countWatches($rootScope)).toEqual(3);
+ it('should be possible to one-time bind a parameter on a component with a template', function() {
+ module(function() {
+ directive('otherTplDir', function() {
+ return {
+ scope: {param1: '=', param2: '='},
+ template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'
+ };
+ });
+ });
- $rootScope.foo = 'from-parent';
- $rootScope.$digest();
- expect(element.html()).toBe('value: from-parent, another value ');
- expect(countWatches($rootScope)).toEqual(2);
+ inject(function($rootScope) {
+ compile('');
+ expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> '='
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(7);
- $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.foo = 'foo';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:;3:foo;4:');
+ expect(countWatches($rootScope)).toEqual(5);
- $rootScope.bar = 'some new value';
- $rootScope.$digest();
- expect(element.html()).toBe('value: from-parent, another value some value');
+ $rootScope.foo = 'baz';
+ $rootScope.bar = 'bar';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.bar = 'baz';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar');
+ });
});
- });
+ it('should be possible to one-time bind a parameter on a component with a template', function() {
+ module(function() {
+ directive('otherTplDir', function() {
+ return {
+ scope: {param1: '@', param2: '@'},
+ template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'
+ };
+ });
+ });
- 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) {
+ compile('');
+ expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> {{ }}
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(5); // (- 2) -> bind-once in template
+
+ $rootScope.foo = 'foo';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.foo = 'baz';
+ $rootScope.bar = 'bar';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:bar;3:;4:');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.bar = 'baz';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:baz;3:;4:');
});
});
- inject(function($rootScope, $templateCache) {
- $templateCache.put('other.html', 'value: {{param}}, another value {{anotherParam}}');
- compile('');
- $rootScope.$digest();
- expect(element.html()).toBe('value: , another value ');
+ it('should be possible to one-time bind a parameter on a component with a template', function() {
+ module(function() {
+ directive('otherTplDir', function() {
+ return {
+ scope: {param1: '=', param2: '='},
+ templateUrl: 'other.html'
+ };
+ });
+ });
- $rootScope.foo = 'from-parent';
- $rootScope.$digest();
- expect(element.html()).toBe('value: from-parent, another value ');
+ inject(function($rootScope, $templateCache) {
+ $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}');
+ compile('');
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> '='
- $rootScope.foo = 'not-from-parent';
- $rootScope.bar = 'some value';
- $rootScope.$digest();
- expect(element.html()).toBe('value: from-parent, another value some value');
+ $rootScope.foo = 'foo';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:;3:foo;4:');
+ expect(countWatches($rootScope)).toEqual(5);
- $rootScope.bar = 'some new value';
- $rootScope.$digest();
- expect(element.html()).toBe('value: from-parent, another value some value');
+ $rootScope.foo = 'baz';
+ $rootScope.bar = 'bar';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.bar = 'baz';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar');
+ });
+ });
+
+ it('should be possible to one-time bind a parameter on a component with a template', function() {
+ module(function() {
+ directive('otherTplDir', function() {
+ return {
+ scope: {param1: '@', param2: '@'},
+ templateUrl: 'other.html'
+ };
+ });
+ });
+
+ inject(function($rootScope, $templateCache) {
+ $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}');
+ compile('');
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(5); // (5 - 2) -> template watch group, 2 -> {{ }}
+
+ $rootScope.foo = 'foo';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:;3:;4:');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.foo = 'baz';
+ $rootScope.bar = 'bar';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:bar;3:;4:');
+ expect(countWatches($rootScope)).toEqual(4);
+
+ $rootScope.bar = 'baz';
+ $rootScope.$digest();
+ expect(element.html()).toBe('1:foo;2:baz;3:;4:');
+ });
});
+
});
diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js
index 0029da5fec16..3635d0516d9e 100644
--- a/test/ng/directive/ngBindSpec.js
+++ b/test/ng/directive/ngBindSpec.js
@@ -99,11 +99,11 @@ describe('ngBind*', function() {
it('should one-time bind the expressions that start with ::', inject(function($rootScope, $compile) {
element = $compile('')($rootScope);
$rootScope.name = 'Misko';
- expect($rootScope.$$watchers.length).toEqual(1);
+ expect($rootScope.$$watchers.length).toEqual(3);
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual(' Misko!');
- expect($rootScope.$$watchers.length).toEqual(1);
+ expect($rootScope.$$watchers.length).toEqual(2);
$rootScope.hello = 'Hello';
$rootScope.name = 'Lucas';
$rootScope.$digest();
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index ec8aed4faa41..327aee71a14d 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -215,6 +215,8 @@ describe('parser', function() {
window.document.securityPolicy = originalSecurityPolicy;
});
+ beforeEach(module(provideLog));
+
beforeEach(inject(function ($rootScope) {
scope = $rootScope;
}));
@@ -780,7 +782,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);
@@ -1079,65 +1080,73 @@ 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) {
+ it('should always use the cache', inject(function($parse) {
expect($parse('foo')).toBe($parse('foo'));
- expect($parse('::foo')).not.toBe($parse('::foo'));
+ expect($parse('::foo')).toBe($parse('::foo'));
}));
- it('should stay stable once the value defined', inject(function($parse, $rootScope) {
+ it('should not affect calling the parseFn directly', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
- expect(fn.$$unwatch).not.toBe(true);
$rootScope.$watch(fn);
+ $rootScope.foo = 'bar';
+ expect($rootScope.$$watchers.length).toBe(1);
+ expect(fn($rootScope)).toEqual('bar');
+
+ $rootScope.$digest();
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(fn($rootScope)).toEqual('bar');
+
+ $rootScope.foo = 'man';
+ $rootScope.$digest();
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(fn($rootScope)).toEqual('man');
+
+ $rootScope.foo = 'shell';
+ $rootScope.$digest();
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(fn($rootScope)).toEqual('shell');
+ }));
+
+ it('should stay stable once the value defined', inject(function($parse, $rootScope, log) {
+ var fn = $parse('::foo');
+ $rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });
+
$rootScope.$digest();
- expect(fn.$$unwatch).not.toBe(true);
+ expect($rootScope.$$watchers.length).toBe(1);
$rootScope.foo = 'bar';
$rootScope.$digest();
- expect(fn.$$unwatch).toBe(true);
- expect(fn($rootScope)).toBe('bar');
- expect(fn()).toBe('bar');
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(log).toEqual('bar');
+ log.reset();
$rootScope.foo = 'man';
$rootScope.$digest();
- expect(fn.$$unwatch).toBe(true);
- expect(fn($rootScope)).toBe('bar');
- expect(fn()).toBe('bar');
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(log).toEqual('');
}));
- it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope) {
+ it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope, log) {
var fn = $parse('::foo');
- $rootScope.$watch(fn);
+ $rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });
$rootScope.foo = 'bar';
$rootScope.$digest();
- expect(fn.$$unwatch).toBe(false);
+ expect($rootScope.$$watchers.length).toBe(2);
+ expect(log).toEqual('');
$rootScope.foo = 'man';
$rootScope.$digest();
- expect(fn.$$unwatch).toBe(true);
- expect(fn($rootScope)).toBe('man');
- expect(fn()).toBe('man');
+ expect($rootScope.$$watchers.length).toBe(1);
+ expect(log).toEqual('; 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'});
+ expect($rootScope.$$watchers.length).toBe(1);
+ expect(log).toEqual('; man');
}));
it('should not throw if the stable value is `null`', inject(function($parse, $rootScope) {
@@ -1149,10 +1158,8 @@ describe('parser', function() {
$rootScope.$digest();
expect(fn()).toEqual(null);
}));
-
});
-
describe('locals', function() {
it('should expose local variables', inject(function($parse) {
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js
index bc0ca80b4f2d..1c26bed7b676 100644
--- a/test/ng/rootScopeSpec.js
+++ b/test/ng/rootScopeSpec.js
@@ -123,8 +123,8 @@ 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() {});
+ it('should clean up stable watches on the watch queue', inject(function($rootScope) {
+ $rootScope.$watch('::foo', function() {});
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
@@ -135,7 +135,7 @@ describe('Scope', function() {
expect($rootScope.$$watchers.length).toEqual(0);
}));
- it('should claen up stable watches from $watchCollection', inject(function($rootScope, $parse) {
+ it('should clean up stable watches from $watchCollection', inject(function($rootScope) {
$rootScope.$watchCollection('::foo', function() {});
expect($rootScope.$$watchers.length).toEqual(1);
@@ -147,7 +147,7 @@ describe('Scope', function() {
expect($rootScope.$$watchers.length).toEqual(0);
}));
- it('should clean up stable watches from $watchGroup', inject(function($rootScope, $parse) {
+ it('should clean up stable watches from $watchGroup', inject(function($rootScope) {
$rootScope.$watchGroup(['::foo', '::bar'], function() {});
expect($rootScope.$$watchers.length).toEqual(3);
@@ -526,6 +526,34 @@ describe('Scope', function() {
expect(log).toEqual(['watch1', 'watchAction1', 'watch2', 'watchAction2', 'watch3', 'watchAction3',
'watch2', 'watch3']);
}));
+
+ describe('deregisterNotifier', function () {
+ it('should call the deregisterNotifier when the watch is deregistered', inject(
+ function($rootScope) {
+ var notifier = jasmine.createSpy('deregisterNotifier');
+ var listenerRemove = $rootScope.$watch('noop', noop, false, notifier);
+
+ expect(notifier).not.toHaveBeenCalled();
+
+ listenerRemove();
+ expect(notifier).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should call the deregisterNotifier when a one-time expression is stable', inject(
+ function($rootScope) {
+ var notifier = jasmine.createSpy('deregisterNotifier');
+ $rootScope.$watch('::foo', noop, false, notifier);
+
+ expect(notifier).not.toHaveBeenCalledOnce();
+ $rootScope.$digest();
+ expect(notifier).not.toHaveBeenCalledOnce();
+
+ $rootScope.foo = 'foo';
+ $rootScope.$digest();
+ expect(notifier).toHaveBeenCalledOnce();
+ }));
+ });
});
@@ -902,6 +930,7 @@ describe('Scope', function() {
scope.$digest();
expect(log).toEqual('');
});
+
});
describe('$destroy', function() {
diff --git a/test/ng/sceSpecs.js b/test/ng/sceSpecs.js
index d3f00d9a010b..9b57eb36eed4 100644
--- a/test/ng/sceSpecs.js
+++ b/test/ng/sceSpecs.js
@@ -209,14 +209,30 @@ 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 be possible to do one-time binding', function () {
+ module(provideLog);
+ inject(function($sce, $rootScope, log) {
+ $rootScope.$watch($sce.parseAsHtml('::foo'), function(value) {
+ log(value+'');
+ });
+
+ $rootScope.$digest();
+ expect(log).toEqual('undefined'); // initial listener call
+ log.reset();
+
+ $rootScope.foo = $sce.trustAs($sce.HTML, 'trustedValue');
+ expect($rootScope.$$watchers.length).toBe(1);
+ $rootScope.$digest();
+
+ expect($rootScope.$$watchers.length).toBe(0);
+ expect(log).toEqual('trustedValue');
+ log.reset();
+
+ $rootScope.foo = $sce.trustAs($sce.HTML, 'anotherTrustedValue');
+ $rootScope.$digest();
+ expect(log).toEqual(''); // watcher no longer active
+ });
+ });
it('should NOT parse constant non-literals', inject(function($sce) {
// Until there's a real world use case for this, we're disallowing