feat(animations): add support for disabling animations through BrowserAnimationsModule.withConfig (#40731)

Currently the only way to disable animations is by providing the `NoopAnimationsModule`
which doesn't allow for it to be disabled based on runtime information. These changes
add support for disabling animations based on runtime information by using
`BrowserAnimationsModule.withConfig({disableAnimations: true})`.

PR Close #40731
This commit is contained in:
Kristiyan Kostadinov 2021-02-21 14:17:53 +01:00 committed by Zach Arend
parent 3c24136b98
commit 29d8a0ab09
6 changed files with 162 additions and 87 deletions

View File

@ -1,6 +1,11 @@
export declare const ANIMATION_MODULE_TYPE: InjectionToken<"NoopAnimations" | "BrowserAnimations">; export declare const ANIMATION_MODULE_TYPE: InjectionToken<"NoopAnimations" | "BrowserAnimations">;
export declare class BrowserAnimationsModule { export declare class BrowserAnimationsModule {
static withConfig(config: BrowserAnimationsModuleConfig): ModuleWithProviders<BrowserAnimationsModule>;
}
export declare interface BrowserAnimationsModuleConfig {
disableAnimations?: boolean;
} }
export declare class NoopAnimationsModule { export declare class NoopAnimationsModule {

View File

@ -12,7 +12,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3033, "runtime-es2015": 3033,
"main-es2015": 447894, "main-es2015": 448055,
"polyfills-es2015": 52493 "polyfills-es2015": 52493
} }
} }
@ -21,7 +21,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3153, "runtime-es2015": 3153,
"main-es2015": 432647, "main-es2015": 433285,
"polyfills-es2015": 52493 "polyfills-es2015": 52493
} }
} }

View File

@ -46,8 +46,7 @@ describe('animation tests', function() {
{declarations: [SharedAnimationCmp], imports: [BrowserAnimationsModule]}); {declarations: [SharedAnimationCmp], imports: [BrowserAnimationsModule]});
const fixture = TestBed.createComponent(SharedAnimationCmp); const fixture = TestBed.createComponent(SharedAnimationCmp);
const cmp = fixture.componentInstance; expect(fixture.componentInstance.animationType).toEqual('BrowserAnimations');
expect(cmp.animationType).toEqual('BrowserAnimations');
}); });
it('should hint at NoopAnimationsModule being used', () => { it('should hint at NoopAnimationsModule being used', () => {
@ -56,9 +55,20 @@ describe('animation tests', function() {
{declarations: [SharedAnimationCmp], imports: [NoopAnimationsModule]}); {declarations: [SharedAnimationCmp], imports: [NoopAnimationsModule]});
const fixture = TestBed.createComponent(SharedAnimationCmp); const fixture = TestBed.createComponent(SharedAnimationCmp);
const cmp = fixture.componentInstance; expect(fixture.componentInstance.animationType).toEqual('NoopAnimations');
expect(cmp.animationType).toEqual('NoopAnimations');
}); });
it('should hint at NoopAnimationsModule being used when BrowserAnimationsModule is provided with disabled animations',
() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
declarations: [SharedAnimationCmp],
imports: [BrowserAnimationsModule.withConfig({disableAnimations: true})]
});
const fixture = TestBed.createComponent(SharedAnimationCmp);
expect(fixture.componentInstance.animationType).toEqual('NoopAnimations');
});
}); });
@Component({template: '<p>template text</p>'}) @Component({template: '<p>template text</p>'})

View File

@ -11,7 +11,7 @@
* @description * @description
* Entry point for all animation APIs of the animation browser package. * Entry point for all animation APIs of the animation browser package.
*/ */
export {BrowserAnimationsModule, NoopAnimationsModule} from './module'; export {BrowserAnimationsModule, BrowserAnimationsModuleConfig, NoopAnimationsModule} from './module';
export {ANIMATION_MODULE_TYPE} from './providers'; export {ANIMATION_MODULE_TYPE} from './providers';

View File

@ -5,11 +5,23 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {NgModule} from '@angular/core'; import {ModuleWithProviders, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {BROWSER_ANIMATIONS_PROVIDERS, BROWSER_NOOP_ANIMATIONS_PROVIDERS} from './providers'; import {BROWSER_ANIMATIONS_PROVIDERS, BROWSER_NOOP_ANIMATIONS_PROVIDERS} from './providers';
/**
* Object used to configure the behavior of {@link BrowserAnimationsModule}
* @publicApi
*/
export interface BrowserAnimationsModuleConfig {
/**
* Whether animations should be disabled. Passing this is identical to providing the
* `NoopAnimationsModule`, but it can be controlled based on a runtime value.
*/
disableAnimations?: boolean;
}
/** /**
* Exports `BrowserModule` with additional [dependency-injection providers](guide/glossary#provider) * Exports `BrowserModule` with additional [dependency-injection providers](guide/glossary#provider)
* for use with animations. See [Animations](guide/animations). * for use with animations. See [Animations](guide/animations).
@ -20,6 +32,30 @@ import {BROWSER_ANIMATIONS_PROVIDERS, BROWSER_NOOP_ANIMATIONS_PROVIDERS} from '.
providers: BROWSER_ANIMATIONS_PROVIDERS, providers: BROWSER_ANIMATIONS_PROVIDERS,
}) })
export class BrowserAnimationsModule { export class BrowserAnimationsModule {
/**
* Configures the module based on the specified object.
*
* @param config Object used to configure the behavior of the `BrowserAnimationsModule`.
* @see `BrowserAnimationsModuleConfig`
*
* @usageNotes
* When registering the `BrowserAnimationsModule`, you can use the `withConfig`
* function as follows:
* ```
* @NgModule({
* imports: [BrowserAnimationsModule.withConfig(config)]
* })
* class MyNgModule {}
* ```
*/
static withConfig(config: BrowserAnimationsModuleConfig):
ModuleWithProviders<BrowserAnimationsModule> {
return {
ngModule: BrowserAnimationsModule,
providers: config.disableAnimations ? BROWSER_NOOP_ANIMATIONS_PROVIDERS :
BROWSER_ANIMATIONS_PROVIDERS
};
}
} }
/** /**

View File

@ -9,97 +9,121 @@ import {animate, style, transition, trigger} from '@angular/animations';
import {ɵAnimationEngine} from '@angular/animations/browser'; import {ɵAnimationEngine} from '@angular/animations/browser';
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
{ describe('NoopAnimationsModule', () => {
describe('NoopAnimationsModule', () => { beforeEach(() => {
beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]});
TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); });
});
it('should flush and fire callbacks when the zone becomes stable', (async) => { noopAnimationTests();
@Component({ });
selector: 'my-cmp',
template: describe('BrowserAnimationsModule with disableAnimations = true', () => {
'<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>', beforeEach(() => {
animations: [trigger( TestBed.configureTestingModule(
'myAnimation', {imports: [BrowserAnimationsModule.withConfig({disableAnimations: true})]});
[transition( });
'* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
}) noopAnimationTests();
class Cmp { });
exp: any;
startEvent: any;
doneEvent: any; function noopAnimationTests() {
onStart(event: any) { it('should flush and fire callbacks when the zone becomes stable', (async) => {
this.startEvent = event; // This test is only meant to be run inside the browser.
} if (isNode) {
onDone(event: any) { async();
this.doneEvent = event; return;
} }
@Component({
selector: 'my-cmp',
template:
'<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
animations: [trigger(
'myAnimation',
[transition(
'* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any;
startEvent: any;
doneEvent: any;
onStart(event: any) {
this.startEvent = event;
} }
onDone(event: any) {
this.doneEvent = event;
}
}
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp); const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance; const cmp = fixture.componentInstance;
cmp.exp = 'state'; cmp.exp = 'state';
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
expect(cmp.startEvent.triggerName).toEqual('myAnimation'); expect(cmp.startEvent.triggerName).toEqual('myAnimation');
expect(cmp.startEvent.phaseName).toEqual('start'); expect(cmp.startEvent.phaseName).toEqual('start');
expect(cmp.doneEvent.triggerName).toEqual('myAnimation'); expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
expect(cmp.doneEvent.phaseName).toEqual('done'); expect(cmp.doneEvent.phaseName).toEqual('done');
async(); async();
});
}); });
});
it('should handle leave animation callbacks even if the element is destroyed in the process', it('should handle leave animation callbacks even if the element is destroyed in the process',
(async) => { (async) => {
@Component({ // This test is only meant to be run inside the browser.
selector: 'my-cmp', if (isNode) {
template: async();
'<div *ngIf="exp" @myAnimation (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>', return;
animations: [trigger( }
'myAnimation',
[transition( @Component({
':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], selector: 'my-cmp',
}) template:
class Cmp { '<div *ngIf="exp" @myAnimation (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
exp: any; animations: [trigger(
startEvent: any; 'myAnimation',
doneEvent: any; [transition(
onStart(event: any) { ':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
this.startEvent = event; })
} class Cmp {
onDone(event: any) { exp: any;
this.doneEvent = event; startEvent: any;
} doneEvent: any;
onStart(event: any) {
this.startEvent = event;
} }
onDone(event: any) {
this.doneEvent = event;
}
}
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.inject(ɵAnimationEngine); const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp); const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance; const cmp = fixture.componentInstance;
cmp.exp = true; cmp.exp = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
cmp.startEvent = null;
cmp.doneEvent = null;
cmp.exp = false;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
cmp.startEvent = null; expect(cmp.startEvent.triggerName).toEqual('myAnimation');
cmp.doneEvent = null; expect(cmp.startEvent.phaseName).toEqual('start');
expect(cmp.startEvent.toState).toEqual('void');
cmp.exp = false; expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
fixture.detectChanges(); expect(cmp.doneEvent.phaseName).toEqual('done');
fixture.whenStable().then(() => { expect(cmp.doneEvent.toState).toEqual('void');
expect(cmp.startEvent.triggerName).toEqual('myAnimation'); async();
expect(cmp.startEvent.phaseName).toEqual('start');
expect(cmp.startEvent.toState).toEqual('void');
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
expect(cmp.doneEvent.phaseName).toEqual('done');
expect(cmp.doneEvent.toState).toEqual('void');
async();
});
}); });
}); });
}); });
} }