Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 435f690

Browse files
committed
feat($animate): add support for customFilter
This commit adds a new `customFilter()` function on `$animateProvider` (similar to `classNameFilter()`), which can be used to filter animations (i.e. decide whether they are allowed or not), based on the return value of a custom filter function. This allows to easily create arbitrarily complex rules for filtering animations, such as allowing specific events only, or enabling animations on specific subtrees of the DOM, etc. Fixes #14891
1 parent 6ebb093 commit 435f690

File tree

3 files changed

+182
-14
lines changed

3 files changed

+182
-14
lines changed

src/ng/animate.js

+40
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ var $$CoreAnimateQueueProvider = /** @this */ function() {
179179
*/
180180
var $AnimateProvider = ['$provide', /** @this */ function($provide) {
181181
var provider = this;
182+
var customFilter = null;
182183

183184
this.$$registeredAnimations = Object.create(null);
184185

@@ -231,6 +232,45 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
231232
$provide.factory(key, factory);
232233
};
233234

235+
/**
236+
* @ngdoc method
237+
* @name $animateProvider#customFilter
238+
*
239+
* @description
240+
* Sets and/or returns the custom filter function that is used to "filter" animations, i.e.
241+
* determine if an animation is allowed or not. When no filter is specified (the default), no
242+
* animation will be blocked. Setting the `customFilter` value will only allow animations for
243+
* which the filter function's return value is truthy.
244+
*
245+
* This allows to easily create arbitrarily complex rules for filtering animations, such as
246+
* allowing specific events only, or enabling animations on specific subtrees of the DOM, etc.
247+
* Filtering animations can also boost performance for low-powered devices, as well as
248+
* applications containing a lot of structural operations.
249+
*
250+
* <div class="alert alert-success">
251+
* **Best Practice:**
252+
* The filtering function will be called for each animation, so try to keep it as lean as
253+
* possible. Performing computationally expensive or time-consuming operations can make your
254+
* animations sluggish.
255+
* </div>
256+
*
257+
* @param {Function=} filterFn - The filter function which will be used to filter all animations.
258+
* If a falsy value is returned, no animation will be performed. The function will be called
259+
* with the following arguments:
260+
* - **node** `{DOMElement}` - The DOM element to be animated.
261+
* - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass`
262+
* etc).
263+
* - **options** `{Object}` - A collection of options/styles used for the animation.
264+
* @return {Function} The current filter function or `null` if there is none set.
265+
*/
266+
this.customFilter = function(filterFn) {
267+
if (arguments.length === 1) {
268+
customFilter = isFunction(filterFn) ? filterFn : null;
269+
}
270+
271+
return customFilter;
272+
};
273+
234274
/**
235275
* @ngdoc method
236276
* @name $animateProvider#classNameFilter

src/ngAnimate/animateQueue.js

+13-14
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,17 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
160160

161161
var callbackRegistry = Object.create(null);
162162

163-
// remember that the classNameFilter is set during the provider/config
164-
// stage therefore we can optimize here and setup a helper function
163+
// remember that the `customFilter`/`classNameFilter` are set during the
164+
// provider/config stage therefore we can optimize here and setup helper functions
165+
var customFilter = $animateProvider.customFilter();
165166
var classNameFilter = $animateProvider.classNameFilter();
166-
var isAnimatableClassName = !classNameFilter
167-
? function() { return true; }
168-
: function(node, options) {
169-
var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
170-
return classNameFilter.test(className);
171-
};
167+
var returnTrue = function() { return true; };
168+
169+
var isAnimatableByFilter = customFilter || returnTrue;
170+
var isAnimatableClassName = !classNameFilter ? returnTrue : function(node, options) {
171+
var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
172+
return classNameFilter.test(className);
173+
};
172174

173175
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
174176

@@ -355,12 +357,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
355357
// there are situations where a directive issues an animation for
356358
// a jqLite wrapper that contains only comment nodes... If this
357359
// happens then there is no way we can perform an animation
358-
if (!node) {
359-
close();
360-
return runner;
361-
}
362-
363-
if (!isAnimatableClassName(node, options)) {
360+
if (!node ||
361+
!isAnimatableByFilter(node, event, initialOptions) ||
362+
!isAnimatableClassName(node, options)) {
364363
close();
365364
return runner;
366365
}

test/ngAnimate/animateSpec.js

+129
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,135 @@ describe('animations', function() {
295295
});
296296
});
297297

298+
describe('customFilter()', function() {
299+
it('should be `null` by default', module(function($animateProvider) {
300+
expect($animateProvider.customFilter()).toBeNull();
301+
}));
302+
303+
it('should clear the `customFilter` if no function is passed',
304+
module(function($animateProvider) {
305+
$animateProvider.customFilter(angular.noop);
306+
expect($animateProvider.customFilter()).toEqual(jasmine.any(Function));
307+
308+
$animateProvider.customFilter(null);
309+
expect($animateProvider.customFilter()).toBeNull();
310+
311+
$animateProvider.customFilter(angular.noop);
312+
expect($animateProvider.customFilter()).toEqual(jasmine.any(Function));
313+
314+
$animateProvider.customFilter({});
315+
expect($animateProvider.customFilter()).toBeNull();
316+
})
317+
);
318+
319+
it('should only perform animations for which the function returns a truthy value',
320+
function() {
321+
var animationsAllowed = false;
322+
323+
module(function($animateProvider) {
324+
$animateProvider.customFilter(function() { return animationsAllowed; });
325+
});
326+
327+
inject(function($animate, $rootScope) {
328+
$animate.enter(element, parent);
329+
$rootScope.$digest();
330+
expect(capturedAnimation).toBeNull();
331+
332+
$animate.leave(element, parent);
333+
$rootScope.$digest();
334+
expect(capturedAnimation).toBeNull();
335+
336+
animationsAllowed = true;
337+
338+
$animate.enter(element, parent);
339+
$rootScope.$digest();
340+
expect(capturedAnimation).not.toBeNull();
341+
342+
capturedAnimation = null;
343+
344+
$animate.leave(element, parent);
345+
$rootScope.$digest();
346+
expect(capturedAnimation).not.toBeNull();
347+
});
348+
}
349+
);
350+
351+
it('should only perform animations for which the function returns a truthy value (SVG)',
352+
function() {
353+
var animationsAllowed = false;
354+
355+
module(function($animateProvider) {
356+
$animateProvider.customFilter(function() { return animationsAllowed; });
357+
});
358+
359+
inject(function($animate, $compile, $rootScope) {
360+
var svgElement = $compile('<svg class="element"></svg>')($rootScope);
361+
362+
$animate.enter(svgElement, parent);
363+
$rootScope.$digest();
364+
expect(capturedAnimation).toBeNull();
365+
366+
$animate.leave(svgElement, parent);
367+
$rootScope.$digest();
368+
expect(capturedAnimation).toBeNull();
369+
370+
animationsAllowed = true;
371+
372+
$animate.enter(svgElement, parent);
373+
$rootScope.$digest();
374+
expect(capturedAnimation).not.toBeNull();
375+
376+
capturedAnimation = null;
377+
378+
$animate.leave(svgElement, parent);
379+
$rootScope.$digest();
380+
expect(capturedAnimation).not.toBeNull();
381+
});
382+
}
383+
);
384+
385+
it('should pass the DOM element, event name and options to the filter function', function() {
386+
var filterFn = jasmine.createSpy('filterFn');
387+
var options = {};
388+
389+
module(function($animateProvider) {
390+
$animateProvider.customFilter(filterFn);
391+
});
392+
393+
inject(function($animate, $rootScope) {
394+
$animate.enter(element, parent, null, options);
395+
expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'enter', options);
396+
397+
filterFn.calls.reset();
398+
399+
$animate.leave(element);
400+
expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'leave', jasmine.any(Object));
401+
});
402+
});
403+
404+
it('should complete the DOM operation even if filtered out', function() {
405+
module(function($animateProvider) {
406+
$animateProvider.customFilter(function() { return false; });
407+
});
408+
409+
inject(function($animate, $rootScope) {
410+
expect(element.parent()[0]).toBeUndefined();
411+
412+
$animate.enter(element, parent);
413+
$rootScope.$digest();
414+
415+
expect(capturedAnimation).toBeNull();
416+
expect(element.parent()[0]).toBe(parent[0]);
417+
418+
$animate.leave(element);
419+
$rootScope.$digest();
420+
421+
expect(capturedAnimation).toBeNull();
422+
expect(element.parent()[0]).toBeUndefined();
423+
});
424+
});
425+
});
426+
298427
describe('enabled()', function() {
299428
it('should work for all animations', inject(function($animate) {
300429

0 commit comments

Comments
 (0)