Reputation: 2804
I am currently creating my own toastr service as seen in the GIF below
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.
I think that I have read that using querySelector in angular is generally bad practice. So the question is:
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
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
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
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