Skip to content

Commit c9e5b59

Browse files
matskomprobst
authored andcommitted
fix(animations): ensure parent animations are triggered before children (angular#11201)
1 parent e42a057 commit c9e5b59

File tree

7 files changed

+135
-21
lines changed

7 files changed

+135
-21
lines changed

modules/@angular/compiler/src/view_compiler/compile_view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class CompileView implements NameResolver {
3737

3838
public classStatements: o.Statement[] = [];
3939
public createMethod: CompileMethod;
40+
public animationBindingsMethod: CompileMethod;
4041
public injectorGetMethod: CompileMethod;
4142
public updateContentQueriesMethod: CompileMethod;
4243
public dirtyParentQueriesMethod: CompileMethod;
@@ -74,6 +75,7 @@ export class CompileView implements NameResolver {
7475
public animations: CompiledAnimationTriggerResult[], public viewIndex: number,
7576
public declarationElement: CompileElement, public templateVariableBindings: string[][]) {
7677
this.createMethod = new CompileMethod(this);
78+
this.animationBindingsMethod = new CompileMethod(this);
7779
this.injectorGetMethod = new CompileMethod(this);
7880
this.updateContentQueriesMethod = new CompileMethod(this);
7981
this.dirtyParentQueriesMethod = new CompileMethod(this);

modules/@angular/compiler/src/view_compiler/property_binder.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function bindAndWriteToRenderer(
105105
var oldRenderValue: o.Expression = sanitizedValue(boundProp, fieldExpr);
106106
var renderValue: o.Expression = sanitizedValue(boundProp, currValExpr);
107107
var updateStmts: any[] /** TODO #9100 */ = [];
108+
var compileMethod = view.detectChangesRenderPropertiesMethod;
108109
switch (boundProp.type) {
109110
case PropertyBindingType.Property:
110111
if (view.genConfig.logBindingUpdate) {
@@ -150,6 +151,8 @@ function bindAndWriteToRenderer(
150151
targetViewExpr = compileElement.appElement.prop('componentView');
151152
}
152153

154+
compileMethod = view.animationBindingsMethod;
155+
153156
var animationFnExpr =
154157
targetViewExpr.prop('componentType').prop('animations').key(o.literal(animationName));
155158

@@ -178,19 +181,12 @@ function bindAndWriteToRenderer(
178181
animationFnExpr.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue])
179182
.toStmt());
180183

181-
if (!_animationViewCheckedFlagMap.get(view)) {
182-
_animationViewCheckedFlagMap.set(view, true);
183-
var triggerStmt = o.THIS_EXPR.callMethod('triggerQueuedAnimations', []).toStmt();
184-
view.afterViewLifecycleCallbacksMethod.addStmt(triggerStmt);
185-
view.detachMethod.addStmt(triggerStmt);
186-
}
187-
188184
break;
189185
}
190186

191187
bind(
192-
view, currValExpr, fieldExpr, boundProp.value, context, updateStmts,
193-
view.detectChangesRenderPropertiesMethod, view.bindings.length);
188+
view, currValExpr, fieldExpr, boundProp.value, context, updateStmts, compileMethod,
189+
view.bindings.length);
194190
});
195191
}
196192

modules/@angular/compiler/src/view_compiler/view_builder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,12 +574,14 @@ function generateCreateMethod(view: CompileView): o.Statement[] {
574574

575575
function generateDetectChangesMethod(view: CompileView): o.Statement[] {
576576
var stmts: any[] = [];
577-
if (view.detectChangesInInputsMethod.isEmpty() && view.updateContentQueriesMethod.isEmpty() &&
577+
if (view.animationBindingsMethod.isEmpty() && view.detectChangesInInputsMethod.isEmpty() &&
578+
view.updateContentQueriesMethod.isEmpty() &&
578579
view.afterContentLifecycleCallbacksMethod.isEmpty() &&
579580
view.detectChangesRenderPropertiesMethod.isEmpty() &&
580581
view.updateViewQueriesMethod.isEmpty() && view.afterViewLifecycleCallbacksMethod.isEmpty()) {
581582
return stmts;
582583
}
584+
ListWrapper.addAll(stmts, view.animationBindingsMethod.finish());
583585
ListWrapper.addAll(stmts, view.detectChangesInInputsMethod.finish());
584586
stmts.push(
585587
o.THIS_EXPR.callMethod('detectContentChildrenChanges', [DetectChangesVars.throwOnChange])
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {AnimationPlayer} from './animation_player';
10+
11+
var _queuedAnimations: AnimationPlayer[] = [];
12+
13+
/** @internal */
14+
export function queueAnimation(player: AnimationPlayer) {
15+
_queuedAnimations.push(player);
16+
}
17+
18+
/** @internal */
19+
export function triggerQueuedAnimations() {
20+
for (var i = 0; i < _queuedAnimations.length; i++) {
21+
var player = _queuedAnimations[i];
22+
player.play();
23+
}
24+
_queuedAnimations = [];
25+
}

modules/@angular/core/src/linker/view.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {AnimationGroupPlayer} from '../animation/animation_group_player';
1010
import {AnimationOutput} from '../animation/animation_output';
1111
import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player';
12+
import {queueAnimation} from '../animation/animation_queue';
1213
import {AnimationTransitionEvent} from '../animation/animation_transition_event';
1314
import {ViewAnimationMap} from '../animation/view_animation_map';
1415
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
@@ -84,23 +85,18 @@ export abstract class AppView<T> {
8485
queueAnimation(
8586
element: any, animationName: string, player: AnimationPlayer, totalTime: number,
8687
fromState: string, toState: string): void {
88+
queueAnimation(player);
8789
var event = new AnimationTransitionEvent(
8890
{'fromState': fromState, 'toState': toState, 'totalTime': totalTime});
8991
this.animationPlayers.set(element, animationName, player);
92+
9093
player.onDone(() => {
9194
// TODO: make this into a datastructure for done|start
9295
this.triggerAnimationOutput(element, animationName, 'done', event);
9396
this.animationPlayers.remove(element, animationName);
9497
});
95-
player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); });
96-
}
9798

98-
triggerQueuedAnimations() {
99-
this.animationPlayers.getAllPlayers().forEach(player => {
100-
if (!player.hasStarted()) {
101-
player.play();
102-
}
103-
});
99+
player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); });
104100
}
105101

106102
triggerAnimationOutput(

modules/@angular/core/src/linker/view_ref.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {triggerQueuedAnimations} from '../animation/animation_queue';
910
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
1011
import {ChangeDetectorStatus} from '../change_detection/constants';
1112
import {unimplemented} from '../facade/errors';
13+
1214
import {AppView} from './view';
1315

16+
1417
/**
1518
* @stable
1619
*/
@@ -104,7 +107,10 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
104107

105108
markForCheck(): void { this._view.markPathToRootAsCheckOnce(); }
106109
detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; }
107-
detectChanges(): void { this._view.detectChanges(false); }
110+
detectChanges(): void {
111+
this._view.detectChanges(false);
112+
triggerQueuedAnimations();
113+
}
108114
checkNoChanges(): void { this._view.detectChanges(true); }
109115
reattach(): void {
110116
this._view.cdMode = this._originalMode;

modules/@angular/core/test/animation/animation_integration_spec.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function main() {
3030
function declareTests({useJit}: {useJit: boolean}) {
3131
describe('animation tests', function() {
3232
beforeEach(() => {
33+
InnerContentTrackingAnimationPlayer.initLog = [];
34+
3335
TestBed.configureCompiler({useJit: useJit});
3436
TestBed.configureTestingModule({
3537
declarations: [DummyLoadingCmp, DummyIfCmp],
@@ -961,6 +963,85 @@ function declareTests({useJit}: {useJit: boolean}) {
961963
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
962964
expect(player.playAttempts).toEqual(1);
963965
}));
966+
967+
it('should always trigger animations on the parent first before starting the child',
968+
fakeAsync(() => {
969+
TestBed.overrideComponent(DummyIfCmp, {
970+
set: {
971+
template: `
972+
<div *ngIf="exp" [@outer]="exp">
973+
outer
974+
<div *ngIf="exp2" [@inner]="exp">
975+
inner
976+
< </div>
977+
< </div>
978+
`,
979+
animations: [
980+
trigger('outer', [transition('* => *', [animate(1000)])]),
981+
trigger('inner', [transition('* => *', [animate(1000)])]),
982+
]
983+
}
984+
});
985+
986+
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
987+
let fixture = TestBed.createComponent(DummyIfCmp);
988+
var cmp = fixture.debugElement.componentInstance;
989+
cmp.exp = true;
990+
cmp.exp2 = true;
991+
fixture.detectChanges();
992+
flushMicrotasks();
993+
994+
expect(driver.log.length).toEqual(2);
995+
var inner: any = driver.log.pop();
996+
var innerPlayer: any = <InnerContentTrackingAnimationPlayer>inner['player'];
997+
var outer: any = driver.log.pop();
998+
var outerPlayer: any = <InnerContentTrackingAnimationPlayer>outer['player'];
999+
1000+
expect(InnerContentTrackingAnimationPlayer.initLog).toEqual([
1001+
outerPlayer.element, innerPlayer.element
1002+
]);
1003+
}));
1004+
1005+
it('should trigger animations that exist in nested views even if a parent embedded view does not contain an animation',
1006+
fakeAsync(() => {
1007+
TestBed.overrideComponent(DummyIfCmp, {
1008+
set: {
1009+
template: `
1010+
<div *ngIf="exp" [@outer]="exp">
1011+
outer
1012+
<div *ngIf="exp">
1013+
middle
1014+
<div *ngIf="exp2" [@inner]="exp">
1015+
inner
1016+
</div>
1017+
< </div>
1018+
< </div>
1019+
`,
1020+
animations: [
1021+
trigger('outer', [transition('* => *', [animate(1000)])]),
1022+
trigger('inner', [transition('* => *', [animate(1000)])]),
1023+
]
1024+
}
1025+
});
1026+
1027+
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
1028+
let fixture = TestBed.createComponent(DummyIfCmp);
1029+
var cmp = fixture.debugElement.componentInstance;
1030+
cmp.exp = true;
1031+
cmp.exp2 = true;
1032+
fixture.detectChanges();
1033+
flushMicrotasks();
1034+
1035+
expect(driver.log.length).toEqual(2);
1036+
var inner: any = driver.log.pop();
1037+
var innerPlayer: any = <InnerContentTrackingAnimationPlayer>inner['player'];
1038+
var outer: any = driver.log.pop();
1039+
var outerPlayer: any = <InnerContentTrackingAnimationPlayer>outer['player'];
1040+
1041+
expect(InnerContentTrackingAnimationPlayer.initLog).toEqual([
1042+
outerPlayer.element, innerPlayer.element
1043+
]);
1044+
}));
9641045
});
9651046

9661047
describe('animation output events', () => {
@@ -1714,17 +1795,23 @@ class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
17141795
}
17151796

17161797
class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer {
1798+
static initLog: any[] = [];
1799+
17171800
constructor(public element: any) { super(); }
17181801

17191802
public computedHeight: number;
17201803
public capturedInnerText: string;
17211804
public playAttempts = 0;
17221805

1723-
init() { this.computedHeight = getDOM().getComputedStyle(this.element)['height']; }
1806+
init() {
1807+
InnerContentTrackingAnimationPlayer.initLog.push(this.element);
1808+
this.computedHeight = getDOM().getComputedStyle(this.element)['height'];
1809+
}
17241810

17251811
play() {
17261812
this.playAttempts++;
1727-
this.capturedInnerText = this.element.querySelector('.inner').innerText;
1813+
var innerElm = this.element.querySelector('.inner');
1814+
this.capturedInnerText = innerElm ? innerElm.innerText : '';
17281815
}
17291816
}
17301817

0 commit comments

Comments
 (0)