Skip to content

Commit abc217b

Browse files
dario-piotrowiczatscott
authored andcommitted
fix(animations): retain triggers values for moved tracked list items (#44578)
when reordering a list with animations the list items lose their current triggers values, that is because the reordering of an item is implemented as follows (_note:_ the following implementation has been added in PR #23534) - the item is removed and marked _setForRemoval_ - the item is re-inserted, and the _setForRemoval_ is changed to _setForMove_ - the player set for animating the removal is destroyed when _setForMove_ is detected the above steps allow the element not to be animated and to keep its styling but the triggers values get lost since the removal transition/player has already been initialized, so this change adds a _previousTriggersValues_ map in the _REMOVAL_FLAG_ field in order to restore the triggers values if/when the removal turns out to be a move, changing the above steps to: - the item is removed and marked setForRemoval __and its current triggers values are saved as well__ - the item is re-inserted, and the setForRemoval is changed to setForMove - the player set for animating the removal is destroyed when setForMove is detected __and the trigger values are re-applied in the engine's statesByElement map__ resolves #29526 PR Close #44578
1 parent 919f8ae commit abc217b

File tree

2 files changed

+92
-5
lines changed

2 files changed

+92
-5
lines changed

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface ElementAnimationState {
6565
hasAnimation: boolean;
6666
namespaceId: string;
6767
removedBeforeQueried: boolean;
68+
previousTriggersValues?: Map<string, string>;
6869
}
6970

7071
export class StateValue {
@@ -344,9 +345,11 @@ export class AnimationTransitionNamespace {
344345
element: any, context: any, destroyAfterComplete?: boolean,
345346
defaultToFallback?: boolean): boolean {
346347
const triggerStates = this._engine.statesByElement.get(element);
348+
const previousTriggersValues = new Map<string, string>();
347349
if (triggerStates) {
348350
const players: TransitionAnimationPlayer[] = [];
349351
Object.keys(triggerStates).forEach(triggerName => {
352+
previousTriggersValues.set(triggerName, triggerStates[triggerName].value);
350353
// this check is here in the event that an element is removed
351354
// twice (both on the host level and the component level)
352355
if (this._triggers[triggerName]) {
@@ -358,7 +361,7 @@ export class AnimationTransitionNamespace {
358361
});
359362

360363
if (players.length) {
361-
this._engine.markElementAsRemoved(this.id, element, true, context);
364+
this._engine.markElementAsRemoved(this.id, element, true, context, previousTriggersValues);
362365
if (destroyAfterComplete) {
363366
optimizeGroupPlayer(players).onDone(() => this._engine.processLeaveNode(element));
364367
}
@@ -755,10 +758,17 @@ export class TransitionAnimationEngine {
755758
}
756759
}
757760

758-
markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) {
761+
markElementAsRemoved(
762+
namespaceId: string, element: any, hasAnimation?: boolean, context?: any,
763+
previousTriggersValues?: Map<string, string>) {
759764
this.collectedLeaveElements.push(element);
760-
element[REMOVAL_FLAG] =
761-
{namespaceId, setForRemoval: context, hasAnimation, removedBeforeQueried: false};
765+
element[REMOVAL_FLAG] = {
766+
namespaceId,
767+
setForRemoval: context,
768+
hasAnimation,
769+
removedBeforeQueried: false,
770+
previousTriggersValues
771+
};
762772
}
763773

764774
listen(
@@ -991,8 +1001,21 @@ export class TransitionAnimationEngine {
9911001

9921002
if (this.collectedEnterElements.length) {
9931003
const details = element[REMOVAL_FLAG] as ElementAnimationState;
994-
// move animations are currently not supported...
1004+
// animations for move operations (elements being removed and reinserted,
1005+
// e.g. when the order of an *ngFor list changes) are currently not supported
9951006
if (details && details.setForMove) {
1007+
if (details.previousTriggersValues &&
1008+
details.previousTriggersValues.has(entry.triggerName)) {
1009+
const previousValue = details.previousTriggersValues.get(entry.triggerName) as string;
1010+
1011+
// we need to restore the previous trigger value since the element has
1012+
// only been moved and hasn't actually left the DOM
1013+
const triggersWithStates = this.statesByElement.get(entry.element);
1014+
if (triggersWithStates && triggersWithStates[entry.triggerName]) {
1015+
triggersWithStates[entry.triggerName].value = previousValue;
1016+
}
1017+
}
1018+
9961019
player.destroy();
9971020
return;
9981021
}

packages/core/test/animation/animation_integration_spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,70 @@ describe('animation tests', function() {
16441644
}
16451645
});
16461646

1647+
it('should keep/restore the trigger value when there are move operations (with *ngFor + trackBy)',
1648+
fakeAsync(() => {
1649+
@Component({
1650+
selector: 'ani-cmp',
1651+
template: `
1652+
<div *ngFor="let item of items, trackBy: trackItem"
1653+
@myAnimation (@myAnimation.start)="cb($event)">
1654+
item{{ item }}
1655+
</div>
1656+
`,
1657+
animations: [trigger('myAnimation', [])]
1658+
})
1659+
class Cmp {
1660+
public items: number[] = [];
1661+
1662+
log: string[] = [];
1663+
cb(event: AnimationEvent) {
1664+
this.log.push(
1665+
`[${event.element.innerText.trim()}] ${event.fromState} => ${event.toState}`);
1666+
}
1667+
1668+
trackItem(_index: number, item: number) {
1669+
return item.toString();
1670+
}
1671+
addItem() {
1672+
this.items.push(this.items.length);
1673+
}
1674+
removeItem() {
1675+
this.items.pop();
1676+
}
1677+
reverseItems() {
1678+
this.items = this.items.reverse();
1679+
}
1680+
}
1681+
1682+
TestBed.configureTestingModule({declarations: [Cmp]});
1683+
1684+
const engine = TestBed.inject(ɵAnimationEngine);
1685+
const fixture = TestBed.createComponent(Cmp);
1686+
const cmp = fixture.componentInstance;
1687+
fixture.detectChanges();
1688+
1689+
const completeAnimations = () => {
1690+
fixture.detectChanges();
1691+
flushMicrotasks();
1692+
engine.players.forEach(player => player.finish());
1693+
};
1694+
1695+
cmp.log = [];
1696+
[0, 1, 2].forEach(() => cmp.addItem());
1697+
completeAnimations();
1698+
expect(cmp.log).toEqual(
1699+
['[item0] void => null', '[item1] void => null', '[item2] void => null']);
1700+
1701+
cmp.reverseItems();
1702+
completeAnimations();
1703+
1704+
cmp.log = [];
1705+
[0, 1, 2].forEach(() => cmp.removeItem());
1706+
completeAnimations();
1707+
expect(cmp.log).toEqual(
1708+
['[item2] null => void', '[item1] null => void', '[item0] null => void']);
1709+
}));
1710+
16471711
it('should animate removals of nodes to the `void` state for each animation trigger, but treat all auto styles as pre styles',
16481712
() => {
16491713
@Component({

0 commit comments

Comments
 (0)