feat($compiler): Allow attr.$observe() interpolated attrs

This commit is contained in:
Vojta Jina 2012-02-15 11:34:56 -08:00 committed by Misko Hevery
parent 3df7b8e57f
commit 6d0ca95fa0
2 changed files with 129 additions and 33 deletions

View file

@ -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()];
/**
* 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;
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, '-');
}
}
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);
}
});
}
// 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, '-');
/**
* 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);
}
}
if (value === null || value === undefined) {
this.$element.removeAttr(attrName);
} else {
this.$element.attr(attrName, value);
}
}
}];
}
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;

View file

@ -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']);
});
})
});