1

Background:

I am working on an Angular 20.3.10 application where I use signals for managing the state of a collection of toast messages. I have SSR and Zoneless enabled and have a web worker registered with plans to learn about PWA.

The Problem:

I have a @for loop in my template that iterates over a signal. The signal is updated correctly when I remove an item, and I have verified this using console.log and an effect() statement. However, the @for loop does not remove the corresponding DOM element when the signal is truncated.

I am using the same signal to run CSS animations which I can see are working. I also tried using ChangeDetectorRef and explicitly marking the component for check after the signal is updated, but the issue persists.

Here is some relevant code:

Toast Service

export interface Toast {
  id: number;
  message: string;
}

@Injectable({
  providedIn: 'root',
})
export class ToastService {
  private readonly toastCollection = signal<Toast[]>([]);
  private nextToastId = 0;

  getToastCollectionSignal() {
    return this.toastCollection.asReadonly();
  }

  addToast(message: string): void {
    const toast: Toast = { id: this.nextToastId++, message };
    this.toastCollection.update((toasts) => [...toasts, toast]);
  }

  removeToast(id: number): void {
    this.toastCollection.update((toasts) =>
      toasts.filter((toast) => toast.id !== id)
    );
  }
}

Toast Template

<div class="fixed top-0 right-0 z-50 flex flex-col flex-nowrap p-4">
  @for (toast of this.toasts(); track toast.id) {
    <aside
      animate.leave="toast-dismiss"
      (animationstart)="onAnimationStart(toast.state, toast.id)"
      (animationend)="onAnimationEnd(toast.state, toast.id)"
      (mouseout)="mouseOutToast(toast.id)"
      (mouseover)="mouseOverToast(toast.id)"
      [class]="
        mergeClasses(
          'toast-item mt-0 mb-2 flex min-h-16 max-w-64 min-w-64 flex-col rounded-md 
           border-l-4 border-solid bg-background-600 px-2 shadow-md shadow-background-800',
          toast.typeDecoration,
          toast.state === ToastState.SHIFT_UP && 'toast-shift',
          toast.state === ToastState.EXPAND && 'toast-expand',
          toast.state === ToastState.DISMISS && 'toast-dismiss'
        )
      "
    >
      <div class="inline-flex w-full flex-row justify-between">
        <header class="font-semibold">{{ toast.title }}</header>
        <button (click)="dismissToast(toast.id)">
          <small>✖</small>
        </button>
      </div>
      <span
        class="overflow-y-hidden text-sm text-pretty hover:overflow-y-scroll"
      >
        {{ toast.message }}
      </span>
    </aside>
  }
</div>

Toast Component

@Component({
  selector: 'app-toast',
  templateUrl: './toast.html',
  styleUrls: ['./toast.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToastComponent {
  private toastService = inject(ToastService);
  private cdRef = inject(ChangeDetectorRef);

  toasts = computed(() => this.toastService.getToastCollectionSignal()());

  addToast(): void {
    this.toastService.addToast('New Toast Message');
  }

  removeToast(id: number): void {
    this.toastService.removeToast(id);
    this.cdRef.markForCheck(); // Tried this, but it doesn't help
  }
}

I've used a lot of console.log statements to follow the code execution and everything seems fine. I'm inclined to believe the issue lies in my lack of understanding when it comes to Zoneless Angular and change detection. However, the fact that the animations are working as expected must mean that the DOM is being updated since I'm using CSS native animations (at least I think that's how that works).

The call to remove the dismissed toast actually occurs inside onAnimationEnd() if that helps.

5
  • 1
    feels like you should do toastCollectionSignal = this.toastCollection.asReadonly(). this way signal instanse will be the same rather than new on each iteration and it should work Commented Nov 12 at 9:19
  • You're right, I had made this change temporarily trying random things. The issue persists with either method. Commented Nov 12 at 10:04
  • @AsadKoths Issue not reproducible stackblitz please replicate in this stackblitz and share back, if possible share github repo with issue happening and steps for replication Commented Nov 12 at 10:19
  • In your template, why are you using this.toasts()? Please try just toast() without this. Commented Nov 12 at 10:20
  • 1
    So I've fixed the issue by rewriting how I handle my animations. My mistake was likely to do with my improper usage of the animate.leave API. The API was failing silently so I'm not exactly sure where the issue lies. I removed the toast-dismiss class applied in mergeClasses, replaced (animationstart) with (animate.leave) Commented Nov 12 at 10:51

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.