fix(animations): ensure parent animations are triggered before children (#11201)

This commit is contained in:
Matias Niemelä 2016-09-01 23:24:26 +03:00 committed by Martin Probst
parent e42a057048
commit c9e5b599e4
7 changed files with 135 additions and 21 deletions

View File

@ -37,6 +37,7 @@ export class CompileView implements NameResolver {
public classStatements: o.Statement[] = []; public classStatements: o.Statement[] = [];
public createMethod: CompileMethod; public createMethod: CompileMethod;
public animationBindingsMethod: CompileMethod;
public injectorGetMethod: CompileMethod; public injectorGetMethod: CompileMethod;
public updateContentQueriesMethod: CompileMethod; public updateContentQueriesMethod: CompileMethod;
public dirtyParentQueriesMethod: CompileMethod; public dirtyParentQueriesMethod: CompileMethod;
@ -74,6 +75,7 @@ export class CompileView implements NameResolver {
public animations: CompiledAnimationTriggerResult[], public viewIndex: number, public animations: CompiledAnimationTriggerResult[], public viewIndex: number,
public declarationElement: CompileElement, public templateVariableBindings: string[][]) { public declarationElement: CompileElement, public templateVariableBindings: string[][]) {
this.createMethod = new CompileMethod(this); this.createMethod = new CompileMethod(this);
this.animationBindingsMethod = new CompileMethod(this);
this.injectorGetMethod = new CompileMethod(this); this.injectorGetMethod = new CompileMethod(this);
this.updateContentQueriesMethod = new CompileMethod(this); this.updateContentQueriesMethod = new CompileMethod(this);
this.dirtyParentQueriesMethod = new CompileMethod(this); this.dirtyParentQueriesMethod = new CompileMethod(this);

View File

@ -105,6 +105,7 @@ function bindAndWriteToRenderer(
var oldRenderValue: o.Expression = sanitizedValue(boundProp, fieldExpr); var oldRenderValue: o.Expression = sanitizedValue(boundProp, fieldExpr);
var renderValue: o.Expression = sanitizedValue(boundProp, currValExpr); var renderValue: o.Expression = sanitizedValue(boundProp, currValExpr);
var updateStmts: any[] /** TODO #9100 */ = []; var updateStmts: any[] /** TODO #9100 */ = [];
var compileMethod = view.detectChangesRenderPropertiesMethod;
switch (boundProp.type) { switch (boundProp.type) {
case PropertyBindingType.Property: case PropertyBindingType.Property:
if (view.genConfig.logBindingUpdate) { if (view.genConfig.logBindingUpdate) {
@ -150,6 +151,8 @@ function bindAndWriteToRenderer(
targetViewExpr = compileElement.appElement.prop('componentView'); targetViewExpr = compileElement.appElement.prop('componentView');
} }
compileMethod = view.animationBindingsMethod;
var animationFnExpr = var animationFnExpr =
targetViewExpr.prop('componentType').prop('animations').key(o.literal(animationName)); targetViewExpr.prop('componentType').prop('animations').key(o.literal(animationName));
@ -178,19 +181,12 @@ function bindAndWriteToRenderer(
animationFnExpr.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue]) animationFnExpr.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue])
.toStmt()); .toStmt());
if (!_animationViewCheckedFlagMap.get(view)) {
_animationViewCheckedFlagMap.set(view, true);
var triggerStmt = o.THIS_EXPR.callMethod('triggerQueuedAnimations', []).toStmt();
view.afterViewLifecycleCallbacksMethod.addStmt(triggerStmt);
view.detachMethod.addStmt(triggerStmt);
}
break; break;
} }
bind( bind(
view, currValExpr, fieldExpr, boundProp.value, context, updateStmts, view, currValExpr, fieldExpr, boundProp.value, context, updateStmts, compileMethod,
view.detectChangesRenderPropertiesMethod, view.bindings.length); view.bindings.length);
}); });
} }

View File

@ -574,12 +574,14 @@ function generateCreateMethod(view: CompileView): o.Statement[] {
function generateDetectChangesMethod(view: CompileView): o.Statement[] { function generateDetectChangesMethod(view: CompileView): o.Statement[] {
var stmts: any[] = []; var stmts: any[] = [];
if (view.detectChangesInInputsMethod.isEmpty() && view.updateContentQueriesMethod.isEmpty() && if (view.animationBindingsMethod.isEmpty() && view.detectChangesInInputsMethod.isEmpty() &&
view.updateContentQueriesMethod.isEmpty() &&
view.afterContentLifecycleCallbacksMethod.isEmpty() && view.afterContentLifecycleCallbacksMethod.isEmpty() &&
view.detectChangesRenderPropertiesMethod.isEmpty() && view.detectChangesRenderPropertiesMethod.isEmpty() &&
view.updateViewQueriesMethod.isEmpty() && view.afterViewLifecycleCallbacksMethod.isEmpty()) { view.updateViewQueriesMethod.isEmpty() && view.afterViewLifecycleCallbacksMethod.isEmpty()) {
return stmts; return stmts;
} }
ListWrapper.addAll(stmts, view.animationBindingsMethod.finish());
ListWrapper.addAll(stmts, view.detectChangesInInputsMethod.finish()); ListWrapper.addAll(stmts, view.detectChangesInInputsMethod.finish());
stmts.push( stmts.push(
o.THIS_EXPR.callMethod('detectContentChildrenChanges', [DetectChangesVars.throwOnChange]) o.THIS_EXPR.callMethod('detectContentChildrenChanges', [DetectChangesVars.throwOnChange])

View File

@ -0,0 +1,25 @@
/**
* @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} from './animation_player';
var _queuedAnimations: AnimationPlayer[] = [];
/** @internal */
export function queueAnimation(player: AnimationPlayer) {
_queuedAnimations.push(player);
}
/** @internal */
export function triggerQueuedAnimations() {
for (var i = 0; i < _queuedAnimations.length; i++) {
var player = _queuedAnimations[i];
player.play();
}
_queuedAnimations = [];
}

View File

@ -9,6 +9,7 @@
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationOutput} from '../animation/animation_output'; import {AnimationOutput} from '../animation/animation_output';
import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player'; import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player';
import {queueAnimation} from '../animation/animation_queue';
import {AnimationTransitionEvent} from '../animation/animation_transition_event'; import {AnimationTransitionEvent} from '../animation/animation_transition_event';
import {ViewAnimationMap} from '../animation/view_animation_map'; import {ViewAnimationMap} from '../animation/view_animation_map';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
@ -84,23 +85,18 @@ export abstract class AppView<T> {
queueAnimation( queueAnimation(
element: any, animationName: string, player: AnimationPlayer, totalTime: number, element: any, animationName: string, player: AnimationPlayer, totalTime: number,
fromState: string, toState: string): void { fromState: string, toState: string): void {
queueAnimation(player);
var event = new AnimationTransitionEvent( var event = new AnimationTransitionEvent(
{'fromState': fromState, 'toState': toState, 'totalTime': totalTime}); {'fromState': fromState, 'toState': toState, 'totalTime': totalTime});
this.animationPlayers.set(element, animationName, player); this.animationPlayers.set(element, animationName, player);
player.onDone(() => { player.onDone(() => {
// TODO: make this into a datastructure for done|start // TODO: make this into a datastructure for done|start
this.triggerAnimationOutput(element, animationName, 'done', event); this.triggerAnimationOutput(element, animationName, 'done', event);
this.animationPlayers.remove(element, animationName); this.animationPlayers.remove(element, animationName);
}); });
player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); });
}
triggerQueuedAnimations() { player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); });
this.animationPlayers.getAllPlayers().forEach(player => {
if (!player.hasStarted()) {
player.play();
}
});
} }
triggerAnimationOutput( triggerAnimationOutput(

View File

@ -6,11 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {triggerQueuedAnimations} from '../animation/animation_queue';
import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ChangeDetectorStatus} from '../change_detection/constants'; import {ChangeDetectorStatus} from '../change_detection/constants';
import {unimplemented} from '../facade/errors'; import {unimplemented} from '../facade/errors';
import {AppView} from './view'; import {AppView} from './view';
/** /**
* @stable * @stable
*/ */
@ -104,7 +107,10 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
markForCheck(): void { this._view.markPathToRootAsCheckOnce(); } markForCheck(): void { this._view.markPathToRootAsCheckOnce(); }
detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; } detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; }
detectChanges(): void { this._view.detectChanges(false); } detectChanges(): void {
this._view.detectChanges(false);
triggerQueuedAnimations();
}
checkNoChanges(): void { this._view.detectChanges(true); } checkNoChanges(): void { this._view.detectChanges(true); }
reattach(): void { reattach(): void {
this._view.cdMode = this._originalMode; this._view.cdMode = this._originalMode;

View File

@ -30,6 +30,8 @@ export function main() {
function declareTests({useJit}: {useJit: boolean}) { function declareTests({useJit}: {useJit: boolean}) {
describe('animation tests', function() { describe('animation tests', function() {
beforeEach(() => { beforeEach(() => {
InnerContentTrackingAnimationPlayer.initLog = [];
TestBed.configureCompiler({useJit: useJit}); TestBed.configureCompiler({useJit: useJit});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DummyLoadingCmp, DummyIfCmp], declarations: [DummyLoadingCmp, DummyIfCmp],
@ -961,6 +963,85 @@ function declareTests({useJit}: {useJit: boolean}) {
var player = <InnerContentTrackingAnimationPlayer>animation['player']; var player = <InnerContentTrackingAnimationPlayer>animation['player'];
expect(player.playAttempts).toEqual(1); expect(player.playAttempts).toEqual(1);
})); }));
it('should always trigger animations on the parent first before starting the child',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngIf="exp" [@outer]="exp">
outer
<div *ngIf="exp2" [@inner]="exp">
inner
< </div>
< </div>
`,
animations: [
trigger('outer', [transition('* => *', [animate(1000)])]),
trigger('inner', [transition('* => *', [animate(1000)])]),
]
}
});
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
cmp.exp2 = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(2);
var inner: any = driver.log.pop();
var innerPlayer: any = <InnerContentTrackingAnimationPlayer>inner['player'];
var outer: any = driver.log.pop();
var outerPlayer: any = <InnerContentTrackingAnimationPlayer>outer['player'];
expect(InnerContentTrackingAnimationPlayer.initLog).toEqual([
outerPlayer.element, innerPlayer.element
]);
}));
it('should trigger animations that exist in nested views even if a parent embedded view does not contain an animation',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngIf="exp" [@outer]="exp">
outer
<div *ngIf="exp">
middle
<div *ngIf="exp2" [@inner]="exp">
inner
</div>
< </div>
< </div>
`,
animations: [
trigger('outer', [transition('* => *', [animate(1000)])]),
trigger('inner', [transition('* => *', [animate(1000)])]),
]
}
});
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
cmp.exp2 = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(2);
var inner: any = driver.log.pop();
var innerPlayer: any = <InnerContentTrackingAnimationPlayer>inner['player'];
var outer: any = driver.log.pop();
var outerPlayer: any = <InnerContentTrackingAnimationPlayer>outer['player'];
expect(InnerContentTrackingAnimationPlayer.initLog).toEqual([
outerPlayer.element, innerPlayer.element
]);
}));
}); });
describe('animation output events', () => { describe('animation output events', () => {
@ -1714,17 +1795,23 @@ class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
} }
class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer { class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer {
static initLog: any[] = [];
constructor(public element: any) { super(); } constructor(public element: any) { super(); }
public computedHeight: number; public computedHeight: number;
public capturedInnerText: string; public capturedInnerText: string;
public playAttempts = 0; public playAttempts = 0;
init() { this.computedHeight = getDOM().getComputedStyle(this.element)['height']; } init() {
InnerContentTrackingAnimationPlayer.initLog.push(this.element);
this.computedHeight = getDOM().getComputedStyle(this.element)['height'];
}
play() { play() {
this.playAttempts++; this.playAttempts++;
this.capturedInnerText = this.element.querySelector('.inner').innerText; var innerElm = this.element.querySelector('.inner');
this.capturedInnerText = innerElm ? innerElm.innerText : '';
} }
} }