mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
feat($compiler): Allow attr.$observe() interpolated attrs
This commit is contained in:
parent
3df7b8e57f
commit
6d0ca95fa0
2 changed files with 129 additions and 33 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue