mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(core): fixes a regression with animate.leave and reordering
This fixes a regression bug that resulted in reordered elements not getting properly removed from the DOM. Reused nodes were not being cleared out in this situation. fixes: #67728
This commit is contained in:
parent
41b1410cb8
commit
dfa149dc68
5 changed files with 99 additions and 1 deletions
|
|
@ -114,4 +114,23 @@ describe('Animations Integration', () => {
|
|||
'Nested child component should have been removed immediately during routing',
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove elements from DOM after reordering and removal with (animate.leave)', async () => {
|
||||
// Wait for the test elements to be rendered
|
||||
await page.waitForSelector('.test-item');
|
||||
|
||||
let items = await page.$$('.test-item');
|
||||
expect(items.length).toBe(2);
|
||||
|
||||
// Shuffle
|
||||
await page.click('#shuffle-test');
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
|
||||
// Remove
|
||||
await page.click('#remove-test');
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
|
||||
items = await page.$$('.test-item');
|
||||
expect(items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,3 +17,11 @@
|
|||
}
|
||||
|
||||
<button id="hide-and-intercept" (click)="hideAndIntercept()">Hide and Intercept</button>
|
||||
|
||||
<div class="test-reorder-container">
|
||||
@for (item of testItems; track item) {
|
||||
<div class="test-item" (animate.leave)="onTestLeave($event)">Item {{ item }}</div>
|
||||
}
|
||||
</div>
|
||||
<button id="shuffle-test" (click)="shuffleTest()">Shuffle Test</button>
|
||||
<button id="remove-test" (click)="removeTest()">Remove Test</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Component, AnimationCallbackEvent} from '@angular/core';
|
||||
import {
|
||||
CdkDragDrop,
|
||||
CdkDropList,
|
||||
|
|
@ -20,6 +20,8 @@ export class HomeComponent {
|
|||
'Episode III - Revenge of the Sith',
|
||||
];
|
||||
|
||||
testItems = ['A', 'B'];
|
||||
|
||||
showFallback = true;
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
|
|
@ -39,4 +41,16 @@ export class HomeComponent {
|
|||
}
|
||||
this.showFallback = false;
|
||||
}
|
||||
|
||||
shuffleTest() {
|
||||
this.testItems = ['B', 'A'];
|
||||
}
|
||||
|
||||
removeTest() {
|
||||
this.testItems = ['A'];
|
||||
}
|
||||
|
||||
onTestLeave(event: AnimationCallbackEvent) {
|
||||
event.animationComplete();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ function applyToElementOrContainer(
|
|||
if (parentLView?.[ANIMATIONS]?.leave?.has(tNode.index)) {
|
||||
trackLeavingNodes(tNode, rNode as HTMLElement);
|
||||
}
|
||||
reusedNodes.delete(rNode as HTMLElement);
|
||||
runLeaveAnimationsWithCallback(
|
||||
parentLView,
|
||||
tNode,
|
||||
|
|
@ -166,6 +167,7 @@ function applyToElementOrContainer(
|
|||
},
|
||||
);
|
||||
} else if (action === WalkTNodeTreeAction.Destroy) {
|
||||
reusedNodes.delete(rNode as HTMLElement);
|
||||
runLeaveAnimationsWithCallback(parentLView, tNode, injector, () => {
|
||||
renderer.destroyNode!(rNode);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {tickAnimationFrames} from '../animation_utils/tick_animation_frames';
|
|||
import {BrowserTestingModule, platformBrowserTesting} from '@angular/platform-browser/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ComponentRef} from '@angular/core/src/render3';
|
||||
import {reusedNodes} from '../../src/animation/utils';
|
||||
|
||||
@NgModule({
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
|
|
@ -511,6 +512,60 @@ describe('Animation', () => {
|
|||
expect(fadeCalled).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should remove element from DOM with (animate.leave) after list reordering', async () => {
|
||||
@Component({
|
||||
selector: 'leak-cmp',
|
||||
template: 'item',
|
||||
})
|
||||
class LeakComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
imports: [LeakComponent],
|
||||
template: `
|
||||
@for (item of items(); track item.id) {
|
||||
<leak-cmp (animate.leave)="onLeave($event)" />
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TestComponent {
|
||||
items = signal([{id: '1'}, {id: '2'}]);
|
||||
|
||||
onLeave(event: AnimationCallbackEvent) {
|
||||
event.animationComplete();
|
||||
}
|
||||
|
||||
shuffle() {
|
||||
const arr = this.items();
|
||||
this.items.set([arr[1], arr[0]]);
|
||||
}
|
||||
|
||||
remove() {
|
||||
const arr = this.items();
|
||||
this.items.set([arr[1]]);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({animationsEnabled: true});
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('item');
|
||||
|
||||
fixture.componentInstance.shuffle();
|
||||
await fixture.whenStable();
|
||||
|
||||
const elementsBeforeRemove = fixture.debugElement.queryAll(By.css('leak-cmp'));
|
||||
// Element is in reused nodes before remove
|
||||
expect(reusedNodes.has(elementsBeforeRemove[0].nativeElement)).toBe(true);
|
||||
|
||||
fixture.componentInstance.remove();
|
||||
await fixture.whenStable();
|
||||
|
||||
const elements = fixture.nativeElement.querySelectorAll('leak-cmp');
|
||||
expect(elements.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should compose class list when host binding and regular binding', fakeAsync(() => {
|
||||
const multiple = `
|
||||
.slide-out {
|
||||
|
|
|
|||
Loading…
Reference in a new issue