feat(animations): noop animation module and zone fixes (#14661)

This commit is contained in:
Matias Niemelä 2017-02-23 08:51:00 -08:00 committed by Igor Minar
parent ab3527c99b
commit e8d2743cfb
16 changed files with 608 additions and 104 deletions

View File

@ -8,11 +8,9 @@
import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations'; import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config'; import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {Component, HostBinding, HostListener, ViewChild} from '@angular/core'; import {Component, HostBinding, HostListener, ViewChild} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AnimationDriver, BrowserAnimationModule, ɵAnimationEngine} from '@angular/platform-browser/animations'; import {AnimationDriver, BrowserAnimationModule, ɵAnimationEngine} from '@angular/platform-browser/animations';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/platform-browser/animations/testing'; import {MockAnimationDriver, MockAnimationPlayer} from '@angular/platform-browser/animations/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {TestBed} from '../../testing'; import {TestBed} from '../../testing';
export function main() { export function main() {
@ -46,7 +44,7 @@ function declareTests({useJit}: {useJit: boolean}) {
resetLog(); resetLog();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}], providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
imports: [BrowserModule, BrowserAnimationModule] imports: [BrowserAnimationModule]
}); });
}); });

View File

@ -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('...'); }
}

View File

@ -12,5 +12,6 @@
* Entry point for all animation APIs of the animation browser package. * Entry point for all animation APIs of the animation browser package.
*/ */
export {BrowserAnimationModule} from './browser_animation_module'; export {BrowserAnimationModule} from './browser_animation_module';
export {NoopBrowserAnimationModule} from './noop_browser_animation_module';
export {AnimationDriver} from './render/animation_driver'; export {AnimationDriver} from './render/animation_driver';
export * from './private_export'; export * from './private_export';

View File

@ -5,18 +5,19 @@
* 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 {Injectable, NgModule, RendererFactoryV2} from '@angular/core'; import {Injectable, NgModule, NgZone, RendererFactoryV2} from '@angular/core';
import {BrowserModule, ɵDomRendererFactoryV2} from '@angular/platform-browser'; import {BrowserModule, ɵDomRendererFactoryV2} from '@angular/platform-browser';
import {AnimationEngine} from './animation_engine';
import {AnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer'; import {AnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer'; import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
import {AnimationDriver, NoOpAnimationDriver} from './render/animation_driver'; import {AnimationDriver, NoOpAnimationDriver} from './render/animation_driver';
import {AnimationEngine} from './render/animation_engine';
import {AnimationRendererFactory} from './render/animation_renderer'; import {AnimationRendererFactory} from './render/animation_renderer';
import {DomAnimationEngine} from './render/dom_animation_engine';
import {WebAnimationsDriver, supportsWebAnimations} from './render/web_animations/web_animations_driver'; import {WebAnimationsDriver, supportsWebAnimations} from './render/web_animations/web_animations_driver';
@Injectable() @Injectable()
export class InjectableAnimationEngine extends AnimationEngine { export class InjectableAnimationEngine extends DomAnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer); super(driver, normalizer);
} }
@ -34,8 +35,8 @@ export function instantiateDefaultStyleNormalizer() {
} }
export function instantiateRendererFactory( export function instantiateRendererFactory(
renderer: ɵDomRendererFactoryV2, engine: AnimationEngine) { renderer: ɵDomRendererFactoryV2, engine: AnimationEngine, zone: NgZone) {
return new AnimationRendererFactory(renderer, engine); return new AnimationRendererFactory(renderer, engine, zone);
} }
/** /**
@ -49,7 +50,7 @@ export function instantiateRendererFactory(
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, { {provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
provide: RendererFactoryV2, provide: RendererFactoryV2,
useFactory: instantiateRendererFactory, useFactory: instantiateRendererFactory,
deps: [ɵDomRendererFactoryV2, AnimationEngine] deps: [ɵDomRendererFactoryV2, AnimationEngine, NgZone]
} }
] ]
}) })

View File

@ -8,7 +8,7 @@
import {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations'; import {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver'; import {AnimationDriver} from '../render/animation_driver';
import {AnimationEngine} from '../render/animation_engine'; import {DomAnimationEngine} from '../render/dom_animation_engine';
import {normalizeStyles} from '../util'; import {normalizeStyles} from '../util';
import {AnimationTimelineInstruction} from './animation_timeline_instruction'; import {AnimationTimelineInstruction} from './animation_timeline_instruction';
@ -49,7 +49,7 @@ export class Animation {
// within core then the code below will interact with Renderer.transition(...)) // within core then the code below will interact with Renderer.transition(...))
const driver: AnimationDriver = injector.get(AnimationDriver); const driver: AnimationDriver = injector.get(AnimationDriver);
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer); const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
const engine = new AnimationEngine(driver, normalizer); const engine = new DomAnimationEngine(driver, normalizer);
return engine.animateTimeline(element, instructions); return engine.animateTimeline(element, instructions);
} }
} }

View File

@ -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 {
}

View File

@ -5,7 +5,9 @@
* 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
*/ */
export {AnimationEngine as ɵAnimationEngine} from './animation_engine';
export {Animation as ɵAnimation} from './dsl/animation'; export {Animation as ɵAnimation} from './dsl/animation';
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer'; export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoOpAnimationStyleNormalizer as ɵNoOpAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine'; export {NoOpAnimationDriver as ɵNoOpAnimationDriver} from './render/animation_driver';
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer'; export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer';
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine';

View File

@ -6,13 +6,15 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationTriggerMetadata} from '@angular/animations'; 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() @Injectable()
export class AnimationRendererFactory implements RendererFactoryV2 { 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 { createRenderer(hostElement: any, type: RendererTypeV2): RendererV2 {
let delegate = this.delegate.createRenderer(hostElement, type); let delegate = this.delegate.createRenderer(hostElement, type);
@ -24,16 +26,17 @@ export class AnimationRendererFactory implements RendererFactoryV2 {
} }
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[]; const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
animationRenderer = (type.data as any)['__animationRenderer__'] = animationRenderer = (type.data as any)['__animationRenderer__'] =
new AnimationRenderer(delegate, this._engine, animationTriggers); new AnimationRenderer(delegate, this._engine, this._zone, animationTriggers);
return animationRenderer; return animationRenderer;
} }
} }
export class AnimationRenderer implements RendererV2 { export class AnimationRenderer implements RendererV2 {
public destroyNode: (node: any) => (void|any) = null; public destroyNode: (node: any) => (void|any) = null;
private _flushPromise: Promise<any> = null;
constructor( constructor(
public delegate: RendererV2, private _engine: AnimationEngine, public delegate: RendererV2, private _engine: AnimationEngine, private _zone: NgZone,
_triggers: AnimationTriggerMetadata[] = null) { _triggers: AnimationTriggerMetadata[] = null) {
this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode(n) : null; this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode(n) : null;
if (_triggers) { if (_triggers) {
@ -92,11 +95,13 @@ export class AnimationRenderer implements RendererV2 {
removeChild(parent: any, oldChild: any): void { removeChild(parent: any, oldChild: any): void {
this._engine.onRemove(oldChild, () => this.delegate.removeChild(parent, oldChild)); this._engine.onRemove(oldChild, () => this.delegate.removeChild(parent, oldChild));
this._queueFlush();
} }
setProperty(el: any, name: string, value: any): void { setProperty(el: any, name: string, value: any): void {
if (name.charAt(0) == '@') { if (name.charAt(0) == '@') {
this._engine.setProperty(el, name.substr(1), value); this._engine.setProperty(el, name.substr(1), value);
this._queueFlush();
} else { } else {
this.delegate.setProperty(el, name, value); this.delegate.setProperty(el, name, value);
} }
@ -107,10 +112,22 @@ export class AnimationRenderer implements RendererV2 {
if (eventName.charAt(0) == '@') { if (eventName.charAt(0) == '@') {
const element = resolveElementFromTarget(target); const element = resolveElementFromTarget(target);
const [name, phase] = parseTriggerCallbackName(eventName.substr(1)); 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); 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 { function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any {

View File

@ -11,6 +11,7 @@ import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instructio
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction'; import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger'; import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {eraseStyles, setStyles} from '../util';
import {AnimationDriver} from './animation_driver'; import {AnimationDriver} from './animation_driver';
@ -20,7 +21,6 @@ export interface QueuedAnimationTransitionTuple {
triggerName: string; triggerName: string;
event: AnimationEvent; event: AnimationEvent;
} }
;
export interface TriggerListenerTuple { export interface TriggerListenerTuple {
triggerName: string; triggerName: string;
@ -31,7 +31,7 @@ export interface TriggerListenerTuple {
const MARKED_FOR_ANIMATION = 'ng-animate'; const MARKED_FOR_ANIMATION = 'ng-animate';
const MARKED_FOR_REMOVAL = '$$ngRemove'; const MARKED_FOR_REMOVAL = '$$ngRemove';
export class AnimationEngine { export class DomAnimationEngine {
private _flaggedInserts = new Set<any>(); private _flaggedInserts = new Set<any>();
private _queuedRemovals = new Map<any, () => any>(); private _queuedRemovals = new Map<any, () => any>();
private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = []; private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = [];
@ -43,11 +43,6 @@ export class AnimationEngine {
private _triggers: {[triggerName: string]: AnimationTrigger} = {}; private _triggers: {[triggerName: string]: AnimationTrigger} = {};
private _triggerListeners = new Map<any, TriggerListenerTuple[]>(); 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) {} constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
get queuedPlayers(): AnimationPlayer[] { get queuedPlayers(): AnimationPlayer[] {
@ -60,7 +55,7 @@ export class AnimationEngine {
return players; return players;
} }
registerTrigger(trigger: AnimationTriggerMetadata) { registerTrigger(trigger: AnimationTriggerMetadata): void {
const name = trigger.name; const name = trigger.name;
if (this._triggers[name]) { if (this._triggers[name]) {
throw new Error(`The provided animation trigger "${name}" has already been registered!`); 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); element.classList.add(MARKED_FOR_ANIMATION);
player.onDone(() => { element.classList.remove(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() { private _flushQueuedAnimations() {
@ -323,7 +308,6 @@ export class AnimationEngine {
} }
flush() { flush() {
this._flushId++;
this._flushQueuedAnimations(); this._flushQueuedAnimations();
let flushAgain = false; 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 { function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
switch (players.length) { switch (players.length) {
case 0: case 0:

View File

@ -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;
}

View File

@ -69,3 +69,19 @@ export function copyStyles(
} }
return destination; 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] = '';
});
}
}

View File

@ -12,7 +12,7 @@ import {el} from '@angular/platform-browser/testing/browser_util';
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor'; import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
import {buildTrigger} from '../../src/dsl/animation_trigger'; import {buildTrigger} from '../../src/dsl/animation_trigger';
import {AnimationStyleNormalizer, NoOpAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer'; 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'; import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/mock_animation_driver';
function makeTrigger(name: string, steps: any) { 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 // these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return; if (typeof Element == 'undefined') return;
describe('AnimationEngine', () => { describe('DomAnimationEngine', () => {
let element: any; let element: any;
beforeEach(() => { beforeEach(() => {
@ -36,7 +36,7 @@ export function main() {
}); });
function makeEngine(normalizer: AnimationStyleNormalizer = null) { function makeEngine(normalizer: AnimationStyleNormalizer = null) {
return new AnimationEngine(driver, normalizer || new NoOpAnimationStyleNormalizer()); return new DomAnimationEngine(driver, normalizer || new NoOpAnimationStyleNormalizer());
} }
describe('trigger registration', () => { 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', () => { describe('instructions', () => {
it('should animate a transition instruction', () => { it('should animate a transition instruction', () => {
const engine = makeEngine(); const engine = makeEngine();

View File

@ -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');
});
});
});
}

View File

@ -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();
});
});
});
}

View File

@ -5,12 +5,13 @@
* 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 {AnimationTriggerMetadata, trigger} from '@angular/animations'; import {AnimationTriggerMetadata, animate, state, style, transition, trigger} from '@angular/animations';
import {Injectable, RendererFactoryV2, RendererTypeV2} from '@angular/core'; import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {Component, Injectable, RendererFactoryV2, RendererTypeV2} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {BrowserAnimationModule, ɵAnimationEngine, ɵAnimationRendererFactory} from '@angular/platform-browser/animations'; 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'; import {el} from '../../testing/browser_util';
export function main() { export function main() {
@ -21,7 +22,7 @@ export function main() {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}], providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}],
imports: [BrowserModule, BrowserAnimationModule] imports: [BrowserAnimationModule]
}); });
}); });
@ -95,7 +96,7 @@ export function main() {
expect(engine.captures['listen']).toBeFalsy(); expect(engine.captures['listen']).toBeFalsy();
renderer.listen(element, '@event.phase', cb); 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', 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); 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): listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
() => void { () => 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 () => {}; return () => {};
} }
flush() {}
} }

View File

@ -9,3 +9,7 @@ export declare abstract class AnimationDriver {
/** @experimental */ /** @experimental */
export declare class BrowserAnimationModule { export declare class BrowserAnimationModule {
} }
/** @experimental */
export declare class NoopBrowserAnimationModule {
}