How can one access a dynamically generated element in Angular without using querySelector?

Currently in the process of developing my custom toastr service, as shown in the GIF below


https://i.sstatic.net/Zpbxs.gif


My Objective: https://stackblitz.com/edit/angular-ivy-tgm4st?file=src/app/app.component.ts But without using queryselector. It's recommended to avoid queryselector for accessing elements in the DOM within Angular.


The Problem: Whenever I click the CTA button, a toast element is added to an array of toasts that the component subscribes to and uses to update the DOM.

The toast generation looks 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)"

Desired Outcome: I aim to attach an event listener to each toast element for 'animationend' to remove the HTML element. Currently, I achieve this by removing the animation classes on click with the following code snippet:

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

Initially, I attempted subscribing to the array and using it as an event listener when new items are pushed. Then, fetching the latest toast and adding another event listener for 'animationend'. However, this method turned out to be slow and consistently returned null upon the first event triggering.

https://i.sstatic.net/YErml.gif


I've learned that utilizing querySelector in Angular is generally discouraged. So, the question remains:

How can we access dynamically generated elements in Angular without relying on 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

Answer №1

It appears there may be two instances of the animationend event happening based on my understanding.

I am looking to attach an event listener to the toast that will trigger when 'animationend' occurs, in order to remove the HTML element.

You have the option to directly bind this in the template:

<div
  *ngFor="let toast of toasts"
  #toastEl
  (animationend)="DestroyToast(toastEl)"
  class="toast">
</div>
DestroyToast(toastEl: HTMLElement) {
    // …
}

Answer №2

As others have mentioned, using ViewChildren is the preferred method in Angular for working with elements rather than querySelector. With ViewChildren, we can also subscribe to changes in the querylist we are observing, making it a suitable choice for your code...

To start, add a reference to the toasts, like so, naming it myToasts:

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

Next, declare the querylist in the component:

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

You can then easily subscribe to changes in AfterViewInit and perform any necessary operations on the elements:

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

Answer №3

By implementing the rxjs delay function after your observable variable in the following manner

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

You can avoid encountering a null reference error. If you prefer not to use queryselector, Angular provides viewchildren as an alternative option. For further details, refer to the official Angular documentation site. https://angular.io/api/core/ViewChildren

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having difficulties in properly connecting faces while UV mapping a cube in Three.js

After successfully applying an image texture to a cube through UV mapping for a photo-sphere viewer, I noticed that thin straight lines are visible where the faces of the cube join. Interestingly, this issue does not occur when splitting texture tiles via ...

Is it possible to analyze the performance of NodeJS applications using Visual Studio Code?

I have managed to establish a successful connection between the VS Code debugger and my remote NodeJS target through the Chrome protocol. I am aware that this protocol allows for profiling and performance measurements within the Chrome Dev Tools, but I am ...

I am curious about why I am unable to utilize inline functions in component props. Could you please provide a detailed explanation and perhaps give an example to illustrate? Furthermore, what is

Please take note: The component prop accepts a component, not a render function. Do not pass an inline function (e.g. component={() => }), as this will cause your component to unmount and remount, losing all state when the parent component re-renders. F ...

What is the process for reinserting a list item into the nested elements?

Looking for help with manipulating a menu in HTML? I have a menu that I want to modify by removing and adding list items. While I've had success removing items, I'm struggling to properly use the add method. Here's an example of what my menu ...

Distinguishing between `notEmpty` and `exists` in express-validator: what sets them apart

I'm confused about the distinction between exists and notEmpty in express-validator as they seem to function identically. ...

The customized sweet alert button is failing to trigger its designated function

I integrated vue-swal to show a pop-up dialog with customized functionality. However, I faced an issue while modifying the swal. In my modified version, there are 3 buttons each with specific actions that should be triggered upon clicking. But for some rea ...

The JQuery library seems to be unresponsive on my webpage, despite being correctly included

Despite trying multiple ways to include the JQuery library on my page, I keep encountering the "$ is not defined" error. I have ensured that all the links were correct and from various sources, both local and external. What other options should I consider ...

Attempting to achieve a carousel animation using jQuery

Upon loading the page, only one character out of four is displayed. Two arrows are provided - one on the left and one on the right. Clicking on the left arrow causes the current character to fade out and the previous character to fade in. Clicking on the r ...

"Customizable rectangular container with jagged edges created with Scalable Vector Graphics

Currently, I am undertaking a small project that involves creating a box with rough edges around some text. To achieve this effect, I am utilizing an SVG with unique edges similar to the design found at this link: (except mine is in SVG format). My goal ...

Initial request in the sequence is a conditional request

Currently, I am attempting to make a request in rxjs that is conditional based on whether or not the user has uploaded a file. If a file has been uploaded, I need to attach it to the user object before sending it off, and then proceed to patch the user aft ...

Unable to locate additional elements following javascript append utilizing Chrome WebDriver

I have a simple HTML code generated from a C# dotnet core ASP application. I am working on a webdriver test to count the number of input boxes inside the colorList div. Initially, the count is two which is correct, but when I click the button labeled "+", ...

What is the best way to determine the total of values from user-input fields that are created dynamically

Scenario- A scenario where a parent component is able to create and delete input fields (child components) within an app by clicking buttons. The value of each input field is captured using v-model. Issue- The problem arises when a new input field is crea ...

There is a button on my website that uses a small piece of Javascript to post a tweet, but unfortunately, it is not functional on mobile devices

Check out this link to view the demonstration - http://codepen.io/illpill/pen/VbeVEq function sendTweet(message, author) { window.open('https://twitter.com/intent/tweet?hashtags=thequotemachine&text=' + encodeURIComponent('"' + m ...

Including the --aot flag in the Angular CLI can cause issues with the app

Recently, I encountered an issue with my Angular app while using dynamic forms. Everything was working fine until I added the --aot flag to my CLI command. Suddenly, I started receiving the error message "Property 'controls' does not exist on typ ...

Steps to finish (refresh) a mongoDB record

Currently, I am dealing with the following scenario: An API request from one service is creating multiple MongoDB documents in a single collection. For example: [ {_id: 1, test1: 2, test: 3}, {_id: 2, test1: 3, test: 4} ] Subsequently, a second service ...

The variable 'props' is given a value but is never utilized - warning about unused variables in Vue 3

Within my Vue component file, I am using the following code: <template> <router-link :to="{ name: routerName }" type="is-primary" class="inline-flex justify-center py-2 px-3 mb-3 border border-transparent shado ...

The CSS selector functions as expected when used in a web browser, however, it

While conducting test automation using Selenium, I typically rely on css selectors to find elements. However, I recently came across a peculiar issue. I observed that in certain cases, the css selector works perfectly when tested in the browser console. Fo ...

Creating a line chart using data from a MySQL database with the help of PHP and

After completing a program, I am now tasked with extracting data from MySQL and presenting it using HTML/PHP. The data retrieval process involves utilizing the mysql.php file: <?php $hostname = "localhost"; $database = "database"; $username ...

How to properly implement ng-if for a select option in Angular.js?

Within the controller, I defined a scope variable: $scope.readOnly = true. In the HTML code: <select ng-if="readOnly" ng-model="readOnly" required> <option value="true" selected>True</option> <option value="false">False ...

TypeScript and the Safety of Curried Functions

What is the safest way to type curried functions in typescript? Especially when working with the following example interface Prop { <T, K extends keyof T>(name: K, object: T): T[K]; <K>(name: K): <T>(object: T) => /* ?? */; ...