From f2dfa8916f8ed855d55187f5400c4c2566ce9a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 1 Aug 2013 20:13:36 -0400 Subject: [PATCH] feat($compile): support compile animation hooks classes --- src/ng/compile.js | 138 ++++++++++++++++++++++++++---------- src/ng/directive/ngClass.js | 17 ++--- test/ng/compileSpec.js | 51 +++++++++++++ 3 files changed, 156 insertions(+), 50 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 6aebe537752d..340263b31918 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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; - } + //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 { + var booleanKey = getBooleanAttrName(this.$$element[0], key), + normalizedVal, + nodeName; - this[key] = value; + if (booleanKey) { + this.$$element.prop(key, value); + attrName = booleanKey; + } - // 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, '-'); + 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, '-'); + } } - } - 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; + 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 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) { }; } }; - }]; + }; } /** diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index c1dedd4ab72c..5dfad4be7f29 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -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('
'); + $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); + })); + }); });