From 8cca44464e384e0b08ab4aab98ced0c23aad25a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 16 Dec 2013 15:45:57 -0500 Subject: [PATCH] feat(ngAnimate): provide configuration support to match specific className values to trigger animations Closes #5357 Closes #5283 --- src/ng/animate.js | 22 +++++++++++++++ src/ngAnimate/animate.js | 18 ++++++++++--- test/ngAnimate/animateSpec.js | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/ng/animate.js b/src/ng/animate.js index b662d9c18cdf..fa5b936d7ed5 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -61,6 +61,28 @@ var $AnimateProvider = ['$provide', function($provide) { $provide.factory(key, factory); }; + /** + * @ngdoc function + * @name ng.$animateProvider#classNameFilter + * @methodOf ng.$animateProvider + * + * @description + * Sets and/or returns the CSS class regular expression that is checked when performing + * an animation. Upon bootstrap the classNameFilter value is not set at all and will + * therefore enable $animate to attempt to perform an animation on any element. + * When setting the classNameFilter value, animations will only be performed on elements + * that successfully match the filter expression. This in turn can boost performance + * for low-powered devices as well as applications containing a lot of structural operations. + * @param {RegExp=} expression The className expression which will be checked against all animations + * @return {RegExp} The current CSS className expression value. If null then there is no expression value + */ + this.classNameFilter = function(expression) { + if(arguments.length === 1) { + this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + } + return this.$$classNameFilter; + }; + this.$get = ['$timeout', function($timeout) { /** diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 16d0aa0d00e2..08c088e99de0 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -288,6 +288,13 @@ angular.module('ngAnimate', ['ng']) }); }); + var classNameFilter = $animateProvider.classNameFilter(); + var isAnimatableClassName = !classNameFilter + ? function() { return true; } + : function(className) { + return classNameFilter.test(className); + }; + function lookup(name) { if (name) { var matches = [], @@ -569,17 +576,20 @@ angular.module('ngAnimate', ['ng']) and the onComplete callback will be fired once the animation is fully complete. */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - var node = extractElementNode(element); + var currentClassName, classes, node = extractElementNode(element); + if(node) { + currentClassName = node.className; + classes = currentClassName + ' ' + className; + } + //transcluded directives may sometimes fire an animation using only comment nodes //best to catch this early on to prevent any animation operations from occurring - if(!node) { + if(!node || !isAnimatableClassName(classes)) { fireDOMOperation(); closeAnimation(); return; } - var currentClassName = node.className; - var classes = currentClassName + ' ' + className; var animationLookup = (' ' + classes).replace(/\s+/g,'.'); if (!parentElement) { parentElement = afterElement ? afterElement.parent() : element.parent(); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index c9a08fe8ebb6..fedea535b658 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -2947,5 +2947,55 @@ describe("ngAnimate", function() { expect(capturedAnimation).toBe('leave'); }); }); + + it('should animate only the specified CSS className', function() { + var captures = {}; + module(function($animateProvider) { + $animateProvider.classNameFilter(/prefixed-animation/); + $animateProvider.register('.capture', function() { + return { + enter : buildFn('enter'), + leave : buildFn('leave') + }; + + function buildFn(key) { + return function(element, className, done) { + captures[key] = true; + (done || className)(); + } + } + }); + }); + inject(function($rootScope, $compile, $rootElement, $document, $timeout, $templateCache, $sniffer, $animate) { + if(!$sniffer.transitions) return; + + var element = $compile('
')($rootScope); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + var enterDone = false; + $animate.enter(element, $rootElement, null, function() { + enterDone = true; + }); + + $rootScope.$digest(); + $timeout.flush(); + + expect(captures['enter']).toBeUndefined(); + expect(enterDone).toBe(true); + + element.addClass('prefixed-animation'); + + var leaveDone = false; + $animate.leave(element, function() { + leaveDone = true; + }); + $rootScope.$digest(); + $timeout.flush(); + + expect(captures['leave']).toBe(true); + expect(leaveDone).toBe(true); + }); + }); }); });