I am working with a directive created a while ago by a colleague who no longer works here. I use it to open dynamic overlays triggered by events.
For example I can do :
<button [myDirective]="getTheComponentToOpenAsOverlay()"></button>
When I use the button, it opens an overlay based on a component and some configuration options :
public getTheComponentToOpenAsOverlay(): DynamicOverlayConfig<TheComponent> {
return {
component: TheComponent,
data: {
...
},
hostRef: this.appContainer,
removeOnScroll: true,
removeOnOutsideClick: true
};
}
In the “TheComponent” I set up html/css and any function I need.
But now, I need to update my parent component (in which I open the overlay) after executing some functions. I’d gladly use the @Output
from Angular, but I don’t see how to use it in this case : I call the component using a directive, not with its selector.
I guess I could add a service to broadcast calls from the child to the parent, but I’m not sure this is a good way of doing things.
Here’s the directive code, I guess it’ll help :
import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Directive,
EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, SimpleChanges, Type,
ViewContainerRef } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { DynamicOverlayReadyEvent } from './dynamic-overlay.interface';
export interface DynamicOverlayConfig<T> {
component: Type<T>;
hostRef?: ViewContainerRef;
data?: { [key in keyof T]?: any };
removeOnScroll?: boolean;
removeOnOutsideClick?: boolean;
}
@Directive({
selector: '[myDirective]',
})
export class SyDynamicOverlayDirective<T> implements OnChanges, OnDestroy {
@Input('syDynamicOverlay') config: SyDynamicOverlayConfig<T>;
@Output('onSyDynamicOverlayReady') onSyDynamicOverlayReady: EventEmitter<DynamicOverlayReadyEvent> = new EventEmitter();
componentFactory: ComponentFactory<T>;
dynamicComponentRef: ComponentRef<T>;
isComponentCreated = new BehaviorSubject(false);
listeners: Map<string, EventListener> = new Map();
@HostListener('click', ['$event']) createRemoveDynamicOverlayComponent(event: Event) {
event.stopPropagation();
if (this.hostRef && this.componentFactory) {
if (!this.isComponentCreated.getValue()) {
this.createComponent();
} else {
this.removeComponent();
}
}
}
constructor(
private hostRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
) {
this.isComponentCreated.subscribe(isComponentCreated => {
if (this.dynamicComponentRef) {
if (isComponentCreated) {
if (this.config.removeOnScroll) {
this.removeOnScroll();
}
if (this.config.removeOnOutsideClick) {
this.removeOnOutsideClick();
}
} else {
this.removeListeners();
}
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.config?.currentValue) {
if (changes.config.currentValue.component) {
this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(changes.config.currentValue.component);
}
if (changes.config.currentValue.hostRef) {
this.hostRef = changes.config.currentValue.hostRef;
}
}
}
ngAfterViewInit(): void {
this.onSyDynamicOverlayReady.emit({overlayApi: this.generateDynamicOverlayApi()});
}
removeOnOutsideClick() {
const outsideClickListener = event => {
event.stopPropagation();
if (this.dynamicComponentRef) {
const clickedInside = this.dynamicComponentRef?.location?.nativeElement?.contains(event.target);
if (!clickedInside) {
this.removeComponent();
}
}
};
this.listeners.set('click', outsideClickListener);
window.addEventListener('click', outsideClickListener);
}
removeOnScroll() {
const scrollListener = () => {
this.removeComponent();
};
this.listeners.set('scroll', scrollListener);
window.addEventListener('scroll', scrollListener);
}
removeComponent() {
// Find the component
const componentIndex = this.hostRef.indexOf(this.dynamicComponentRef?.hostView);
// Remove component from both view and array
if (componentIndex !== -1) {
this.hostRef.remove(componentIndex);
}
this.isComponentCreated.next(false);
this.removeListeners();
}
createComponent() {
this.hostRef.clear();
this.dynamicComponentRef = this.hostRef.createComponent(this.componentFactory);
Object.keys(this.config.data).forEach(key => {
this.dynamicComponentRef.instance[key] = this.config.data[key];
});
this.isComponentCreated.next(true);
}
removeListeners() {
this.listeners.forEach((listener, eventType) => {
window.removeEventListener(eventType, listener);
});
this.listeners = new Map();
}
ngOnDestroy() {
this.removeListeners();
}
generateDynamicOverlayApi() {
return {
close: this.removeComponent.bind(this)
};
}
}
Is it even possible to do it with my current code ?
I tried adding an @Ouput
on the DynamicOverlayDirective
but I have no idea how to call it from my child component, I’m not event sure that it is possible.