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 class BrowserAnimationsModule {
static withConfig(config: BrowserAnimationsModuleConfig): ModuleWithProviders<BrowserAnimationsModule>;
}
export declare interface BrowserAnimationsModuleConfig {
disableAnimations?: boolean;
}
export declare class NoopAnimationsModule {

View File

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

View File

@ -46,8 +46,7 @@ describe('animation tests', function() {
{declarations: [SharedAnimationCmp], imports: [BrowserAnimationsModule]});
const fixture = TestBed.createComponent(SharedAnimationCmp);
const cmp = fixture.componentInstance;
expect(cmp.animationType).toEqual('BrowserAnimations');
expect(fixture.componentInstance.animationType).toEqual('BrowserAnimations');
});
it('should hint at NoopAnimationsModule being used', () => {
@ -56,9 +55,20 @@ describe('animation tests', function() {
{declarations: [SharedAnimationCmp], imports: [NoopAnimationsModule]});
const fixture = TestBed.createComponent(SharedAnimationCmp);
const cmp = fixture.componentInstance;
expect(cmp.animationType).toEqual('NoopAnimations');
expect(fixture.componentInstance.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>'})

View File

@ -11,7 +11,7 @@
* @description
* 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';

View File

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