Reputation: 5068
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()">
Upvotes: 6
Views: 11579
Reputation: 5655
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.
Upvotes: 4
Reputation: 658007
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.
Upvotes: 4