From 003720338b6c7411b82d5edff583103390d99b60 Mon Sep 17 00:00:00 2001 From: Michael Krog Date: Wed, 16 Dec 2020 11:03:32 +0100 Subject: [PATCH 1/5] Adds eg-brs extension This commit adds support for: * native custom element tag names * content projection via ng-content * less verbose configuration when many components are registered. --- .../src/lib/ngx-element.component.ts | 53 ++++++++++++---- .../ngx-element/src/lib/ngx-element.module.ts | 23 ++++--- .../src/lib/ngx-element.service.ts | 61 +++++++++++-------- projects/ngx-element/src/lib/tokens.ts | 15 ++++- 4 files changed, 103 insertions(+), 49 deletions(-) diff --git a/projects/ngx-element/src/lib/ngx-element.component.ts b/projects/ngx-element/src/lib/ngx-element.component.ts index 9bfca92..a42418c 100644 --- a/projects/ngx-element/src/lib/ngx-element.component.ts +++ b/projects/ngx-element/src/lib/ngx-element.component.ts @@ -12,16 +12,19 @@ import { EventEmitter, ElementRef, Injector, - ReflectiveInjector + ReflectiveInjector, + Inject } from '@angular/core'; import {NgxElementService} from './ngx-element.service'; import {merge, Subscription} from 'rxjs'; import {map} from 'rxjs/operators'; +import { LazyComponentRegistry, LAZY_CMPS_REGISTRY } from './tokens'; @Component({ - selector: 'lib-ngx-element', template: ` - + + + `, styles: [] }) @@ -35,12 +38,13 @@ export class NgxElementComponent implements OnInit, OnDestroy { componentToLoad: Type; componentFactoryResolver: ComponentFactoryResolver; injector: Injector; - refInjector: ReflectiveInjector; + refInjector: Injector; constructor( private ngxElementService: NgxElementService, - private elementRef: ElementRef - ) {} + private elementRef: ElementRef, + @Inject(LAZY_CMPS_REGISTRY) private registry: LazyComponentRegistry + ) { } /** * Subscribe to event emitters of a lazy loaded and dynamically instantiated Angular component @@ -60,7 +64,11 @@ export class NgxElementComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.ngxElementService.getComponentToLoad(this.selector).subscribe(event => { + const selector = this.registry.useCustomElementNames ? + this.elementRef.nativeElement.localName.substring(this.registry.customElementNamePrefix.length + 1) : + this.selector; + + this.ngxElementService.getComponentToLoad(selector).subscribe(event => { this.componentToLoad = event.componentClass; this.componentFactoryResolver = this.ngxElementService.getComponentFactoryResolver(this.componentToLoad); this.injector = this.ngxElementService.getInjector(this.componentToLoad); @@ -71,13 +79,32 @@ export class NgxElementComponent implements OnInit, OnDestroy { } createComponent(attributes) { - this.container.clear(); const factory = this.componentFactoryResolver.resolveComponentFactory(this.componentToLoad); - this.refInjector = ReflectiveInjector.resolveAndCreate( - [{provide: this.componentToLoad, useValue: this.componentToLoad}], this.injector - ); - this.componentRef = this.container.createComponent(factory, 0, this.refInjector); + // If selector have already registered as a customComponent then ignore the component + if (this.ngxElementService.isSelectorRegistered(factory.selector)) { + console.log(`Cannot load ${factory.selector} because it defines a selector that is already registered as a customComponent. This + warning may occur if the lazy-loaded component has the same selector defined as defined in the config for lazy loading.`); + return; + } + + this.refInjector = Injector.create({ providers: [{ provide: this.componentToLoad, useValue: this.componentToLoad }] }); + + const projectNodes = []; + factory.ngContentSelectors.forEach(selector => { + const el = this.elementRef.nativeElement as HTMLElement; + const content = el.querySelectorAll(selector); + if(content) { + const nodes = []; + content.forEach(c => { + const p = c.parentElement; + nodes.push(p.removeChild(c)); + }) + projectNodes.push(nodes); + } + }); + this.container.clear(); + this.componentRef = this.container.createComponent(factory, 0, this.refInjector, projectNodes); this.setAttributes(attributes); this.listenToAttributeChanges(); @@ -97,7 +124,7 @@ export class NgxElementComponent implements OnInit, OnDestroy { for (let attr, i = 0; i < attrs.length; i++) { attr = attrs[i]; - if (attr.nodeName.match('^data-')) { + if ((!this.registry.useCustomElementNames && attr.nodeName.match('^data-')) || this.registry.useCustomElementNames) { attributes.push({ name: this.camelCaseAttribute(attr.nodeName), value: attr.nodeValue diff --git a/projects/ngx-element/src/lib/ngx-element.module.ts b/projects/ngx-element/src/lib/ngx-element.module.ts index 04f5bbe..90f2ff2 100644 --- a/projects/ngx-element/src/lib/ngx-element.module.ts +++ b/projects/ngx-element/src/lib/ngx-element.module.ts @@ -1,7 +1,7 @@ -import { NgModule, Injector, ModuleWithProviders } from '@angular/core'; +import { NgModule, Injector, ModuleWithProviders, Inject } from '@angular/core'; import { createCustomElement } from '@angular/elements'; import { NgxElementComponent } from './ngx-element.component'; -import { LAZY_CMPS_PATH_TOKEN } from './tokens'; +import { LazyComponentRegistry, LAZY_CMPS_REGISTRY } from './tokens'; @NgModule({ declarations: [NgxElementComponent], @@ -9,18 +9,25 @@ import { LAZY_CMPS_PATH_TOKEN } from './tokens'; }) export class NgxElementModule { - constructor(private injector: Injector) { - const ngxElement = createCustomElement(NgxElementComponent, { injector }); - customElements.define('ngx-element', ngxElement); + constructor(private injector: Injector, @Inject(LAZY_CMPS_REGISTRY) private registry: LazyComponentRegistry) { + if(!registry.useCustomElementNames) { + const ngxElement = createCustomElement(NgxElementComponent, { injector }); + customElements.define('ngx-element', ngxElement); + } else { + registry.definitions.forEach(def => { + const ngxElement = createCustomElement(NgxElementComponent, { injector }); + customElements.define(`${registry.customElementNamePrefix}-${def.selector}`, ngxElement); + }); + } } - static forRoot(modulePaths: any[]): ModuleWithProviders { + static forRoot(registry: any): ModuleWithProviders { return { ngModule: NgxElementModule, providers: [ { - provide: LAZY_CMPS_PATH_TOKEN, - useValue: modulePaths + provide: LAZY_CMPS_REGISTRY, + useValue: registry } ] }; diff --git a/projects/ngx-element/src/lib/ngx-element.service.ts b/projects/ngx-element/src/lib/ngx-element.service.ts index 6c8d4eb..afa2578 100644 --- a/projects/ngx-element/src/lib/ngx-element.service.ts +++ b/projects/ngx-element/src/lib/ngx-element.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, NgModuleFactory, Type, Compiler, Injector, ComponentFactoryResolver } from '@angular/core'; -import { LAZY_CMPS_PATH_TOKEN, LazyComponentDef } from './tokens'; +import { LAZY_CMPS_REGISTRY, LazyComponentDef, LazyComponentRegistry } from './tokens'; import { LazyCmpLoadedEvent } from './lazy-component-loaded-event'; import { Observable, from } from 'rxjs'; @@ -15,15 +15,12 @@ export class NgxElementService { componentFactoryResolvers = new Map, ComponentFactoryResolver>(); constructor( - @Inject(LAZY_CMPS_PATH_TOKEN) - modulePaths: { - selector: string - }[], + @Inject(LAZY_CMPS_REGISTRY) private registry: LazyComponentRegistry, private compiler: Compiler, private injector: Injector ) { const ELEMENT_MODULE_PATHS = new Map(); - modulePaths.forEach(route => { + registry.definitions.forEach(route => { ELEMENT_MODULE_PATHS.set(route.selector, route); }); @@ -53,6 +50,16 @@ export class NgxElementService { return from(registered); } + isSelectorRegistered(selector: string) { + let result = false; + this.registry.definitions.forEach(def => { + if (selector === def.selector) { + result = true; + } + }); + return result; + } + /** * Allows to lazy load a component given its selector. * If the component selector has been registered, it's according module @@ -93,35 +100,35 @@ export class NgxElementService { } }) .then(moduleFactory => { - const elementModuleRef = moduleFactory.create(this.injector); - let componentClass; + const elementModuleRef = moduleFactory.create(this.injector); + let componentClass; - if (typeof elementModuleRef.instance.customElementComponent === 'object') { - componentClass = elementModuleRef.instance.customElementComponent[componentSelector]; + if (typeof elementModuleRef.instance.customElementComponent === 'object') { + componentClass = elementModuleRef.instance.customElementComponent[componentSelector]; - if (!componentClass) { - // tslint:disable-next-line: no-string-throw - throw `You specified multiple component elements in module ${elementModuleRef} but there was no match for tag + if (!componentClass) { + // tslint:disable-next-line: no-string-throw + throw `You specified multiple component elements in module ${elementModuleRef} but there was no match for tag ${componentSelector} in ${JSON.stringify(elementModuleRef.instance.customElementComponent)}. Make sure the selector in the module is aligned with the one specified in the lazy module definition.`; - } - } else { - componentClass = elementModuleRef.instance.customElementComponent; } + } else { + componentClass = elementModuleRef.instance.customElementComponent; + } - // Register injector of the lazy module. - // This is needed to share the entryComponents between the lazy module and the application - const moduleInjector = elementModuleRef.injector; - this.receiveContext(componentClass, moduleInjector); + // Register injector of the lazy module. + // This is needed to share the entryComponents between the lazy module and the application + const moduleInjector = elementModuleRef.injector; + this.receiveContext(componentClass, moduleInjector); - this.loadedComponents.set(componentSelector, componentClass); - this.elementsLoading.delete(componentSelector); - this.componentsToLoad.delete(componentSelector); + this.loadedComponents.set(componentSelector, componentClass); + this.elementsLoading.delete(componentSelector); + this.componentsToLoad.delete(componentSelector); - resolve({ - selector: componentSelector, - componentClass - }); + resolve({ + selector: componentSelector, + componentClass + }); }) .catch(err => { this.elementsLoading.delete(componentSelector); diff --git a/projects/ngx-element/src/lib/tokens.ts b/projects/ngx-element/src/lib/tokens.ts index 15c47e0..837aa99 100644 --- a/projects/ngx-element/src/lib/tokens.ts +++ b/projects/ngx-element/src/lib/tokens.ts @@ -2,9 +2,22 @@ import { InjectionToken } from '@angular/core'; import { LoadChildrenCallback } from '@angular/router'; /* Injection token to provide the element path modules. */ -export const LAZY_CMPS_PATH_TOKEN = new InjectionToken('ngx-lazy-cmp-registry'); +export const LAZY_CMPS_REGISTRY = new InjectionToken('ngx-lazy-cmp-registry'); export interface LazyComponentDef { selector: string; loadChildren: LoadChildrenCallback; // prop needs to be named like this } + +export interface LazyComponentRegistry { + /** A list of LazyComponentDef for this registry. */ + definitions: LazyComponentDef[]; + /** Whether this uses native custom element tag names or an selector attribute. */ + useCustomElementNames: boolean; + /** If useCustomElementNames is true, then this specifies the REQUIRED prefix for the tag names according to Custom Element specification. */ + customElementNamePrefix?: string; +} + +export function createDef(selector: string, loadChildren: LoadChildrenCallback): LazyComponentDef { + return {selector, loadChildren}; +} From 19be9c9fee3db77abc58ec03c9174dcd2f5c4252 Mon Sep 17 00:00:00 2001 From: Michael Krog Date: Wed, 16 Dec 2020 11:04:53 +0100 Subject: [PATCH 2/5] Removes unused imports. --- projects/ngx-element/src/lib/ngx-element.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/ngx-element/src/lib/ngx-element.component.ts b/projects/ngx-element/src/lib/ngx-element.component.ts index a42418c..ed5c243 100644 --- a/projects/ngx-element/src/lib/ngx-element.component.ts +++ b/projects/ngx-element/src/lib/ngx-element.component.ts @@ -3,7 +3,6 @@ import { ComponentFactory, OnInit, Input, - Output, Type, ViewChild, ViewContainerRef, @@ -12,7 +11,6 @@ import { EventEmitter, ElementRef, Injector, - ReflectiveInjector, Inject } from '@angular/core'; import {NgxElementService} from './ngx-element.service'; From 6c535877fdd3a1aeadb24c397ed10c4ab3a70005 Mon Sep 17 00:00:00 2001 From: Michael Krog Date: Wed, 16 Dec 2020 11:06:46 +0100 Subject: [PATCH 3/5] Cleans code a bit to make it easier to read. --- .../src/lib/ngx-element.component.ts | 51 +++++++++++-------- .../src/lib/ngx-element.service.ts | 4 ++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/projects/ngx-element/src/lib/ngx-element.component.ts b/projects/ngx-element/src/lib/ngx-element.component.ts index ed5c243..62d5787 100644 --- a/projects/ngx-element/src/lib/ngx-element.component.ts +++ b/projects/ngx-element/src/lib/ngx-element.component.ts @@ -20,8 +20,7 @@ import { LazyComponentRegistry, LAZY_CMPS_REGISTRY } from './tokens'; @Component({ template: ` - - + `, styles: [] @@ -62,9 +61,7 @@ export class NgxElementComponent implements OnInit, OnDestroy { } ngOnInit(): void { - const selector = this.registry.useCustomElementNames ? - this.elementRef.nativeElement.localName.substring(this.registry.customElementNamePrefix.length + 1) : - this.selector; + const selector = this.resolveSelector(); this.ngxElementService.getComponentToLoad(selector).subscribe(event => { this.componentToLoad = event.componentClass; @@ -79,28 +76,15 @@ export class NgxElementComponent implements OnInit, OnDestroy { createComponent(attributes) { const factory = this.componentFactoryResolver.resolveComponentFactory(this.componentToLoad); - // If selector have already registered as a customComponent then ignore the component - if (this.ngxElementService.isSelectorRegistered(factory.selector)) { - console.log(`Cannot load ${factory.selector} because it defines a selector that is already registered as a customComponent. This - warning may occur if the lazy-loaded component has the same selector defined as defined in the config for lazy loading.`); + if (this.registry.useCustomElementNames && this.ngxElementService.isSelectorRegistered(factory.selector)) { + console.warn(`Cannot lazy load component that defines ${factory.selector} as a selector, because the selector is + already reserved in the LazyComponentRegistry.`); return; } this.refInjector = Injector.create({ providers: [{ provide: this.componentToLoad, useValue: this.componentToLoad }] }); - const projectNodes = []; - factory.ngContentSelectors.forEach(selector => { - const el = this.elementRef.nativeElement as HTMLElement; - const content = el.querySelectorAll(selector); - if(content) { - const nodes = []; - content.forEach(c => { - const p = c.parentElement; - nodes.push(p.removeChild(c)); - }) - projectNodes.push(nodes); - } - }); + const projectNodes = this.extractProjectedNodes(factory); this.container.clear(); this.componentRef = this.container.createComponent(factory, 0, this.refInjector, projectNodes); @@ -163,4 +147,27 @@ export class NgxElementComponent implements OnInit, OnDestroy { this.componentRef.destroy(); this.ngElementEventsSubscription.unsubscribe(); } + + private extractProjectedNodes(factory: ComponentFactory) { + const projectNodes = []; + factory.ngContentSelectors.forEach(selector => { + const el = this.elementRef.nativeElement as HTMLElement; + const content = el.querySelectorAll(selector); + if (content) { + const nodes = []; + content.forEach(c => { + const p = c.parentElement; + nodes.push(p.removeChild(c)); + }); + projectNodes.push(nodes); + } + }); + return projectNodes; + } + + private resolveSelector() { + return this.registry.useCustomElementNames ? + this.elementRef.nativeElement.localName.substring(this.registry.customElementNamePrefix.length + 1) : + this.selector; + } } diff --git a/projects/ngx-element/src/lib/ngx-element.service.ts b/projects/ngx-element/src/lib/ngx-element.service.ts index afa2578..abd9030 100644 --- a/projects/ngx-element/src/lib/ngx-element.service.ts +++ b/projects/ngx-element/src/lib/ngx-element.service.ts @@ -50,6 +50,10 @@ export class NgxElementService { return from(registered); } + /** + * Checks whether the selector is registered in the registry. + * @param selector + */ isSelectorRegistered(selector: string) { let result = false; this.registry.definitions.forEach(def => { From 8f9a9d466b194d190be7ceeaa196c5a19b9aea71 Mon Sep 17 00:00:00 2001 From: Michael Krog Date: Wed, 16 Dec 2020 11:11:26 +0100 Subject: [PATCH 4/5] Updates tests (which I forgot) --- .../src/lib/ngx-element.component.spec.ts | 25 ++++++++----------- .../src/lib/ngx-element.service.spec.ts | 21 +++++++--------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/projects/ngx-element/src/lib/ngx-element.component.spec.ts b/projects/ngx-element/src/lib/ngx-element.component.spec.ts index f7688c0..2da169e 100644 --- a/projects/ngx-element/src/lib/ngx-element.component.spec.ts +++ b/projects/ngx-element/src/lib/ngx-element.component.spec.ts @@ -1,34 +1,31 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NgxElementComponent } from './ngx-element.component'; -import { LAZY_CMPS_PATH_TOKEN } from './tokens'; +import { createDef, LazyComponentRegistry, LAZY_CMPS_REGISTRY } from './tokens'; describe('NgxElementComponent', () => { let component: NgxElementComponent; let fixture: ComponentFixture; - const lazyConfig = [ - { - selector: 'talk', - loadChildren: () => import('../../../ngx-element-app/src/app/talk/talk.module').then(m => m.TalkModule) - }, - { - selector: 'sponsor', - loadChildren: () => import('../../../ngx-element-app/src/app/sponsor/sponsor.module').then(m => m.SponsorModule) - } - ]; + const lazyConfig: LazyComponentRegistry = { + definitions: [ + createDef('talk', () => import('../../../ngx-element-app/src/app/talk/talk.module').then(m => m.TalkModule)), + createDef('sponsor', () => import('../../../ngx-element-app/src/app/sponsor/sponsor.module').then(m => m.SponsorModule)) + ], + useCustomElementNames: false + }; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ NgxElementComponent ], + declarations: [NgxElementComponent], providers: [ { - provide: LAZY_CMPS_PATH_TOKEN, + provide: LAZY_CMPS_REGISTRY, useValue: lazyConfig } ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/projects/ngx-element/src/lib/ngx-element.service.spec.ts b/projects/ngx-element/src/lib/ngx-element.service.spec.ts index bb5d93d..6d0ad7e 100644 --- a/projects/ngx-element/src/lib/ngx-element.service.spec.ts +++ b/projects/ngx-element/src/lib/ngx-element.service.spec.ts @@ -1,26 +1,23 @@ import { TestBed } from '@angular/core/testing'; import { NgxElementService } from './ngx-element.service'; -import { LAZY_CMPS_PATH_TOKEN } from './tokens'; +import { createDef, LazyComponentRegistry, LAZY_CMPS_REGISTRY } from './tokens'; describe('NgxElementService', () => { let service: NgxElementService; - const lazyConfig = [ - { - selector: 'talk', - loadChildren: () => import('../../../ngx-element-app/src/app/talk/talk.module').then(m => m.TalkModule) - }, - { - selector: 'sponsor', - loadChildren: () => import('../../../ngx-element-app/src/app/sponsor/sponsor.module').then(m => m.SponsorModule) - } - ]; + const lazyConfig: LazyComponentRegistry = { + definitions: [ + createDef('talk', () => import('../../../ngx-element-app/src/app/talk/talk.module').then(m => m.TalkModule)), + createDef('sponsor', () => import('../../../ngx-element-app/src/app/sponsor/sponsor.module').then(m => m.SponsorModule)) + ], + useCustomElementNames: false + }; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { - provide: LAZY_CMPS_PATH_TOKEN, + provide: LAZY_CMPS_REGISTRY, useValue: lazyConfig } ] From 3b811c36d6899dc662ea01489da78426ba734f13 Mon Sep 17 00:00:00 2001 From: Michael Krog Date: Wed, 16 Dec 2020 11:14:50 +0100 Subject: [PATCH 5/5] Updates readme's --- README.md | 13 ++++++++----- projects/ngx-element/README.md | 14 +++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 140f1a9..15d726d 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,14 @@ export class TalkModule { Just like with the Angular Router, define the map of component selector and lazy module. ``` -const lazyConfig = [ - { - selector: 'talk', - loadChildren: () => import('./talk/talk.module').then(m => m.TalkModule) - } +const lazyConfig = { + definitions: [ + { + selector: 'talk', + loadChildren: () => import('./talk/talk.module').then(m => m.TalkModule) + } + ], + useCustomElementNames: false ]; @NgModule({ diff --git a/projects/ngx-element/README.md b/projects/ngx-element/README.md index d0f01d4..ec1b4df 100644 --- a/projects/ngx-element/README.md +++ b/projects/ngx-element/README.md @@ -42,13 +42,17 @@ export class TalkModule { Just like with the Angular Router, define the map of component selector and lazy module. ``` -const lazyConfig = [ - { - selector: 'talk', - loadChildren: () => import('./talk/talk.module').then(m => m.TalkModule) - } +const lazyConfig = { + definitions: [ + { + selector: 'talk', + loadChildren: () => import('./talk/talk.module').then(m => m.TalkModule) + } + ], + useCustomElementNames: false ]; + @NgModule({ ..., imports: [