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
This commit is contained in:
@@ -282,14 +282,46 @@ myModule.run(function($animate) {
|
||||
|
||||
## How to (selectively) enable, disable and skip animations
|
||||
|
||||
There are three different ways to disable animations, both globally and for specific animations.
|
||||
There are several different ways to disable animations, both globally and for specific animations.
|
||||
Disabling specific animations can help to speed up the render performance, for example for large
|
||||
`ngRepeat` lists that don't actually have animations. Because `ngAnimate` checks at runtime if
|
||||
animations are present, performance will take a hit even if an element has no animation.
|
||||
|
||||
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
|
||||
### During the config: {@link $animateProvider#customFilter $animateProvider.customFilter()}
|
||||
|
||||
This function can be called during the {@link angular.Module#config config} phase of an app. It
|
||||
takes a filter function as the only argument, which will then be used to "filter" animations (based
|
||||
on the animated element, the event type, and the animation options). Only when the filter function
|
||||
returns `true`, will the animation be performed. This allows great flexibility - you can easily
|
||||
create complex rules, such as allowing specific events only or enabling animations on specific
|
||||
subtrees of the DOM, and dynamically modify them, for example disabling animations at certain points
|
||||
in time or under certain circumstances.
|
||||
|
||||
```js
|
||||
app.config(function($animateProvider) {
|
||||
$animateProvider.customFilter(function(node, event, options) {
|
||||
// Example: Only animate `enter` and `leave` operations.
|
||||
return event === 'enter' || event === 'leave';
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The `customFilter` approach generally gives a big speed boost compared to other strategies, because
|
||||
the matching is done before other animation disabling strategies are checked.
|
||||
|
||||
<div class="alert alert-success">
|
||||
**Best Practice:**
|
||||
Keep the filtering function as lean as possible, because it will be called for each DOM
|
||||
action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
|
||||
See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
|
||||
directives that support animations.
|
||||
Performing computationally expensive or time-consuming operations on each call of the
|
||||
filtering function can make your animations sluggish.
|
||||
</div>
|
||||
|
||||
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
|
||||
|
||||
This function too can be called during the {@link angular.Module#config config} phase of an app. It
|
||||
takes a regex as the only argument, which will then be matched against the classes of any element
|
||||
that is about to be animated. The regex allows a lot of flexibility - you can either allow
|
||||
animations for specific classes only (useful when you are working with 3rd party animations), or
|
||||
@@ -309,10 +341,11 @@ app.config(function($animateProvider) {
|
||||
}
|
||||
```
|
||||
|
||||
The `classNameFilter` approach generally gives the biggest speed boost, because the matching is done
|
||||
before any other animation disabling strategies are checked. However, that also means it is not
|
||||
possible to override class name matching with the two following strategies. It's of course still
|
||||
possible to enable / disable animations by changing an element's class name at runtime.
|
||||
The `classNameFilter` approach generally gives a big speed boost compared to other strategies,
|
||||
because the matching is done before other animation disabling strategies are checked. However, that
|
||||
also means it is not possible to override class name matching with the two following strategies.
|
||||
It's of course still possible to enable / disable animations by changing an element's class name at
|
||||
runtime.
|
||||
|
||||
### At runtime: {@link ng.$animate#enabled $animate.enabled()}
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ var $$CoreAnimateQueueProvider = /** @this */ function() {
|
||||
var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
var provider = this;
|
||||
var classNameFilter = null;
|
||||
var customFilter = null;
|
||||
|
||||
this.$$registeredAnimations = Object.create(null);
|
||||
|
||||
@@ -232,6 +233,51 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
$provide.factory(key, factory);
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $animateProvider#customFilter
|
||||
*
|
||||
* @description
|
||||
* Sets and/or returns the custom filter function that is used to "filter" animations, i.e.
|
||||
* determine if an animation is allowed or not. When no filter is specified (the default), no
|
||||
* animation will be blocked. Setting the `customFilter` value will only allow animations for
|
||||
* which the filter function's return value is truthy.
|
||||
*
|
||||
* 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.
|
||||
* Filtering animations can also boost performance for low-powered devices, as well as
|
||||
* applications containing a lot of structural operations.
|
||||
*
|
||||
* <div class="alert alert-success">
|
||||
* **Best Practice:**
|
||||
* Keep the filtering function as lean as possible, because it will be called for each DOM
|
||||
* action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
|
||||
* See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
|
||||
* directives that support animations.
|
||||
* Performing computationally expensive or time-consuming operations on each call of the
|
||||
* filtering function can make your animations sluggish.
|
||||
* </div>
|
||||
*
|
||||
* **Note:** If present, `customFilter` will be checked before
|
||||
* {@link $animateProvider#classNameFilter classNameFilter}.
|
||||
*
|
||||
* @param {Function=} filterFn - The filter function which will be used to filter all animations.
|
||||
* If a falsy value is returned, no animation will be performed. The function will be called
|
||||
* with the following arguments:
|
||||
* - **node** `{DOMElement}` - The DOM element to be animated.
|
||||
* - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass`
|
||||
* etc).
|
||||
* - **options** `{Object}` - A collection of options/styles used for the animation.
|
||||
* @return {Function} The current filter function or `null` if there is none set.
|
||||
*/
|
||||
this.customFilter = function(filterFn) {
|
||||
if (arguments.length === 1) {
|
||||
customFilter = isFunction(filterFn) ? filterFn : null;
|
||||
}
|
||||
|
||||
return customFilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $animateProvider#classNameFilter
|
||||
@@ -243,6 +289,11 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* 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.
|
||||
*
|
||||
* **Note:** If present, `classNameFilter` will be checked after
|
||||
* {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns
|
||||
* false, `classNameFilter` will not be checked.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
|
||||
@@ -160,15 +160,17 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
|
||||
var callbackRegistry = Object.create(null);
|
||||
|
||||
// remember that the classNameFilter is set during the provider/config
|
||||
// stage therefore we can optimize here and setup a helper function
|
||||
// remember that the `customFilter`/`classNameFilter` are set during the
|
||||
// provider/config stage therefore we can optimize here and setup helper functions
|
||||
var customFilter = $animateProvider.customFilter();
|
||||
var classNameFilter = $animateProvider.classNameFilter();
|
||||
var isAnimatableClassName = !classNameFilter
|
||||
? function() { return true; }
|
||||
: function(node, options) {
|
||||
var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
|
||||
return classNameFilter.test(className);
|
||||
};
|
||||
var returnTrue = function() { return true; };
|
||||
|
||||
var isAnimatableByFilter = customFilter || returnTrue;
|
||||
var isAnimatableClassName = !classNameFilter ? returnTrue : function(node, options) {
|
||||
var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
|
||||
return classNameFilter.test(className);
|
||||
};
|
||||
|
||||
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
|
||||
|
||||
@@ -349,12 +351,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
// there are situations where a directive issues an animation for
|
||||
// a jqLite wrapper that contains only comment nodes... If this
|
||||
// happens then there is no way we can perform an animation
|
||||
if (!node) {
|
||||
close();
|
||||
return runner;
|
||||
}
|
||||
|
||||
if (!isAnimatableClassName(node, options)) {
|
||||
if (!node ||
|
||||
!isAnimatableByFilter(node, event, initialOptions) ||
|
||||
!isAnimatableClassName(node, options)) {
|
||||
close();
|
||||
return runner;
|
||||
}
|
||||
|
||||
@@ -307,6 +307,135 @@ describe('animations', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('customFilter()', function() {
|
||||
it('should be `null` by default', module(function($animateProvider) {
|
||||
expect($animateProvider.customFilter()).toBeNull();
|
||||
}));
|
||||
|
||||
it('should clear the `customFilter` if no function is passed',
|
||||
module(function($animateProvider) {
|
||||
$animateProvider.customFilter(angular.noop);
|
||||
expect($animateProvider.customFilter()).toEqual(jasmine.any(Function));
|
||||
|
||||
$animateProvider.customFilter(null);
|
||||
expect($animateProvider.customFilter()).toBeNull();
|
||||
|
||||
$animateProvider.customFilter(angular.noop);
|
||||
expect($animateProvider.customFilter()).toEqual(jasmine.any(Function));
|
||||
|
||||
$animateProvider.customFilter({});
|
||||
expect($animateProvider.customFilter()).toBeNull();
|
||||
})
|
||||
);
|
||||
|
||||
it('should only perform animations for which the function returns a truthy value',
|
||||
function() {
|
||||
var animationsAllowed = false;
|
||||
|
||||
module(function($animateProvider) {
|
||||
$animateProvider.customFilter(function() { return animationsAllowed; });
|
||||
});
|
||||
|
||||
inject(function($animate, $rootScope) {
|
||||
$animate.enter(element, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).toBeNull();
|
||||
|
||||
$animate.leave(element, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).toBeNull();
|
||||
|
||||
animationsAllowed = true;
|
||||
|
||||
$animate.enter(element, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).not.toBeNull();
|
||||
|
||||
capturedAnimation = null;
|
||||
|
||||
$animate.leave(element, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).not.toBeNull();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('should only perform animations for which the function returns a truthy value (SVG)',
|
||||
function() {
|
||||
var animationsAllowed = false;
|
||||
|
||||
module(function($animateProvider) {
|
||||
$animateProvider.customFilter(function() { return animationsAllowed; });
|
||||
});
|
||||
|
||||
inject(function($animate, $compile, $rootScope) {
|
||||
var svgElement = $compile('<svg class="element"></svg>')($rootScope);
|
||||
|
||||
$animate.enter(svgElement, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).toBeNull();
|
||||
|
||||
$animate.leave(svgElement, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).toBeNull();
|
||||
|
||||
animationsAllowed = true;
|
||||
|
||||
$animate.enter(svgElement, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).not.toBeNull();
|
||||
|
||||
capturedAnimation = null;
|
||||
|
||||
$animate.leave(svgElement, parent);
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).not.toBeNull();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('should pass the DOM element, event name and options to the filter function', function() {
|
||||
var filterFn = jasmine.createSpy('filterFn');
|
||||
var options = {};
|
||||
|
||||
module(function($animateProvider) {
|
||||
$animateProvider.customFilter(filterFn);
|
||||
});
|
||||
|
||||
inject(function($animate, $rootScope) {
|
||||
$animate.enter(element, parent, null, options);
|
||||
expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'enter', options);
|
||||
|
||||
filterFn.calls.reset();
|
||||
|
||||
$animate.leave(element);
|
||||
expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'leave', jasmine.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
it('should complete the DOM operation even if filtered out', function() {
|
||||
module(function($animateProvider) {
|
||||
$animateProvider.customFilter(function() { return false; });
|
||||
});
|
||||
|
||||
inject(function($animate, $rootScope) {
|
||||
expect(element.parent()[0]).toBeUndefined();
|
||||
|
||||
$animate.enter(element, parent);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(capturedAnimation).toBeNull();
|
||||
expect(element.parent()[0]).toBe(parent[0]);
|
||||
|
||||
$animate.leave(element);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(capturedAnimation).toBeNull();
|
||||
expect(element.parent()[0]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled()', function() {
|
||||
it('should work for all animations', inject(function($animate) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user