feat($compile): support compile animation hooks classes

This commit is contained in:
Matias Niemelä 2013-08-01 20:13:36 -04:00 committed by Misko Hevery
parent d45ac7707e
commit f2dfa8916f
3 changed files with 158 additions and 52 deletions

View file

@ -274,9 +274,9 @@ function $CompileProvider($provide) {
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils',
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate',
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope, $document, $sce, $$urlUtils) {
$controller, $rootScope, $document, $sce, $$urlUtils, $animate) {
var Attributes = function(element, attr) {
this.$$element = element;
@ -287,6 +287,42 @@ function $CompileProvider($provide) {
$normalize: directiveNormalize,
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$addClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Adds the CSS class value specified by the classVal parameter to the element. If animations
* are enabled then an animation will be triggered for the class addition.
*
* @param {string} classVal The className value that will be added to the element
*/
$addClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.addClass(this.$$element, classVal);
}
},
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$removeClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Removes the CSS class value specified by the classVal parameter from the element. If animations
* are enabled then an animation will be triggered for the class removal.
*
* @param {string} classVal The className value that will be removed from the element
*/
$removeClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.removeClass(this.$$element, classVal);
}
},
/**
* Set a normalized attribute on the element in a way such that all directives
* can share the attribute. This function properly handles boolean attributes.
@ -297,54 +333,64 @@ function $CompileProvider($provide) {
* @param {string=} attrName Optional none normalized name. Defaults to key.
*/
$set: function(key, value, writeAttr, attrName) {
var booleanKey = getBooleanAttrName(this.$$element[0], key),
$$observers = this.$$observers,
normalizedVal,
nodeName;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
}
this[key] = value;
// translate normalized key to actual key
if (attrName) {
this.$attr[key] = attrName;
//special case for class attribute addition + removal
//so that class changes can tap into the animation
//hooks provided by the $animate service
if(key == 'class') {
value = value || '';
var current = this.$$element.attr('class') || '';
this.$removeClass(tokenDifference(current, value).join(' '));
this.$addClass(tokenDifference(value, current).join(' '));
} else {
attrName = this.$attr[key];
if (!attrName) {
this.$attr[key] = attrName = snake_case(key, '-');
var booleanKey = getBooleanAttrName(this.$$element[0], key),
normalizedVal,
nodeName;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
}
}
nodeName = nodeName_(this.$$element);
this[key] = value;
// sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) {
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = $$urlUtils.resolve(value);
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
// 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, '-');
}
}
nodeName = nodeName_(this.$$element);
// sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) {
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = $$urlUtils.resolve(value);
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
}
}
}
}
if (writeAttr !== false) {
if (value === null || value === undefined) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
}
}
}
if (writeAttr !== false) {
if (value === null || value === undefined) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
}
}
// fire observers
var $$observers = this.$$observers;
$$observers && forEach($$observers[key], function(fn) {
try {
fn(value);
@ -352,6 +398,22 @@ function $CompileProvider($provide) {
$exceptionHandler(e);
}
});
function tokenDifference(str1, str2) {
var values = [],
tokens1 = str1.split(/\s+/),
tokens2 = str2.split(/\s+/);
outer:
for(var i=0;i<tokens1.length;i++) {
var token = tokens1[i];
for(var j=0;j<tokens2.length;j++) {
if(token == tokens2[j]) continue outer;
}
values.push(token);
}
return values;
};
},

View file

@ -2,7 +2,7 @@
function classDirective(name, selector) {
name = 'ngClass' + name;
return ['$animate', function($animate) {
return function() {
return {
restrict: 'AC',
link: function(scope, element, attr) {
@ -11,8 +11,7 @@ function classDirective(name, selector) {
scope.$watch(attr[name], ngClassWatchAction, true);
attr.$observe('class', function(value) {
var ngClass = scope.$eval(attr[name]);
ngClassWatchAction(ngClass, ngClass);
ngClassWatchAction(scope.$eval(attr[name]));
});
@ -42,18 +41,12 @@ function classDirective(name, selector) {
function removeClass(classVal) {
classVal = flattenClasses(classVal);
if(classVal && classVal.length > 0) {
$animate.removeClass(element, classVal);
}
attr.$removeClass(flattenClasses(classVal));
}
function addClass(classVal) {
classVal = flattenClasses(classVal);
if(classVal && classVal.length > 0) {
$animate.addClass(element, classVal);
}
attr.$addClass(flattenClasses(classVal));
}
function flattenClasses(classVal) {
@ -73,7 +66,7 @@ function classDirective(name, selector) {
};
}
};
}];
};
}
/**

View file

@ -3268,4 +3268,55 @@ describe('$compile', function() {
expect(spans.eq(3)).toBeHidden();
}));
});
describe('$animate animation hooks', function() {
beforeEach(module('mock.animate'));
it('should automatically fire the addClass and removeClass animation hooks',
inject(function($compile, $animate, $rootScope) {
var data, element = jqLite('<div class="{{val1}} {{val2}} fire"></div>');
$compile(element)($rootScope);
$rootScope.$digest();
data = $animate.flushNext('removeClass');
expect(element.hasClass('fire')).toBe(true);
$rootScope.val1 = 'ice';
$rootScope.val2 = 'rice';
$rootScope.$digest();
data = $animate.flushNext('addClass');
expect(data.params[1]).toBe('ice rice');
expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('rice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);
$rootScope.val2 = 'dice';
$rootScope.$digest();
data = $animate.flushNext('removeClass');
expect(data.params[1]).toBe('rice');
data = $animate.flushNext('addClass');
expect(data.params[1]).toBe('dice');
expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('dice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);
$rootScope.val1 = '';
$rootScope.val2 = '';
$rootScope.$digest();
data = $animate.flushNext('removeClass');
expect(data.params[1]).toBe('ice dice');
expect(element.hasClass('ice')).toBe(false);
expect(element.hasClass('dice')).toBe(false);
expect(element.hasClass('fire')).toBe(true);
}));
});
});