Skip to content

Commit ab6b89a

Browse files
committed
fix(ivy): ensure that super/sub classes can both update an animation binding in harmony
In ivy, because metadata is inherited, this may cause super and sub host bindings on two components to update an animation binding in parallel. This patch ensures that the animation transition code can handle this. Jira-Issue: FW-1220
1 parent 3f6bf6d commit ab6b89a

File tree

3 files changed

+126
-8
lines changed

3 files changed

+126
-8
lines changed

packages/animations/browser/src/render/transition_animation_engine.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ export class AnimationTransitionNamespace {
183183
return trigger;
184184
}
185185

186-
trigger(element: any, triggerName: string, value: any, defaultToFallback: boolean = true):
187-
TransitionAnimationPlayer|undefined {
186+
trigger(
187+
element: any, triggerName: string, value: any, defaultToFallback: boolean = true,
188+
isFinal: boolean = false): TransitionAnimationPlayer|undefined {
188189
const trigger = this._getTrigger(triggerName);
189190
const player = new TransitionAnimationPlayer(this.id, triggerName, element);
190191

@@ -203,21 +204,17 @@ export class AnimationTransitionNamespace {
203204
toState.absorbOptions(fromState.options);
204205
}
205206

206-
triggersWithStates[triggerName] = toState;
207-
208207
if (!fromState) {
209208
fromState = DEFAULT_STATE_VALUE;
210209
}
211210

212-
const isRemoval = toState.value === VOID_VALUE;
213-
214211
// normally this isn't reached by here, however, if an object expression
215212
// is passed in then it may be a new object each time. Comparing the value
216213
// is important since that will stay the same despite there being a new object.
217214
// The removal arc here is special cased because the same element is triggered
218215
// twice in the event that it contains animations on the outer/inner portions
219216
// of the host container
220-
if (!isRemoval && fromState.value === toState.value) {
217+
if (fromState.value === toState.value) {
221218
// this means that despite the value not changing, some inner params
222219
// have changed which means that the animation final styles need to be applied
223220
if (!objEquals(fromState.params, toState.params)) {
@@ -236,6 +233,10 @@ export class AnimationTransitionNamespace {
236233
return;
237234
}
238235

236+
if (!isFinal) {
237+
triggersWithStates[triggerName] = toState;
238+
}
239+
239240
const playersOnElement: TransitionAnimationPlayer[] =
240241
getOrSetAsInMap(this._engine.playersByElement, element, []);
241242
playersOnElement.forEach(player => {
@@ -336,7 +337,7 @@ export class AnimationTransitionNamespace {
336337
// this check is here in the event that an element is removed
337338
// twice (both on the host level and the component level)
338339
if (this._triggers[triggerName]) {
339-
const player = this.trigger(element, triggerName, VOID_VALUE, defaultToFallback);
340+
const player = this.trigger(element, triggerName, VOID_VALUE, defaultToFallback, true);
340341
if (player) {
341342
players.push(player);
342343
}

packages/core/test/acceptance/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ ts_library(
99
["**/*.ts"],
1010
),
1111
deps = [
12+
"//packages/animations",
1213
"//packages/common",
1314
"//packages/compiler",
1415
"//packages/compiler/testing",
1516
"//packages/core",
1617
"//packages/core/testing",
1718
"//packages/platform-browser",
1819
"//packages/platform-browser-dynamic",
20+
"//packages/platform-browser/animations",
1921
"//packages/platform-browser/testing",
2022
"//packages/private/testing",
2123
"@npm//zone.js",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
import {AnimationEvent, animate, style, transition, trigger} from '@angular/animations';
9+
import {Component, ViewChild} from '@angular/core';
10+
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
11+
import {expect} from '@angular/platform-browser/testing/src/matchers';
12+
import {onlyInIvy} from '@angular/private/testing';
13+
14+
describe('acceptance integration tests', () => {
15+
onlyInIvy('animation and host metadata is inherited only in ivy')
16+
.fit(
17+
'animation trigger events should only fire once in both super class and sub class components',
18+
fakeAsync(() => {
19+
const sharedAnimations = [
20+
trigger(
21+
'foo',
22+
[
23+
transition(
24+
':increment',
25+
[
26+
style({opacity: 0}),
27+
animate('1s', style({opacity: 1})),
28+
]),
29+
]),
30+
];
31+
32+
@Component({
33+
selector: 'super-comp',
34+
animations: sharedAnimations,
35+
template: '...',
36+
host: {
37+
'[@foo]': 'foo1Value',
38+
'(@foo.start)': 'foo1Start($event)',
39+
'(@foo.done)': 'foo1Done($event)',
40+
}
41+
})
42+
class SuperClassComp {
43+
foo1Value = 0;
44+
log1: any[] = [];
45+
46+
foo1Start(event: AnimationEvent) { this.log1.push('start-parent', event.toState); }
47+
48+
foo1Done(event: AnimationEvent) { this.log1.push('done-parent', event.toState); }
49+
50+
fire() { this.foo1Value++; }
51+
}
52+
53+
@Component({
54+
selector: 'sub-comp',
55+
animations: sharedAnimations,
56+
template: '...',
57+
host: {
58+
'[@foo]': 'foo2Value',
59+
'(@foo.start)': 'foo2Start($event)',
60+
'(@foo.done)': 'foo2Done($event)',
61+
}
62+
})
63+
class SubClassComp extends SuperClassComp {
64+
foo2Value = 0;
65+
log2: any[] = [];
66+
67+
foo2Start(event: AnimationEvent) { this.log2.push('start-child', event.toState); }
68+
69+
foo2Done(event: AnimationEvent) { this.log2.push('done-child', event.toState); }
70+
71+
fire() {
72+
super.fire();
73+
this.foo2Value++;
74+
}
75+
}
76+
77+
@Component({
78+
selector: 'app',
79+
template: `
80+
<sub-comp #sub></sub-comp>
81+
`
82+
})
83+
class AppComp {
84+
@ViewChild('sub')
85+
subComp: SubClassComp|null = null;
86+
}
87+
88+
TestBed.configureTestingModule({declarations: [SubClassComp, SuperClassComp, AppComp]});
89+
const fixture = TestBed.createComponent(AppComp);
90+
fixture.detectChanges();
91+
flushMicrotasks();
92+
93+
const comp = fixture.componentInstance.subComp !;
94+
comp.log1.length = 0;
95+
comp.log2.length = 0;
96+
97+
comp.fire();
98+
fixture.detectChanges();
99+
flushMicrotasks();
100+
101+
expect(comp.log1).toEqual([
102+
'start-parent',
103+
1,
104+
'done-parent',
105+
1,
106+
]);
107+
108+
expect(comp.log2).toEqual([
109+
'start-child',
110+
1,
111+
'done-child',
112+
1,
113+
]);
114+
}));
115+
});

0 commit comments

Comments
 (0)