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: [ 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.component.ts b/projects/ngx-element/src/lib/ngx-element.component.ts index 9bfca92..62d5787 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,16 +11,17 @@ import { EventEmitter, ElementRef, Injector, - 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 +35,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 +61,9 @@ export class NgxElementComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.ngxElementService.getComponentToLoad(this.selector).subscribe(event => { + const selector = this.resolveSelector(); + + 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 +74,19 @@ 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 (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 = this.extractProjectedNodes(factory); + this.container.clear(); + this.componentRef = this.container.createComponent(factory, 0, this.refInjector, projectNodes); this.setAttributes(attributes); this.listenToAttributeChanges(); @@ -97,7 +106,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 @@ -138,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.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.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 } ] diff --git a/projects/ngx-element/src/lib/ngx-element.service.ts b/projects/ngx-element/src/lib/ngx-element.service.ts index 6c8d4eb..abd9030 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,20 @@ 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 => { + 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 +104,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}; +}