import { Component, ComponentRef, Input, Type, ViewChild, ViewContainerRef, OnDestroy, AfterViewInit, EventEmitter, ElementRef, EnvironmentInjector, reflectComponentType } from '@angular/core'; import { Subscription, merge } from 'rxjs'; import { map } from 'rxjs/operators'; import { NgxElementService } from './ngx-element.service'; @Component({ selector: 'lib-ngx-element', template: ``, standalone: true }) export class NgxElementComponent implements AfterViewInit, OnDestroy { private ngElementEventsSubscription?: Subscription; private attributeObserver?: MutationObserver; @Input() selector!: string; @ViewChild('container', { read: ViewContainerRef, static: true }) container!: ViewContainerRef; componentRef?: ComponentRef; componentToLoad?: Type; injector?: EnvironmentInjector; constructor( private readonly ngxElementService: NgxElementService, private readonly elementRef: ElementRef ) {} /** * Subscribe to event emitters of a lazy loaded and dynamically instantiated Angular component * and dispatch them as Custom Events on the NgxElementComponent that is used in a template. */ private setProxiedOutputs(): void { if (!this.componentToLoad || !this.componentRef) { return; } const mirror = reflectComponentType(this.componentToLoad); if (!mirror || mirror.outputs.length === 0) { return; } const eventEmitters = mirror.outputs.map(({ propName, templateName }) => { const emitter = (this.componentRef!.instance as Record>)[ propName ]; return emitter.pipe(map((value: unknown) => ({ name: templateName, value }))); }); this.ngElementEventsSubscription = merge(...eventEmitters).subscribe((subscription) => { this.elementRef.nativeElement.dispatchEvent( new CustomEvent(subscription.name, { detail: subscription.value }) ); }); } ngAfterViewInit(): void { this.ngxElementService.getComponentToLoad(this.selector).subscribe((event) => { this.componentToLoad = event.componentClass; this.injector = this.ngxElementService.getInjector(this.componentToLoad); this.createComponent(this.getElementAttributes()); }); } createComponent(attributes: { name: string; value: string }[]): void { this.container.clear(); this.componentRef = this.container.createComponent(this.componentToLoad!, { environmentInjector: this.injector }); this.setAttributes(attributes); this.componentRef.changeDetectorRef.detectChanges(); this.listenToAttributeChanges(); this.setProxiedOutputs(); } setAttributes(attributes: { name: string; value: string }[]): void { attributes.forEach((attr) => { (this.componentRef!.instance as Record)[attr.name] = attr.value; }); } getElementAttributes(): { name: string; value: string }[] { const attrs = this.elementRef.nativeElement.attributes; const attributes: { name: string; value: string }[] = []; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.nodeName.match('^data-')) { attributes.push({ name: this.camelCaseAttribute(attr.nodeName), value: attr.nodeValue ?? '' }); } } return attributes; } camelCaseAttribute(attribute: string): string { const attr = attribute.replace('data-', ''); const chunks = attr.split('-'); if (chunks.length > 1) { return ( chunks[0] + chunks .slice(1) .map((chunk) => chunk.replace(/^\w/, (c) => c.toUpperCase())) .join('') ); } return attr; } listenToAttributeChanges(): void { this.attributeObserver?.disconnect(); this.attributeObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes') { const attributes = this.getElementAttributes(); this.setAttributes(attributes); this.componentRef?.changeDetectorRef.detectChanges(); } }); }); this.attributeObserver.observe(this.elementRef.nativeElement, { attributes: true }); } ngOnDestroy(): void { this.attributeObserver?.disconnect(); this.ngElementEventsSubscription?.unsubscribe(); this.componentRef?.destroy(); } }