aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/service/compiler.js96
-rw-r--r--test/service/compilerSpec.js66
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('<div some-attr="{{value}}" observer></div>')($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('<div some-attr="{{whatever}}" observer></div>')($rootScope);
+ expect(attrValueDuringLinking).toBeUndefined();
+ }));
+
+
+ it('should not call observer of non-interpolated attr', inject(function($rootScope, $compile) {
+ $compile('<div some-attr="nonBound" observer></div>')($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('<div some-attr="{{value}}" error></div>')($rootScope);
+ $rootScope.$digest();
+
+ expect(observeSpy).toHaveBeenCalled();
+ expect(observeSpy.callCount).toBe(2);
+ expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']);
+ });
+ })
});