From 05aa5e0179c2c5fb9205381dd122eaeab79ea5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 24 Apr 2018 14:28:18 -0700 Subject: [PATCH] fix(animations): retain state styling for nodes that are moved around (#23686) PR Close #23686 --- .../src/render/transition_animation_engine.ts | 25 +++- .../animation/animation_integration_spec.ts | 111 ++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index ec69059ef2..192c747e87 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -28,13 +28,15 @@ const STAR_SELECTOR = '.ng-star-inserted'; const EMPTY_PLAYER_ARRAY: TransitionAnimationPlayer[] = []; const NULL_REMOVAL_STATE: ElementAnimationState = { namespaceId: '', - setForRemoval: null, + setForRemoval: false, + setForMove: false, hasAnimation: false, removedBeforeQueried: false }; const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { namespaceId: '', - setForRemoval: null, + setForMove: false, + setForRemoval: false, hasAnimation: false, removedBeforeQueried: true }; @@ -58,7 +60,8 @@ export interface QueueInstruction { export const REMOVAL_FLAG = '__ng_removed'; export interface ElementAnimationState { - setForRemoval: any; + setForRemoval: boolean; + setForMove: boolean; hasAnimation: boolean; namespaceId: string; removedBeforeQueried: boolean; @@ -660,6 +663,11 @@ export class TransitionAnimationEngine { const details = element[REMOVAL_FLAG] as ElementAnimationState; if (details && details.setForRemoval) { details.setForRemoval = false; + details.setForMove = true; + const index = this.collectedLeaveElements.indexOf(element); + if (index >= 0) { + this.collectedLeaveElements.splice(index, 1); + } } // in the event that the namespaceId is blank then the caller @@ -946,9 +954,18 @@ export class TransitionAnimationEngine { const ns = this._namespaceList[i]; ns.drainQueuedTransitions(microtaskId).forEach(entry => { const player = entry.player; + const element = entry.element; allPlayers.push(player); - const element = entry.element; + if (this.collectedEnterElements.length) { + const details = element[REMOVAL_FLAG] as ElementAnimationState; + // move animations are currently not supported... + if (details && details.setForMove) { + player.destroy(); + return; + } + } + if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { player.destroy(); return; diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index cc8b9556f1..4980722a36 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -1453,6 +1453,117 @@ const DEFAULT_COMPONENT_ID = '1'; .toBeTruthy(); }); + it('should retain state styles when the underlying DOM structure changes even if there are no insert/remove animations', + () => { + @Component({ + selector: 'ani-cmp', + template: ` +
+ {{ item }} +
+ `, + animations: [trigger('color', [state('green', style({backgroundColor: 'green'}))])] + }) + class Cmp { + public colorExp = 'green'; + public items = [0, 1, 2, 3]; + + reorder() { + const temp = this.items[0]; + this.items[0] = this.items[1]; + this.items[1] = temp; + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + + let elements: HTMLElement[] = fixture.nativeElement.querySelectorAll('.item'); + assertBackgroundColor(elements[0], 'green'); + assertBackgroundColor(elements[1], 'green'); + assertBackgroundColor(elements[2], 'green'); + assertBackgroundColor(elements[3], 'green'); + + elements[0].title = '0a'; + elements[1].title = '1a'; + + cmp.reorder(); + fixture.detectChanges(); + + elements = fixture.nativeElement.querySelectorAll('.item'); + assertBackgroundColor(elements[0], 'green'); + assertBackgroundColor(elements[1], 'green'); + assertBackgroundColor(elements[2], 'green'); + assertBackgroundColor(elements[3], 'green'); + + function assertBackgroundColor(element: HTMLElement, color: string) { + expect(element.style.getPropertyValue('background-color')).toEqual(color); + } + }); + + it('should retain state styles when the underlying DOM structure changes even if there are insert/remove animations', + () => { + @Component({ + selector: 'ani-cmp', + template: ` +
+ {{ item }} +
+ `, + animations: [trigger( + 'color', + [ + transition('* => *', animate(500)), + state('green', style({backgroundColor: 'green'})) + ])] + }) + class Cmp { + public colorExp = 'green'; + public items = [0, 1, 2, 3]; + + reorder() { + const temp = this.items[0]; + this.items[0] = this.items[1]; + this.items[1] = temp; + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + + getLog().forEach(p => p.finish()); + + let elements: HTMLElement[] = fixture.nativeElement.querySelectorAll('.item'); + assertBackgroundColor(elements[0], 'green'); + assertBackgroundColor(elements[1], 'green'); + assertBackgroundColor(elements[2], 'green'); + assertBackgroundColor(elements[3], 'green'); + + elements[0].title = '0a'; + elements[1].title = '1a'; + + cmp.reorder(); + fixture.detectChanges(); + + getLog().forEach(p => p.finish()); + + elements = fixture.nativeElement.querySelectorAll('.item'); + assertBackgroundColor(elements[0], 'green'); + assertBackgroundColor(elements[1], 'green'); + assertBackgroundColor(elements[2], 'green'); + assertBackgroundColor(elements[3], 'green'); + + function assertBackgroundColor(element: HTMLElement, color: string) { + expect(element.style.getPropertyValue('background-color')).toEqual(color); + } + }); + it('should animate removals of nodes to the `void` state for each animation trigger, but treat all auto styles as pre styles', () => { @Component({