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

Commit 6b70025

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 ee587cc commit 6b70025

File tree

4 files changed

+212
-20
lines changed

4 files changed

+212
-20
lines changed

docs/content/guide/animations.ngdoc

+30-6
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,37 @@ myModule.run(function($animate) {
281281

282282
## How to (selectively) enable, disable and skip animations
283283

284-
There are three different ways to disable animations, both globally and for specific animations.
284+
There are several different ways to disable animations, both globally and for specific animations.
285285
Disabling specific animations can help to speed up the render performance, for example for large
286286
`ngRepeat` lists that don't actually have animations. Because `ngAnimate` checks at runtime if
287287
animations are present, performance will take a hit even if an element has no animation.
288288

289-
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
289+
### During the config: {@link $animateProvider#customFilter $animateProvider.customFilter()}
290290

291291
This function can be called during the {@link angular.Module#config config} phase of an app. It
292+
takes a filter function as the only argument, which will then be used to "filter" animations (based
293+
on the animated element, the event type, and the animation options). Only when the filter function
294+
returns `true`, will the animation be performed. This allows great flexibility - you can easily
295+
create complex rules, such as allowing specific events only or enabling animations on specific
296+
subtrees of the DOM, and dynamically modify them, for example disabling animations at certain points
297+
in time or under certain circumstances.
298+
299+
```js
300+
app.config(function($animateProvider) {
301+
$animateProvider.customFilter(function(node, event, options) {
302+
// Example: Only animate `enter` and `leave` operations.
303+
return event === 'enter' || event === 'leave';
304+
});
305+
});
306+
```
307+
308+
The `customFilter` approach generally gives a big speed boost compared to other strategies, because
309+
the matching is done before other animation disabling strategies are checked. However, the filter
310+
function has to be kept as lean as possible, since it will be executed for each animation.
311+
312+
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
313+
314+
This function too can be called during the {@link angular.Module#config config} phase of an app. It
292315
takes a regex as the only argument, which will then be matched against the classes of any element
293316
that is about to be animated. The regex allows a lot of flexibility - you can either allow
294317
animations for specific classes only (useful when you are working with 3rd party animations), or
@@ -308,10 +331,11 @@ app.config(function($animateProvider) {
308331
}
309332
```
310333

311-
The `classNameFilter` approach generally gives the biggest speed boost, because the matching is done
312-
before any other animation disabling strategies are checked. However, that also means it is not
313-
possible to override class name matching with the two following strategies. It's of course still
314-
possible to enable / disable animations by changing an element's class name at runtime.
334+
The `classNameFilter` approach generally gives a big speed boost compared to other strategies,
335+
because the matching is done before other animation disabling strategies are checked. However, that
336+
also means it is not possible to override class name matching with the two following strategies.
337+
It's of course still possible to enable / disable animations by changing an element's class name at
338+
runtime.
315339

316340
### At runtime: {@link ng.$animate#enabled $animate.enabled()}
317341

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)