feat(animations): noop animation module and zone fixes (#14661)
This commit is contained in:
parent
ab3527c99b
commit
e8d2743cfb
|
@ -8,11 +8,9 @@
|
|||
import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
|
||||
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
|
||||
import {Component, HostBinding, HostListener, ViewChild} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {AnimationDriver, BrowserAnimationModule, ɵAnimationEngine} from '@angular/platform-browser/animations';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/platform-browser/animations/testing';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
import {TestBed} from '../../testing';
|
||||
|
||||
export function main() {
|
||||
|
@ -46,7 +44,7 @@ function declareTests({useJit}: {useJit: boolean}) {
|
|||
resetLog();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
|
||||
imports: [BrowserModule, BrowserAnimationModule]
|
||||
imports: [BrowserAnimationModule]
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export abstract class AnimationEngine {
|
||||
abstract registerTrigger(trigger: AnimationTriggerMetadata): void;
|
||||
abstract onInsert(element: any, domFn: () => any): void;
|
||||
abstract onRemove(element: any, domFn: () => any): void;
|
||||
abstract setProperty(element: any, property: string, value: any): void;
|
||||
abstract listen(
|
||||
element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any;
|
||||
abstract flush(): void;
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
get queuedPlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
}
|
|
@ -12,5 +12,6 @@
|
|||
* Entry point for all animation APIs of the animation browser package.
|
||||
*/
|
||||
export {BrowserAnimationModule} from './browser_animation_module';
|
||||
export {NoopBrowserAnimationModule} from './noop_browser_animation_module';
|
||||
export {AnimationDriver} from './render/animation_driver';
|
||||
export * from './private_export';
|
||||
|
|
|
@ -5,18 +5,19 @@
|
|||
* 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 {Injectable, NgModule, RendererFactoryV2} from '@angular/core';
|
||||
import {Injectable, NgModule, NgZone, RendererFactoryV2} from '@angular/core';
|
||||
import {BrowserModule, ɵDomRendererFactoryV2} from '@angular/platform-browser';
|
||||
|
||||
import {AnimationEngine} from './animation_engine';
|
||||
import {AnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
|
||||
import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
|
||||
import {AnimationDriver, NoOpAnimationDriver} from './render/animation_driver';
|
||||
import {AnimationEngine} from './render/animation_engine';
|
||||
import {AnimationRendererFactory} from './render/animation_renderer';
|
||||
import {DomAnimationEngine} from './render/dom_animation_engine';
|
||||
import {WebAnimationsDriver, supportsWebAnimations} from './render/web_animations/web_animations_driver';
|
||||
|
||||
@Injectable()
|
||||
export class InjectableAnimationEngine extends AnimationEngine {
|
||||
export class InjectableAnimationEngine extends DomAnimationEngine {
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
super(driver, normalizer);
|
||||
}
|
||||
|
@ -34,8 +35,8 @@ export function instantiateDefaultStyleNormalizer() {
|
|||
}
|
||||
|
||||
export function instantiateRendererFactory(
|
||||
renderer: ɵDomRendererFactoryV2, engine: AnimationEngine) {
|
||||
return new AnimationRendererFactory(renderer, engine);
|
||||
renderer: ɵDomRendererFactoryV2, engine: AnimationEngine, zone: NgZone) {
|
||||
return new AnimationRendererFactory(renderer, engine, zone);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,7 +50,7 @@ export function instantiateRendererFactory(
|
|||
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
|
||||
provide: RendererFactoryV2,
|
||||
useFactory: instantiateRendererFactory,
|
||||
deps: [ɵDomRendererFactoryV2, AnimationEngine]
|
||||
deps: [ɵDomRendererFactoryV2, AnimationEngine, NgZone]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {AnimationEngine} from '../render/animation_engine';
|
||||
import {DomAnimationEngine} from '../render/dom_animation_engine';
|
||||
import {normalizeStyles} from '../util';
|
||||
|
||||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
@ -49,7 +49,7 @@ export class Animation {
|
|||
// within core then the code below will interact with Renderer.transition(...))
|
||||
const driver: AnimationDriver = injector.get(AnimationDriver);
|
||||
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
|
||||
const engine = new AnimationEngine(driver, normalizer);
|
||||
const engine = new DomAnimationEngine(driver, normalizer);
|
||||
return engine.animateTimeline(element, instructions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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, NgZone, RendererFactoryV2} from '@angular/core';
|
||||
import {BrowserModule, ɵDomRendererFactoryV2} from '@angular/platform-browser';
|
||||
|
||||
import {AnimationEngine} from './animation_engine';
|
||||
import {AnimationRendererFactory} from './render/animation_renderer';
|
||||
import {NoopAnimationEngine} from './render/noop_animation_engine';
|
||||
|
||||
export function instantiateRendererFactory(
|
||||
renderer: ɵDomRendererFactoryV2, engine: AnimationEngine, zone: NgZone) {
|
||||
return new AnimationRendererFactory(renderer, engine, zone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [BrowserModule],
|
||||
providers: [
|
||||
{provide: AnimationEngine, useClass: NoopAnimationEngine}, {
|
||||
provide: RendererFactoryV2,
|
||||
useFactory: instantiateRendererFactory,
|
||||
deps: [ɵDomRendererFactoryV2, AnimationEngine, NgZone]
|
||||
}
|
||||
]
|
||||
})
|
||||
export class NoopBrowserAnimationModule {
|
||||
}
|
|
@ -5,7 +5,9 @@
|
|||
* 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
|
||||
*/
|
||||
export {AnimationEngine as ɵAnimationEngine} from './animation_engine';
|
||||
export {Animation as ɵAnimation} from './dsl/animation';
|
||||
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
|
||||
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine';
|
||||
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoOpAnimationStyleNormalizer as ɵNoOpAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
|
||||
export {NoOpAnimationDriver as ɵNoOpAnimationDriver} from './render/animation_driver';
|
||||
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer';
|
||||
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine';
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimationTriggerMetadata} from '@angular/animations';
|
||||
import {Injectable, RendererFactoryV2, RendererTypeV2, RendererV2} from '@angular/core';
|
||||
import {Injectable, NgZone, RendererFactoryV2, RendererTypeV2, RendererV2} from '@angular/core';
|
||||
|
||||
import {AnimationEngine} from './animation_engine';
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
|
||||
@Injectable()
|
||||
export class AnimationRendererFactory implements RendererFactoryV2 {
|
||||
constructor(private delegate: RendererFactoryV2, private _engine: AnimationEngine) {}
|
||||
constructor(
|
||||
private delegate: RendererFactoryV2, private _engine: AnimationEngine,
|
||||
private _zone: NgZone) {}
|
||||
|
||||
createRenderer(hostElement: any, type: RendererTypeV2): RendererV2 {
|
||||
let delegate = this.delegate.createRenderer(hostElement, type);
|
||||
|
@ -24,16 +26,17 @@ export class AnimationRendererFactory implements RendererFactoryV2 {
|
|||
}
|
||||
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
|
||||
animationRenderer = (type.data as any)['__animationRenderer__'] =
|
||||
new AnimationRenderer(delegate, this._engine, animationTriggers);
|
||||
new AnimationRenderer(delegate, this._engine, this._zone, animationTriggers);
|
||||
return animationRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationRenderer implements RendererV2 {
|
||||
public destroyNode: (node: any) => (void|any) = null;
|
||||
private _flushPromise: Promise<any> = null;
|
||||
|
||||
constructor(
|
||||
public delegate: RendererV2, private _engine: AnimationEngine,
|
||||
public delegate: RendererV2, private _engine: AnimationEngine, private _zone: NgZone,
|
||||
_triggers: AnimationTriggerMetadata[] = null) {
|
||||
this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode(n) : null;
|
||||
if (_triggers) {
|
||||
|
@ -92,11 +95,13 @@ export class AnimationRenderer implements RendererV2 {
|
|||
|
||||
removeChild(parent: any, oldChild: any): void {
|
||||
this._engine.onRemove(oldChild, () => this.delegate.removeChild(parent, oldChild));
|
||||
this._queueFlush();
|
||||
}
|
||||
|
||||
setProperty(el: any, name: string, value: any): void {
|
||||
if (name.charAt(0) == '@') {
|
||||
this._engine.setProperty(el, name.substr(1), value);
|
||||
this._queueFlush();
|
||||
} else {
|
||||
this.delegate.setProperty(el, name, value);
|
||||
}
|
||||
|
@ -107,10 +112,22 @@ export class AnimationRenderer implements RendererV2 {
|
|||
if (eventName.charAt(0) == '@') {
|
||||
const element = resolveElementFromTarget(target);
|
||||
const [name, phase] = parseTriggerCallbackName(eventName.substr(1));
|
||||
return this._engine.listen(element, name, phase, callback);
|
||||
return this._engine.listen(
|
||||
element, name, phase, (event: any) => this._zone.run(() => callback(event)));
|
||||
}
|
||||
return this.delegate.listen(target, eventName, callback);
|
||||
}
|
||||
|
||||
private _queueFlush() {
|
||||
if (!this._flushPromise) {
|
||||
this._zone.runOutsideAngular(() => {
|
||||
this._flushPromise = Promise.resolve(null).then(() => {
|
||||
this._flushPromise = null;
|
||||
this._engine.flush();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instructio
|
|||
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
|
||||
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {eraseStyles, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
|
||||
|
@ -20,7 +21,6 @@ export interface QueuedAnimationTransitionTuple {
|
|||
triggerName: string;
|
||||
event: AnimationEvent;
|
||||
}
|
||||
;
|
||||
|
||||
export interface TriggerListenerTuple {
|
||||
triggerName: string;
|
||||
|
@ -31,7 +31,7 @@ export interface TriggerListenerTuple {
|
|||
const MARKED_FOR_ANIMATION = 'ng-animate';
|
||||
const MARKED_FOR_REMOVAL = '$$ngRemove';
|
||||
|
||||
export class AnimationEngine {
|
||||
export class DomAnimationEngine {
|
||||
private _flaggedInserts = new Set<any>();
|
||||
private _queuedRemovals = new Map<any, () => any>();
|
||||
private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = [];
|
||||
|
@ -43,11 +43,6 @@ export class AnimationEngine {
|
|||
private _triggers: {[triggerName: string]: AnimationTrigger} = {};
|
||||
private _triggerListeners = new Map<any, TriggerListenerTuple[]>();
|
||||
|
||||
private _flushId = 0;
|
||||
private _awaitingFlush = false;
|
||||
|
||||
static raf = (fn: () => any): any => { return requestAnimationFrame(fn); };
|
||||
|
||||
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
|
||||
|
||||
get queuedPlayers(): AnimationPlayer[] {
|
||||
|
@ -60,7 +55,7 @@ export class AnimationEngine {
|
|||
return players;
|
||||
}
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata) {
|
||||
registerTrigger(trigger: AnimationTriggerMetadata): void {
|
||||
const name = trigger.name;
|
||||
if (this._triggers[name]) {
|
||||
throw new Error(`The provided animation trigger "${name}" has already been registered!`);
|
||||
|
@ -271,16 +266,6 @@ export class AnimationEngine {
|
|||
|
||||
element.classList.add(MARKED_FOR_ANIMATION);
|
||||
player.onDone(() => { element.classList.remove(MARKED_FOR_ANIMATION); });
|
||||
|
||||
if (!this._awaitingFlush) {
|
||||
const flushId = this._flushId;
|
||||
AnimationEngine.raf(() => {
|
||||
if (flushId == this._flushId) {
|
||||
this._awaitingFlush = false;
|
||||
this.flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _flushQueuedAnimations() {
|
||||
|
@ -323,7 +308,6 @@ export class AnimationEngine {
|
|||
}
|
||||
|
||||
flush() {
|
||||
this._flushId++;
|
||||
this._flushQueuedAnimations();
|
||||
|
||||
let flushAgain = false;
|
||||
|
@ -406,18 +390,6 @@ function deleteFromArrayMap(map: Map<any, any[]>, key: any, value: any) {
|
|||
}
|
||||
}
|
||||
|
||||
function setStyles(element: any, styles: ɵStyleData) {
|
||||
Object.keys(styles).forEach(prop => { element.style[prop] = styles[prop]; });
|
||||
}
|
||||
|
||||
function eraseStyles(element: any, styles: ɵStyleData) {
|
||||
Object.keys(styles).forEach(prop => {
|
||||
// IE requires '' instead of null
|
||||
// see https://github.com/angular/angular/issues/7916
|
||||
element.style[prop] = '';
|
||||
});
|
||||
}
|
||||
|
||||
function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
|
||||
switch (players.length) {
|
||||
case 0:
|
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {AnimationEvent, AnimationMetadataType, AnimationPlayer, AnimationStateMetadata, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
import {copyStyles, eraseStyles, normalizeStyles, setStyles} from '../util';
|
||||
|
||||
interface ListenerTuple {
|
||||
eventPhase: string;
|
||||
triggerName: string;
|
||||
callback: (event: any) => any;
|
||||
}
|
||||
|
||||
interface ChangeTuple {
|
||||
element: any;
|
||||
triggerName: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE_VALUE = 'void';
|
||||
const DEFAULT_STATE_STYLES = '*';
|
||||
|
||||
export class NoopAnimationEngine extends AnimationEngine {
|
||||
private _listeners = new Map<any, ListenerTuple[]>();
|
||||
private _changes: ChangeTuple[] = [];
|
||||
private _flaggedRemovals = new Set<any>();
|
||||
private _onDoneFns: (() => any)[] = [];
|
||||
private _triggerStyles: {[triggerName: string]: {[stateName: string]: ɵStyleData}} = {};
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata): void {
|
||||
const stateMap: {[stateName: string]: ɵStyleData} = {};
|
||||
trigger.definitions.forEach(def => {
|
||||
if (def.type === AnimationMetadataType.State) {
|
||||
const stateDef = def as AnimationStateMetadata;
|
||||
stateMap[stateDef.name] = normalizeStyles(stateDef.styles.styles);
|
||||
}
|
||||
});
|
||||
this._triggerStyles[trigger.name] = stateMap;
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void { domFn(); }
|
||||
|
||||
onRemove(element: any, domFn: () => any): void {
|
||||
domFn();
|
||||
this._flaggedRemovals.add(element);
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
const storageProp = makeStorageProp(property);
|
||||
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
|
||||
this._changes.push(<ChangeTuple>{element, oldValue, newValue: value, triggerName: property});
|
||||
|
||||
const triggerStateStyles = this._triggerStyles[property] || {};
|
||||
const fromStateStyles =
|
||||
triggerStateStyles[oldValue] || triggerStateStyles[DEFAULT_STATE_STYLES];
|
||||
if (fromStateStyles) {
|
||||
eraseStyles(element, fromStateStyles);
|
||||
}
|
||||
|
||||
element[storageProp] = value;
|
||||
this._onDoneFns.push(() => {
|
||||
const toStateStyles = triggerStateStyles[value] || triggerStateStyles[DEFAULT_STATE_STYLES];
|
||||
if (toStateStyles) {
|
||||
setStyles(element, toStateStyles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => any {
|
||||
let listeners = this._listeners.get(element);
|
||||
if (!listeners) {
|
||||
this._listeners.set(element, listeners = []);
|
||||
}
|
||||
|
||||
const tuple = <ListenerTuple>{triggerName: eventName, eventPhase, callback};
|
||||
listeners.push(tuple);
|
||||
|
||||
return () => {
|
||||
const index = listeners.indexOf(tuple);
|
||||
if (index >= 0) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
const onStartCallbacks: (() => any)[] = [];
|
||||
const onDoneCallbacks: (() => any)[] = [];
|
||||
|
||||
function handleListener(listener: ListenerTuple, data: ChangeTuple) {
|
||||
const phase = listener.eventPhase;
|
||||
const event = makeAnimationEvent(
|
||||
data.element, data.triggerName, data.oldValue, data.newValue, phase, 0);
|
||||
if (phase == 'start') {
|
||||
onStartCallbacks.push(() => listener.callback(event));
|
||||
} else if (phase == 'done') {
|
||||
onDoneCallbacks.push(() => listener.callback(event));
|
||||
}
|
||||
}
|
||||
|
||||
this._changes.forEach(change => {
|
||||
const element = change.element;
|
||||
const listeners = this._listeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
if (listener.triggerName == change.triggerName) {
|
||||
handleListener(listener, change);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// upon removal ALL the animation triggers need to get fired
|
||||
this._flaggedRemovals.forEach(element => {
|
||||
const listeners = this._listeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
const triggerName = listener.triggerName;
|
||||
const storageProp = makeStorageProp(triggerName);
|
||||
handleListener(listener, <ChangeTuple>{
|
||||
element: element,
|
||||
triggerName: triggerName,
|
||||
oldValue: element[storageProp] || DEFAULT_STATE_VALUE,
|
||||
newValue: DEFAULT_STATE_VALUE
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onStartCallbacks.forEach(fn => fn());
|
||||
onDoneCallbacks.forEach(fn => fn());
|
||||
this._flaggedRemovals.clear();
|
||||
this._changes = [];
|
||||
|
||||
this._onDoneFns.forEach(doneFn => doneFn());
|
||||
this._onDoneFns = [];
|
||||
}
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { return []; }
|
||||
get queuedPlayers(): AnimationPlayer[] { return []; }
|
||||
}
|
||||
|
||||
function makeAnimationEvent(
|
||||
element: any, triggerName: string, fromState: string, toState: string, phaseName: string,
|
||||
totalTime: number): AnimationEvent {
|
||||
return <AnimationEvent>{element, triggerName, fromState, toState, phaseName, totalTime};
|
||||
}
|
||||
|
||||
function makeStorageProp(property: string): string {
|
||||
return '_@_' + property;
|
||||
}
|
|
@ -69,3 +69,19 @@ export function copyStyles(
|
|||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function setStyles(element: any, styles: ɵStyleData) {
|
||||
if (element['style']) {
|
||||
Object.keys(styles).forEach(prop => element.style[prop] = styles[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
export function eraseStyles(element: any, styles: ɵStyleData) {
|
||||
if (element['style']) {
|
||||
Object.keys(styles).forEach(prop => {
|
||||
// IE requires '' instead of null
|
||||
// see https://github.com/angular/angular/issues/7916
|
||||
element.style[prop] = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {el} from '@angular/platform-browser/testing/browser_util';
|
|||
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
|
||||
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer, NoOpAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {AnimationEngine} from '../../src/render/animation_engine';
|
||||
import {DomAnimationEngine} from '../../src/render/dom_animation_engine';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/mock_animation_driver';
|
||||
|
||||
function makeTrigger(name: string, steps: any) {
|
||||
|
@ -27,7 +27,7 @@ export function main() {
|
|||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
describe('AnimationEngine', () => {
|
||||
describe('DomAnimationEngine', () => {
|
||||
let element: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -36,7 +36,7 @@ export function main() {
|
|||
});
|
||||
|
||||
function makeEngine(normalizer: AnimationStyleNormalizer = null) {
|
||||
return new AnimationEngine(driver, normalizer || new NoOpAnimationStyleNormalizer());
|
||||
return new DomAnimationEngine(driver, normalizer || new NoOpAnimationStyleNormalizer());
|
||||
}
|
||||
|
||||
describe('trigger registration', () => {
|
||||
|
@ -292,51 +292,6 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('flushing animations', () => {
|
||||
let ticks: (() => any)[];
|
||||
let _raf: () => any;
|
||||
beforeEach(() => {
|
||||
ticks = [];
|
||||
_raf = <() => any>AnimationEngine.raf;
|
||||
AnimationEngine.raf = (cb: () => any) => { ticks.push(cb); };
|
||||
});
|
||||
|
||||
afterEach(() => AnimationEngine.raf = _raf);
|
||||
|
||||
function flushTicks() {
|
||||
ticks.forEach(tick => tick());
|
||||
ticks = [];
|
||||
}
|
||||
|
||||
it('should invoke queued transition animations after a requestAnimationFrame flushes', () => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger('myTrigger', [transition('* => *', animate(1234))]));
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'on');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
expect(engine.activePlayers.length).toEqual(0);
|
||||
|
||||
flushTicks();
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
expect(engine.activePlayers.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not flush the animations twice when flushed right away before a frame changes',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger('myTrigger', [transition('* => *', animate(1234))]));
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'on');
|
||||
expect(engine.activePlayers.length).toEqual(0);
|
||||
|
||||
engine.flush();
|
||||
expect(engine.activePlayers.length).toEqual(1);
|
||||
|
||||
flushTicks();
|
||||
expect(engine.activePlayers.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('instructions', () => {
|
||||
it('should animate a transition instruction', () => {
|
||||
const engine = makeEngine();
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {state, style, trigger} from '@angular/animations';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {NoopAnimationEngine} from '../src/render/noop_animation_engine';
|
||||
|
||||
export function main() {
|
||||
describe('NoopAnimationEngine', () => {
|
||||
let captures: string[] = [];
|
||||
function capture(value: string = null) { return (v: any = null) => captures.push(value || v); }
|
||||
|
||||
beforeEach(() => { captures = []; });
|
||||
|
||||
it('should immediately issue DOM removals during remove animations and then fire the animation callbacks after flush',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
engine.onRemove(elm1, capture('1'));
|
||||
engine.onRemove(elm2, capture('2'));
|
||||
|
||||
engine.listen(elm1, 'trig', 'start', capture('1-start'));
|
||||
engine.listen(elm2, 'trig', 'start', capture('2-start'));
|
||||
engine.listen(elm1, 'trig', 'done', capture('1-done'));
|
||||
engine.listen(elm2, 'trig', 'done', capture('2-done'));
|
||||
|
||||
expect(captures).toEqual(['1', '2']);
|
||||
engine.flush();
|
||||
|
||||
expect(captures).toEqual(['1', '2', '1-start', '2-start', '1-done', '2-done']);
|
||||
});
|
||||
|
||||
it('should only fire the `start` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'start', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'cool');
|
||||
engine.setProperty(elm2, 'trig2', 'sweet');
|
||||
engine.listen(elm2, 'trig2', 'start', capture());
|
||||
engine.listen(elm3, 'trig3', 'start', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
||||
expect(captures.length).toEqual(2);
|
||||
const trig1Data = captures.shift();
|
||||
const trig2Data = captures.shift();
|
||||
expect(trig1Data).toEqual({
|
||||
element: elm1,
|
||||
triggerName: 'trig1',
|
||||
fromState: 'void',
|
||||
toState: 'cool',
|
||||
phaseName: 'start',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
expect(trig2Data).toEqual({
|
||||
element: elm2,
|
||||
triggerName: 'trig2',
|
||||
fromState: 'void',
|
||||
toState: 'sweet',
|
||||
phaseName: 'start',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
captures = [];
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only fire the `done` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'done', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'awesome');
|
||||
engine.setProperty(elm2, 'trig2', 'amazing');
|
||||
engine.listen(elm2, 'trig2', 'done', capture());
|
||||
engine.listen(elm3, 'trig3', 'done', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
||||
expect(captures.length).toEqual(2);
|
||||
const trig1Data = captures.shift();
|
||||
const trig2Data = captures.shift();
|
||||
expect(trig1Data).toEqual({
|
||||
element: elm1,
|
||||
triggerName: 'trig1',
|
||||
fromState: 'void',
|
||||
toState: 'awesome',
|
||||
phaseName: 'done',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
expect(trig2Data).toEqual({
|
||||
element: elm2,
|
||||
triggerName: 'trig2',
|
||||
fromState: 'void',
|
||||
toState: 'amazing',
|
||||
phaseName: 'done',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
captures = [];
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should deregister a listener when the return function is called', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const elm = {};
|
||||
|
||||
const fn1 = engine.listen(elm, 'trig1', 'start', capture('trig1-start'));
|
||||
const fn2 = engine.listen(elm, 'trig2', 'done', capture('trig2-done'));
|
||||
|
||||
engine.setProperty(elm, 'trig1', 'value1');
|
||||
engine.setProperty(elm, 'trig2', 'value2');
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig1-start', 'trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value3');
|
||||
engine.setProperty(elm, 'trig2', 'value4');
|
||||
|
||||
fn1();
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value5');
|
||||
engine.setProperty(elm, 'trig2', 'value6');
|
||||
|
||||
fn2();
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
describe('styling', () => {
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
it('should persist the styles on the element when the animation is complete', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.flush();
|
||||
expect(element.style.width).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should remove previously persist styles off of the element when a follow-up animation starts',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
state('b', style({height: '100px'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
engine.flush();
|
||||
expect(element.style.width).toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'b');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
expect(element.style.height).not.toEqual('100px');
|
||||
|
||||
engine.flush();
|
||||
expect(element.style.height).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should fall back to `*` styles incase the target state styles are not found', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('*', style({opacity: '0.5'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'xyz');
|
||||
engine.flush();
|
||||
expect(element.style.opacity).toEqual('0.5');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
|
||||
import {Component} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {ɵAnimationEngine} from '@angular/platform-browser/animations';
|
||||
import {NoopBrowserAnimationModule} from '../src/noop_browser_animation_module';
|
||||
import {NoopAnimationEngine} from '../src/render/noop_animation_engine';
|
||||
|
||||
export function main() {
|
||||
describe('NoopBrowserAnimationModule', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({imports: [NoopBrowserAnimationModule]});
|
||||
TestBed.configureCompiler({
|
||||
useJit: true,
|
||||
providers: [{
|
||||
provide: USE_VIEW_ENGINE,
|
||||
useValue: true,
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('the engine should be a Noop engine', () => {
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
expect(engine instanceof NoopAnimationEngine).toBeTruthy();
|
||||
});
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -5,12 +5,13 @@
|
|||
* 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 {AnimationTriggerMetadata, trigger} from '@angular/animations';
|
||||
import {Injectable, RendererFactoryV2, RendererTypeV2} from '@angular/core';
|
||||
import {AnimationTriggerMetadata, animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
|
||||
import {Component, Injectable, RendererFactoryV2, RendererTypeV2} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BrowserAnimationModule, ɵAnimationEngine, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
|
||||
|
||||
import {BrowserModule} from '../../src/browser';
|
||||
import {InjectableAnimationEngine} from '../../animations/src/browser_animation_module';
|
||||
import {el} from '../../testing/browser_util';
|
||||
|
||||
export function main() {
|
||||
|
@ -21,7 +22,7 @@ export function main() {
|
|||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}],
|
||||
imports: [BrowserModule, BrowserAnimationModule]
|
||||
imports: [BrowserAnimationModule]
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -95,7 +96,7 @@ export function main() {
|
|||
expect(engine.captures['listen']).toBeFalsy();
|
||||
|
||||
renderer.listen(element, '@event.phase', cb);
|
||||
expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase', cb]);
|
||||
expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']);
|
||||
});
|
||||
|
||||
it('should resolve the body|document|window nodes given their values as strings as input',
|
||||
|
@ -115,6 +116,50 @@ export function main() {
|
|||
expect(engine.captures['listen'].pop()[0]).toBe(window);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flushing animations', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler(
|
||||
{useJit: true, providers: [{provide: USE_VIEW_ENGINE, useValue: true}]});
|
||||
});
|
||||
|
||||
it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: '<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)"></div>',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'* => state',
|
||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
event: any;
|
||||
onStart(event: any) { this.event = event; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'state';
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
cmp.event = null;
|
||||
|
||||
engine.flush();
|
||||
expect(cmp.event).toBeFalsy();
|
||||
async();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -140,7 +185,10 @@ class MockAnimationEngine extends ɵAnimationEngine {
|
|||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => void {
|
||||
this._capture('listen', [element, eventName, eventPhase, callback]);
|
||||
// we don't capture the callback here since the renderer wraps it in a zone
|
||||
this._capture('listen', [element, eventName, eventPhase]);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
flush() {}
|
||||
}
|
||||
|
|
|
@ -9,3 +9,7 @@ export declare abstract class AnimationDriver {
|
|||
/** @experimental */
|
||||
export declare class BrowserAnimationModule {
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare class NoopBrowserAnimationModule {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue