Sitelet https://web.archive.org/web/20230627195720/https://github.com/angular/angular/issues/40752
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Animations] :enter / :leave animations are triggered incorrectly for reordered items #40752

Open
udos86 opened this issue Feb 8, 2021 · 6 comments
Labels
area: animations P4 A relatively minor issue that is not relevant to core functions
Milestone

Comments

@udos86
Copy link

udos86 commented Feb 8, 2021 •

Reproduction

I've created a fork of the CDK Drag&Drop custom placeholder example to demonstrate this bug:

https://stackblitz.com/edit/angular-3katn7?file=src/app/cdk-drag-drop-custom-placeholder-example.ts

Steps to reproduce:

  1. Drag a list item and drop it at some list position further down below
  2. Click the 'Remove' button on the dropped list item
  3. Watch the :leave animation being triggered correctly
  4. Now click the 'Remove' button on some item that was repositioned above the dropped list item
  5. Watch the :enter animation being triggered instead of :leave

Expected Behavior

What behavior were you expecting to see?

The :leave animation should be triggered correctly on any list item wether or not it has been repositioned due to drag & drop at some point in time.

Environment

  • Angular: 11.1.1
  • CDK/Material: 11.1.1
  • Browser(s): Chrome
  • Operating System (e.g. Windows, macOS, Ubuntu): win32 x64
@crisbeto
Copy link
Member

crisbeto commented Feb 8, 2021

This is likely an issue with the Angular animations module. The same problem can be observed after removing all CDK drag&drop directives and changing the list using a button. Here's a forked example that shows the issue: https://stackblitz.com/edit/angular-3katn7-12d5fw?file=src%2Fapp%2Fcdk-drag-drop-custom-placeholder-example.html.

I'll transfer this over to the Framework repo.

@crisbeto crisbeto transferred this issue from angular/components Feb 8, 2021
@ngbot ngbot bot added this to the needsTriage milestone Feb 8, 2021
@jelbourn jelbourn changed the title bug(cdk-drag-drop): :enter / :leave animations are triggered incorrectly [Animations] :enter / :leave animations are triggered incorrectly Feb 10, 2021
@jelbourn jelbourn changed the title [Animations] :enter / :leave animations are triggered incorrectly [Animations] :enter / :leave animations are triggered incorrectly for reordered items Feb 10, 2021
@jelbourn jelbourn added the P4 A relatively minor issue that is not relevant to core functions label Feb 10, 2021
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Feb 10, 2021
@dario-piotrowicz
Copy link
Contributor

Can this be related to the lack or a trackBy function? 🤔

I tried adding one and it changes how thing play out (still not right but differently)

With a trackBy function the leave animation works perfectly, but... always animates the last element in the list, after the elements have been shifted...

the animation package likely doesn't know which element is being removed and just animated the last one as leaving (which makes sense I guess)

I guess this could be updated to that it would detect which element is actually being removed, but then it would just "snap" the rest of the elements in place... right? doesn't seem the best either 🤔

@wolanskip
Copy link

wolanskip commented Mar 8, 2022 •

trackBy function did not correct the issue for me; but for my purposes I have a partial workaround that might be helpful in some other scenarios. It does not seem to solve the exact example case however, because the example is using an array of strings, whereas my scenario has an array of objects so I can force the changedetection by updating the object reference.

It seems that it is related to using the moveItemInArray method. The object reference is staying the same, and the position in the array is changing, but the array reference is unchanged. This is beneficial because it prevents a total redraw (which would trigger many :leave and :enter animations). I suspect though, that there's some disconnect between the movement in the shadow dom vs the actual dom, as the visual element changes order, without triggering leave and enter.

By manually recreating the moved object, it would be detected as a change by *ngFor, and it then adds a :leave animation to the old position, and an :enter animation to the new position, but then the :leave works perfectly on the deletion at the new location. The side effect seems to be that the "old" position lingers and the "new" is created, until the animation completes, then the "old" is removed.

It's an acceptable workaround for my scenario, but it is not ideal.

html template

<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="drop($event)">
    <div class="example-box" fxLayout="row" fxLayoutAlign="space-between center" *ngFor="let option of picklistOptions" [@addRemove] cdkDrag>
        <div cdkDragHandle><mat-icon>drag_handle</mat-icon></div>
        <div style="flex: 1 1 0">{{option.value}}</div>
        <div>
            <button mat-button (click)="updateSelection(option)">
                <mat-icon>{{(option.selected) ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
            </button>
            <button mat-button (click)="deleteMe(option)"><mat-icon>delete</mat-icon></button>
        </div>
    </div>
</div>

drop event

    public drop(event: CdkDragDrop<string[]>) {
        moveItemInArray(this.picklistOptions, event.previousIndex, event.currentIndex);
        
        // recreating the object at the new index forces an :enter animation to trigger at the new location
        // as well as a :leave animation at the old location.
        // this allows subsequent `:leave` animation to work correctly after the item has been moved.
        
        const {value, selected} = this.picklistOptions[event.currentIndex];
        this.picklistOptions[event.currentIndex] = {value, selected};

        this.formControl.setValue({picklistValues: this.picklistOptions});
    }

Change in action - :enter is green, :leave is red:
image

The subsequent delete on the moved element:
image

@wolanskip
Copy link

Followup: By using the [@.disabled] attribute to conditionally disable the animation, and a changeDetectorRef.detectChanges(), I am able to suppress either the :leave or the :enter animations on the drop, and that gets me to a good place.

The below snippet will suppress the :leave animation so the old position just disappears immediately. It still renders the :enter.

html:

<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="drop($event)" >
    <div class="example-box" fxLayout="row" fxLayoutAlign="space-between center" *ngFor="let option of picklistOptions" [@addRemove] cdkDrag [@.disabled]="animationsDisabled">
        <div cdkDragHandle><mat-icon>drag_handle</mat-icon></div>
        <div fxFlex="0">{{option.value}}</div>
        <div>
            <button mat-button (click)="updateSelection(option)">
                <mat-icon>{{(option.selected) ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
            </button>
            <button mat-button (click)="deleteMe(option)"><mat-icon>delete</mat-icon></button>
        </div>
    </div>
</div>

ts:

    public animationsDisabled = false;
    
    public drop(event: CdkDragDrop<string[]>) {
        
        // If you want to suppress the leave event of the duplicate, disable animation
        this.animationsDisabled = true;
        this.changeDetectorRef.detectChanges();
        
        moveItemInArray(this.picklistOptions, event.previousIndex, event.currentIndex);
        const {value, selected} = this.picklistOptions[event.currentIndex];
        this.picklistOptions[event.currentIndex] = {value, selected};

        this.formControl.setValue({picklistValues: this.picklistOptions});

        // restore animation
        this.animationsDisabled = false;
    }

@stuartgibson
Copy link

I have what I assume is a related issue, but for my use case it's significantly more of an issue than a basic animation glitch.

I have reorderable components that can be toggled between a view and edit state, with a (rather hacky) crossfade animation. After a component has been moved in the list, toggling into edit mode leaves a permanent non-interactive ghost of the view state. You can then move the component again, and repeat, ending up with the view state repeated multiple times.

https://stackblitz.com/edit/angular-55zvwf-3srsuo?file=src%2Fapp%2Fcdk-drag-drop-sorting-example.html

@simeyla
Copy link

simeyla commented Jun 27, 2023 •

@jessicajaniuk What ever happened to this? It can be more serious than just a glitchy animation if it hits you.

I came across a very similar issue to the one described by @stuartgibson above.

I was able to demonstrate broken behavior even with just a simple opacity fade of elements inside the *ngFor item template (not :enter or :leave animation).

Basically my conclusion is that is just isn't safe to use any kind of Angular animation inside a list that may get reordered. The current state of the item's animation is just not retained after the item is moved.

I created a new issue #50861

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: animations P4 A relatively minor issue that is not relevant to core functions
Projects
None yet
Development

No branches or pull requests

8 participants