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

Commit ab114af

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 2759788 commit ab114af

File tree

4 files changed

+232
-20
lines changed

4 files changed

+232
-20
lines changed

docs/content/guide/animations.ngdoc

+39-6
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,46 @@ myModule.run(function($animate) {
282282

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

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

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

292292
This function can be called during the {@link angular.Module#config config} phase of an app. It
293+
takes a filter function as the only argument, which will then be used to "filter" animations (based
294+
on the animated element, the event type, and the animation options). Only when the filter function
295+
returns `true`, will the animation be performed. This allows great flexibility - you can easily
296+
create complex rules, such as allowing specific events only or enabling animations on specific
297+
subtrees of the DOM, and dynamically modify them, for example disabling animations at certain points
298+
in time or under certain circumstances.
299+
300+
```js
301+
app.config(function($animateProvider) {
302+
$animateProvider.customFilter(function(node, event, options) {
303+
// Example: Only animate `enter` and `leave` operations.
304+
return event === 'enter' || event === 'leave';
305+
});
306+
});
307+
```
308+
309+
The `customFilter` approach generally gives a big speed boost compared to other strategies, because
310+
the matching is done before other animation disabling strategies are checked.
311+
312+
<div class="alert alert-success">
313+
**Best Practice:**
314+
Keep the filtering function as lean as possible, because it will be called for each DOM
315+
action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
316+
See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
317+
directives that support animations.
318+
Performing computationally expensive or time-consuming operations on each call of the
319+
filtering function can make your animations sluggish.
320+
</div>
321+
322+
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
323+
324+
This function too can be called during the {@link angular.Module#config config} phase of an app. It
293325
takes a regex as the only argument, which will then be matched against the classes of any element
294326
that is about to be animated. The regex allows a lot of flexibility - you can either allow
295327
animations for specific classes only (useful when you are working with 3rd party animations), or
@@ -309,10 +341,11 @@ app.config(function($animateProvider) {
309341
}
310342
```
311343

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

317350
### At runtime: {@link ng.$animate#enabled $animate.enabled()}
318351

src/ng/animate.js

+51
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ var $$CoreAnimateQueueProvider = /** @this */ function() {
180180
var $AnimateProvider = ['$provide', /** @this */ function($provide) {
181181
var provider = this;
182182
var classNameFilter = null;
183+
var customFilter = null;
183184

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

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

236+
/**
237+
* @ngdoc method
238+
* @name $animateProvider#customFilter
239+
*
240+
* @description
241+
* Sets and/or returns the custom filter function that is used to "filter" animations, i.e.
242+
* determine if an animation is allowed or not. When no filter is specified (the default), no
243+
* animation will be blocked. Setting the `customFilter` value will only allow animations for
244+
* which the filter function's return value is truthy.
245+
*
246+
* This allows to easily create arbitrarily complex rules for filtering animations, such as
247+
* allowing specific events only, or enabling animations on specific subtrees of the DOM, etc.
248+
* Filtering animations can also boost performance for low-powered devices, as well as
249+
* applications containing a lot of structural operations.
250+
*
251+
* <div class="alert alert-success">
252+
* **Best Practice:**
253+
* Keep the filtering function as lean as possible, because it will be called for each DOM
254+
* action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
255+
* See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
256+
* directives that support animations.
257+
* Performing computationally expensive or time-consuming operations on each call of the
258+
* filtering function can make your animations sluggish.
259+
* </div>
260+
*
261+
* **Note:** If present, `customFilter` will be checked before
262+
* {@link $animateProvider#classNameFilter classNameFilter}.
263+
*
264+
* @param {Function=} filterFn - The filter function which will be used to filter all animations.
265+
* If a falsy value is returned, no animation will be performed. The function will be called
266+
* with the following arguments:
267+
* - **node** `{DOMElement}` - The DOM element to be animated.
268+
* - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass`
269+
* etc).
270+
* - **options** `{Object}` - A collection of options/styles used for the animation.
271+
* @return {Function} The current filter function or `null` if there is none set.
272+
*/
273+
this.customFilter = function(filterFn) {
274+
if (arguments.length === 1) {
275+
customFilter = isFunction(filterFn) ? filterFn : null;
276+
}
277+
278+
return customFilter;
279+
};
280+
235281
/**
236282
* @ngdoc method
237283
* @name $animateProvider#classNameFilter
@@ -243,6 +289,11 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
243289
* When setting the `classNameFilter` value, animations will only be performed on elements
244290
* that successfully match the filter expression. This in turn can boost performance
245291
* for low-powered devices as well as applications containing a lot of structural operations.
292+
*
293+
* **Note:** If present, `classNameFilter` will be checked after
294+
* {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns
295+
* false, `classNameFilter` will not be checked.
296+
*
246297
* @param {RegExp=} expression The className expression which will be checked against all animations
247298
* @return {RegExp} The current CSS className expression value. If null then there is no expression value
248299
*/

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

@@ -349,12 +351,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
349351
// there are situations where a directive issues an animation for
350352
// a jqLite wrapper that contains only comment nodes... If this
351353
// happens then there is no way we can perform an animation
352-
if (!node) {
353-
close();
354-
return runner;
355-
}
356-
357-
if (!isAnimatableClassName(node, options)) {
354+
if (!node ||
355+
!isAnimatableByFilter(node, event, initialOptions) ||
356+
!isAnimatableClassName(node, options)) {
358357
close();
359358
return runner;
360359
}

test/ngAnimate/animateSpec.js

+129
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,135 @@ describe('animations', function() {
307307
});
308308
});
309309

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

0 commit comments

Comments
 (0)