From 6d0ca95fa05ea6c7bcaef5c1d5a03fa67a6b6d0c Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Wed, 15 Feb 2012 11:34:56 -0800 Subject: feat($compiler): Allow attr.$observe() interpolated attrs --- src/service/compiler.js | 96 +++++++++++++++++++++++++++++--------------- test/service/compilerSpec.js | 66 ++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 33 deletions(-) diff --git a/src/service/compiler.js b/src/service/compiler.js index ef049b50..43db1e9b 100644 --- a/src/service/compiler.js +++ b/src/service/compiler.js @@ -281,7 +281,9 @@ function $CompileProvider($provide) { attrs = { $attr: {}, $normalize: directiveNormalize, - $set: attrSetter + $set: attrSetter, + $observe: interpolatedAttrObserve, + $observers: {} }; // we must always refer to nodeList[i] since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, maxPriority); @@ -861,6 +863,10 @@ function $CompileProvider($provide) { compile: function(element, attr) { if (interpolateFn) { return function(scope, element, attr) { + // we define observers array only for interpolated attrs + // and ignore observers for non interpolated attrs to save some memory + attr.$observers[name] = []; + attr[name] = undefined; scope.$watch(interpolateFn, function(value) { attr.$set(name, value); }); @@ -900,45 +906,69 @@ function $CompileProvider($provide) { } element[0] = newNode; } - }]; - /** - * Set a normalized attribute on the element in a way such that all directives - * can share the attribute. This function properly handles boolean attributes. - * @param {string} key Normalized key. (ie ngAttribute) - * @param {string|boolean} value The value to set. If `null` attribute will be deleted. - * @param {string=} attrName Optional none normalized name. Defaults to key. - */ - function attrSetter(key, value, attrName) { - var booleanKey = BOOLEAN_ATTR[key.toLowerCase()]; - - if (booleanKey) { - value = toBoolean(value); - this.$element.prop(key, value); - this[key] = value; - attrName = key = booleanKey; - value = value ? booleanKey : undefined; - } else { - this[key] = value; - } + /** + * Set a normalized attribute on the element in a way such that all directives + * can share the attribute. This function properly handles boolean attributes. + * @param {string} key Normalized key. (ie ngAttribute) + * @param {string|boolean} value The value to set. If `null` attribute will be deleted. + * @param {string=} attrName Optional none normalized name. Defaults to key. + */ + function attrSetter(key, value, attrName) { + var booleanKey = BOOLEAN_ATTR[key.toLowerCase()]; + + if (booleanKey) { + value = toBoolean(value); + this.$element.prop(key, value); + this[key] = value; + attrName = key = booleanKey; + value = value ? booleanKey : undefined; + } else { + this[key] = value; + } - // translate normalized key to actual key - if (attrName) { - this.$attr[key] = attrName; - } else { - attrName = this.$attr[key]; - if (!attrName) { - this.$attr[key] = attrName = snake_case(key, '-'); + // translate normalized key to actual key + if (attrName) { + this.$attr[key] = attrName; + } else { + attrName = this.$attr[key]; + if (!attrName) { + this.$attr[key] = attrName = snake_case(key, '-'); + } + } + + if (value === null || value === undefined) { + this.$element.removeAttr(attrName); + } else { + this.$element.attr(attrName, value); } + + // fire observers + forEach(this.$observers[key], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); } - if (value === null || value === undefined) { - this.$element.removeAttr(attrName); - } else { - this.$element.attr(attrName, value); + + /** + * Observe an interpolated attribute. + * The observer will never be called, if given attribute is not interpolated. + * + * @param {string} key Normalized key. (ie ngAttribute) . + * @param {function(*)} fn Function that will be called whenever the attribute value changes. + */ + function interpolatedAttrObserve(key, fn) { + // keep only observers for interpolated attrs + if (this.$observers[key]) { + this.$observers[key].push(fn); + } } - } + }]; } var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; diff --git a/test/service/compilerSpec.js b/test/service/compilerSpec.js index c998765c..9af11f33 100644 --- a/test/service/compilerSpec.js +++ b/test/service/compilerSpec.js @@ -1005,6 +1005,19 @@ describe('$compile', function() { describe('interpolation', function() { + var observeSpy, attrValueDuringLinking; + + beforeEach(module(function($compileProvider) { + $compileProvider.directive('observer', function() { + return function(scope, elm, attr) { + observeSpy = jasmine.createSpy('$observe attr'); + + attr.$observe('someAttr', observeSpy); + attrValueDuringLinking = attr.someAttr; + }; + }); + })); + it('should compile and link both attribute and text bindings', inject( function($rootScope, $compile) { @@ -1022,6 +1035,59 @@ describe('$compile', function() { expect(element.hasClass('ng-binding')).toBe(true); expect(element.data('$binding')[0].exp).toEqual('{{1+2}}'); })); + + + it('should observe interpolated attrs', inject(function($rootScope, $compile) { + $compile('
')($rootScope); + + // should be async + expect(observeSpy).not.toHaveBeenCalled(); + + $rootScope.$apply(function() { + $rootScope.value = 'bound-value'; + }); + expect(observeSpy).toHaveBeenCalledOnceWith('bound-value'); + })); + + + it('should set interpolated attrs to undefined', inject(function($rootScope, $compile) { + attrValueDuringLinking = null; + $compile('
')($rootScope); + expect(attrValueDuringLinking).toBeUndefined(); + })); + + + it('should not call observer of non-interpolated attr', inject(function($rootScope, $compile) { + $compile('
')($rootScope); + expect(attrValueDuringLinking).toBe('nonBound'); + + $rootScope.$digest(); + expect(observeSpy).not.toHaveBeenCalled(); + })); + + + it('should delegate exceptions to $exceptionHandler', function() { + observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR'); + + module(function($compileProvider, $exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + $compileProvider.directive('error', function() { + return function(scope, elm, attr) { + attr.$observe('someAttr', observeSpy); + attr.$observe('someAttr', observeSpy); + }; + }); + }); + + inject(function($compile, $rootScope, $exceptionHandler) { + $compile('
')($rootScope); + $rootScope.$digest(); + + expect(observeSpy).toHaveBeenCalled(); + expect(observeSpy.callCount).toBe(2); + expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']); + }); + }) }); -- cgit v1.2.3