Stanley
Stanley

Reputation: 2804

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

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

Upvotes: 4

Views: 1532

Answers (3)

AVJT82
AVJT82

Reputation: 73367

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())
  })
}

Upvotes: 1

Mohammadreza Mohammadi
Mohammadreza Mohammadi

Reputation: 137

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

Upvotes: 0

Ingo B&#252;rk
Ingo B&#252;rk

Reputation: 20043

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) {
    // …
}

Upvotes: 1

Related Questions