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