4

I am currently creating my own toastr service as seen in the GIF below


enter image description here


What I want to achieve https://stackblitz.com/edit/angular-ivy-tgm4st?file=src/app/app.component.ts But without queryselector. From what i have read, you should not be using queryselector for retrieving elements in the DOM in angular


The issue Whenever I click the CTA button I add a toast element to an array of toasts which the component is subscribed to and utilizes to update the DOM.

The toasts are generated like this:

export class ToastComponent implements OnInit {
  constructor(private toast: ToastService, protected elementRef: ElementRef) {}

  toasts = this.toast.Toasts;

  <div
    class="toast-wrapper wobble-animation"
    *ngFor="let t of toasts.value"
    (click)="DestroyToast(t, $event)"

What I want I want to add an eventlistener to the toast whenever 'animationend' to destroy the HTML element. I already do this by when clicking with this line of code:

       DestroyToast(element, event): void {
        event.target.classList.remove('wobble-animation');
        event.target.classList.add('slide-out-animation');
        event.target.addEventListener('animationend', () => {
          this.toasts.value.splice(this.toasts.value.indexOf(element), 1);
        });
      }

My initial thought was to subscribe to the array and use that as an eventlistener for when something is pushed. I would then use a function to fetch the latest toast and add another eventlistener, the 'animationend' one.

I tried the method like this:

  ngOnInit(): void {
      this.toast.Toasts.subscribe((args) => {
      this.UpdateToasts();
     });
  }
  UpdateToasts() {
    let toastElements = document.querySelectorAll('.toast');
    console.log(toastElements);
  }

But unfortunately it is too slow and always returns null on the first event.

enter image description here


I think that I have read that using querySelector in angular is generally bad practice. So the question is:

How to get a dynamically generated element in Angular without querySelector?


FULL CODE

Toast.Component.ts

import { ToastService } from './../../services/toast.service';
import { toast } from './toast.model';
import { Component, OnInit, ElementRef } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-toast',
  templateUrl: './toast.component.html',
  styleUrls: ['./toast.component.scss'],
})
export class ToastComponent implements OnInit {
  constructor(private toast: ToastService, protected elementRef: ElementRef) {}

  toasts = this.toast.Toasts;
  ngOnInit(): void {
    this.toast.Toasts.subscribe((args) => {
      this.UpdateToasts();
    });
  }
  ngOnDestroy() {
    this.toasts.unsubscribe();
  }
  DestroyToast(element, event): void {
    event.target.classList.remove('wobble-animation');
    event.target.classList.add('slide-out-animation');
    event.target.addEventListener('animationend', () => {
      this.toasts.value.splice(this.toasts.value.indexOf(element), 1);
    });
  }
  UpdateToasts() {
    let toastElements = document.querySelectorAll('.toast');
    console.log(toastElements);
  }
}

Toast.Component.html

<div class="toast-container">
  <div
    class="toast-wrapper wobble-animation"
    *ngFor="let t of toasts.value"
    (click)="DestroyToast(t, $event)"
  >
    <div
      class="toast default"
      [ngClass]="{ 'slide-out-animation': t.TimeLeft < 1 }"
    >
      <div class="notification-count" *ngIf="t.Count > 1">
        {{ t.Count }}
      </div>
      <div class="content-container">
        <p class="title">
          {{ t.Title }}
        </p>
        <p class="content">{{ t.Content }}</p>
      </div>
      <span class="progress">
        <span
          class="real-progress"
          [ngStyle]="{ 'width.%': t.PercentageCompleted }"
        ></span>
      </span>
    </div>
  </div>
</div>

Toast.Service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { toast } from '../components/toast/toast.model';

@Injectable({
  providedIn: 'root',
})
export class ToastService {
  public Toasts = new BehaviorSubject<Array<object>>([]);

  constructor() {}

  Toast(Title: string, Message?: string, Style?: string, Timer?: number) {
    const toastModel = new toast({
      Title: Title,
      Content: Message,
      Timer: Timer,
      Style: Style,
      TimeLeft: Timer,
      Count: 1,
      PercentageCompleted: 100,
    });
    this.AddToast(toastModel);
  }

  private AddToast(toast: toast) {
    const currentArr = this.Toasts.value;
    const updatedToast = [...currentArr, toast];
    let timer = setInterval(function () {
      toast.PercentageCompleted = toast.TimeLeft / (toast.Timer / 100);
      toast.TimeLeft = toast.TimeLeft - 10;
      if (toast.TimeLeft <= 0 || !toast.TimeLeft) {
        clearInterval(timer);
      }
    }, 10);
    this.Toasts.next(updatedToast);
  }
}

Link to website with live code ModernnaMedia

3
  • Please add a minimal code stackblitz example showing your use case so it's easier for everyone to provide a solution, btw. ViewChildren should be enough to solve your issue, you just need to move your logic to ngOnChanges to prevent null references Commented Nov 30, 2021 at 18:47
  • @LuisLimas Thanks! I will look into it when i get off work. I'll let you know how it goes :) Commented Dec 1, 2021 at 8:05
  • @LuisLimas I have updated and added stackbiz! Commented Dec 1, 2021 at 19:52

3 Answers 3

1

I'm not 100% sure I understood you correctly, there seem to be two animationend events going on.

I want to add an eventlistener to the toast whenever 'animationend' to destroy the HTML element.

You can bind that directly in the template:

<div
  *ngFor="let toast of toasts"
  #toastEl
  (animationend)="DestroyToast(toastEl)"
  class="toast">
</div>
DestroyToast(toastEl: HTMLElement) {
    // …
}
Sign up to request clarification or add additional context in comments.

3 Comments

Wow did not know you could do tthat! Thats great but unfortunately I have 2 animations. one entry and one exit. I can solve this issue if I am able to use document.querySelector(); but as mentioned, the query selector returns null. This can be solved with a timeout, but I want to try and stick to angular best principles. However, your answer could be a backup solution. +1
Angular also has its own animation framework that might be worth looking into (opinions on it differ – it's also fine to stick with CSS animations, but the animation framework does give you some integration options).
I see, thanks for the tip! I have been looking into it, but I prefer CSS animations
1
+50

Like mentioned by others already, using ViewChildren would be the "Angular" way to do it, instead of queryselector. We can also with ViewChildren subscribe to changes of the querylist we are listening to! I think that is probably suitable for your code...

So first, attach a ref to the toasts, here I just call it myToasts:

<div
  #myToasts
  class="toast default"
  [ngClass]="{ 'slide-out-animation': t.TimeLeft < 1 }"
>

OK, now declare the querylist in the component:

@ViewChildren('myToasts') myToasts: QueryList<ElementRef>;

Now you can simply subscribe to the changes in AfterViewInit and do whatever you need to do with the elements:

ngAfterViewInit() {
  this.myToasts.changes.subscribe(toasts => {
    console.log('Array length: ', toasts.length);
    console.log('Array of elements: ', toasts.toArray())
  })
}

1 Comment

Thank you! this looks very promising. I will test it and get back to you!
0

if you add rxjs delay function after your observable variable like below

this.toast.Toasts.pipe(delay(0)).subscribe(()=>{this.UpdateToasts();})

you will not get null reference error. and if you don't want to use queryselector you can use angular viewchildren for more information visit angular documentation site. https://angular.io/api/core/ViewChildren

1 Comment

You could also use SetTimeout, however i strongly believe that this is not best practice. However thanks for your suggestion

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.