I'm building directive which should add class when element entered viewport and will also trigger custom event. I found 2 approaches to trigger the event - EventEmitter
and dispatchEvent()
, both works fine. Which should be used in this case and why? (Any other advices on the code appreciated)
import { EventEmitter, Directive, ElementRef, Renderer2, OnInit } from '@angular/core';
import { HostListener } from "@angular/core";
import { Component, Input, Output, Inject, PLATFORM_ID, ViewChild, ViewEncapsulation } from "@angular/core";
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { AfterViewInit } from '@angular/core/src/metadata/lifecycle_hooks';
@Directive({
selector: '[animateOnVisible]',
})
export class AnimateOnVisibleDirective implements AfterViewInit {
@Input() animateOnVisible: string = "fadeInUp";
@Output() enteredViewport: EventEmitter<string> = new EventEmitter();
public isBrowser: boolean;
private enableListener: boolean = true;
constructor(private renderer: Renderer2, private hostElement: ElementRef, @Inject(PLATFORM_ID) private platformId: any) {
this.isBrowser = isPlatformBrowser(platformId);
}
@HostListener("window:scroll", [])
onWindowScroll() {
this.checkScrollPosition();
}
ngAfterViewInit() {
this.checkScrollPosition();
}
private checkScrollPosition() {
if (this.isBrowser && this.enableListener && window.scrollY + window.innerHeight / 2 >= this.hostElement.nativeElement.offsetTop) {
this.renderer.addClass(this.hostElement.nativeElement, this.animateOnVisible);
this.enableListener = false;
//triggering custom event
this.enteredViewport.emit("");
//OR
this.hostElement.nativeElement.dispatchEvent(new Event('enteredViewport', { bubbles: true }));
}
}
}
<div class="animated" [animateOnVisible]="'test'" (enteredViewport)="test()">
Unlike "native" events, which are fired by the browser and invoke event handlers asynchronously via the event loop, dispatchEvent() invokes event handlers synchronously.
🎊 Event Emitters in Angular 🎊 Data flows into your component via property bindings and flows out of your component through event bindings. If you want your component to notify his parent about something you can use the Output decorator with EventEmitter to create a custom event.
Publishing custom events To publish custom events, we will need an instance of ApplicationEventPublisher and then call the method ApplicationEventPublisher#publishEvent(..) . Another way to publish event is to use ApplicationContext#publishEvent(....) .
Custom events are created in Angular using its EventEmitter class. These events are used to communicate to the Parent Component from a child Component.
EventEmitter EventEmitter is a class in angular framework. It has emit () method that emits custom events. We can use EventEmitter in custom event binding.
The Angular DebugElement instance provides a handy method for triggering events — triggerEventHandler (). Let’s see how we can use it. We have a simple test for a component that, upon a click, sets an emoji.
Three important facts about the triggerEventHandler () method: It will invoke the event handler only if it was declared on the native element by using Angular event bindings, the @HostListener () or @Output decorators (and the less used Renderer.listen () ). For example:
EventEmitter is a class in angular framework. It has emit () method that emits custom events. We can use EventEmitter in custom event binding. To achieve it first we need to import it in our component file as given below.
EventEmitter
is used for @Output()
s that can be used for Angular event binding
<my-component (myEvent)="doSomething()"
dispatchEvent()
fires a DOM event, that also can be bound to like shown for the Angular @Output()
event, but can also bubble up the DOM tree.
The former is specific to Angular and for the intented use cases more efficient, the later behaves like other DOM events and can also be listened to by non-Angular code, but might be less efficient.
A combined solution is also possible. Consider a child component that has an EventEmitter for deletion requests:
onDeleteRequest: EventEmitter<GridElementDeleteRequestEvent> =
new EventEmitter<GridElementDeleteRequestEvent>();
The child component listens for keyboard events to emit GridElementDeleteRequestEvent
itself and these being picked up by the parent component.
@HostListener('keyup', ['$event']) private keyUpHandler(e: KeyboardEvent) {
if (e.key === 'Delete') {
this.onDeleteRequest.emit(new GridElementDeleteRequestEvent(this._gridElement.id));
}
A parent component subscribes to it:
<app-ipe-grid-element (onDeleteRequest)="this.gridElementDeleteRequestHandler($event)">
Where the handler has the following implementation:
public gridElementDeleteRequestHandler(e: GridElementDeleteRequestEvent) {
// code
...
}
In the child there is a deeper nested structure of inner components. One of these inner components is a context sensitive menu that also offers the possibility to delete the so-called GridElement
(child in this story).
To prevent a cumbersome architecture tying all EventEmitters from nested components to each other the context sensitive menu dispatches a "regular" DOM Event like this:
const event: CustomEvent =
new CustomEvent('GridElementDeleteRequestEvent',
{
bubbles: true,
cancelable: true,
detail: new GridElementDeleteRequestEvent(
this._gridElement.gridElement.id)});
this.nativeElement.dispatchEvent(event);
To account for this the parent component's handler only has to be decorated with the HostListener
directive and the incoming event is checked upon Type (instanceof
) and when it is a CustomEvent
the detail is casted to a GridElementDeleteRequestEvent
like this:
@HostListener('GridElementDeleteRequestEvent', ['$event'])
public gridElementDeleteRequestHandler(e: CustomEvent) {
const customEvent: GridElementDeleteRequestEvent = e instanceof GridElementDeleteRequestEvent ?
e :
<GridElementDeleteRequestEvent>e.detail;
// code
...
With this approach both direct (EventEmitter
) and indirect (DOM dispatched) events are handled within one event handler on the parent.
Note
Off course this raises the question if the EventEmitter
at the child shouldn't be removed completely and the child's keyboard event handler should also just dispatch a DOM Event
like this:
@HostListener('keyup', ['$event']) private keyUpHandler(e: KeyboardEvent) {
if (e.key === 'Delete') {
const event: CustomEvent = new CustomEvent(
'GridElementDeleteRequestEvent',
{
bubbles: true,
cancelable: true,
detail: new GridElementDeleteRequestEvent(this.gridElement.id)
});
this.nativeElement.dispatchEvent(event);
}
This would make the implementation more simple and account for identical Events coming in from different "places" (multiple nested elements at different levels).
A tiny argument to keep the "direct" EventEmitter (in this particular case) might be taken from @Günter Zöchbauer's answer where the EventEmitter is supposed to be (slightly) more efficent. The use case where this answer originates from doesn't involve dozens of GridElementDeletRequestEvent
's so keeping the EventEmitter will have negligible effect.
The requirement for having the GridElementDeleteRequestEvent being fired from multiple Angular components and the desire to keep the code as simple as possible weigh heavier than a supposed slightly more efficient behavior of the EventEmitter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With