From 55452ff211270be440bd178aab91c08b711bd0c2 Mon Sep 17 00:00:00 2001 From: Niek Raaijmakers Date: Mon, 11 Jan 2021 19:16:46 +0100 Subject: [PATCH] feat: support lazy loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 LazyMapTo support Co-authored-by: Niek Raaijmakers Co-authored-by: Cezary Czernecki --- angular.json | 12 +++- .../layout/aem-component.directive.spec.ts | 17 ++++- src/lib/layout/aem-component.directive.ts | 64 +++++++++++++++---- src/lib/layout/component-mapping.spec.ts | 20 +++++- src/lib/layout/component-mapping.ts | 64 ++++++++++++++++--- .../lazy-component-wrapper/lazy.component.ts | 29 +++++++++ .../test/lazy-component-wrapper/lazy.entry.ts | 13 ++++ .../lazy-component-wrapper/lazy.module.ts | 25 ++++++++ src/lib/test/test-comp1.component.ts | 2 +- src/lib/test/test-comp2.component.ts | 2 +- src/lib/test/test-comp3.component.ts | 2 +- 11 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 src/lib/test/lazy-component-wrapper/lazy.component.ts create mode 100644 src/lib/test/lazy-component-wrapper/lazy.entry.ts create mode 100644 src/lib/test/lazy-component-wrapper/lazy.module.ts diff --git a/angular.json b/angular.json index 942f4a3..26c958c 100644 --- a/angular.json +++ b/angular.json @@ -48,7 +48,10 @@ "options": { "main": "src/test.ts", "tsConfig": "tsconfig.spec.json", - "codeCoverageExclude": ["src/test.ts", "src/lib/test/**/*"], + "codeCoverageExclude": [ + "src/test.ts", + "src/lib/test/**/*" + ], "karmaConfig": "karma.conf.js" } }, @@ -67,5 +70,8 @@ } } }, - "defaultProject": "spa-angular-editable-components" -} + "defaultProject": "spa-angular-editable-components", + "cli": { + "analytics": false + } +} \ No newline at end of file diff --git a/src/lib/layout/aem-component.directive.spec.ts b/src/lib/layout/aem-component.directive.spec.ts index 2d88155..7aadb8e 100644 --- a/src/lib/layout/aem-component.directive.spec.ts +++ b/src/lib/layout/aem-component.directive.spec.ts @@ -14,7 +14,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { AEMComponentDirective } from './aem-component.directive'; import { Component, Input } from '@angular/core'; -import { ComponentMapping, MapTo } from './component-mapping'; +import { ComponentMapping, MapTo, LazyMapTo} from './component-mapping'; import { Utils } from './utils'; @Component({ @@ -43,6 +43,7 @@ class DirectiveComponent { } } MapTo('directive/comp')(DirectiveComponent); +LazyMapTo('some/lazy/comp')(() => import('../test/lazy-component-wrapper/lazy.component').then((m) => m.LazyComponent)); describe('AEMComponentDirective', () => { @@ -103,6 +104,20 @@ describe('AEMComponentDirective', () => { expect(dynamicElement.getAttribute('attr1')).toEqual(componentData['attr1']); expect(dynamicElement.getAttribute('attr2')).toEqual(componentData['attr2']); }); + it('should correctly pass the inputs for lazy component', async() => { + const componentData = { + some: 'Some value', + ':type': 'some/lazy/comp' + }; + + component.data = componentData; + + await import('../test/lazy-component-wrapper/lazy.component'); + fixture.detectChanges(); + + }); + + it('should setup the placeholder', () => { isInEditorSpy.and.returnValue(true); diff --git a/src/lib/layout/aem-component.directive.ts b/src/lib/layout/aem-component.directive.ts index e209a49..430e4ee 100644 --- a/src/lib/layout/aem-component.directive.ts +++ b/src/lib/layout/aem-component.directive.ts @@ -11,15 +11,20 @@ */ import { - Directive, - Input, - Renderer2, - ViewContainerRef, + AfterViewInit, + ChangeDetectorRef, + Compiler, + ComponentFactory, ComponentFactoryResolver, ComponentRef, - AfterViewInit, + Directive, + Injector, + Input, + OnChanges, + OnDestroy, OnInit, - OnDestroy, ChangeDetectorRef, OnChanges + Renderer2, + ViewContainerRef } from '@angular/core'; import { ComponentMapping } from './component-mapping'; @@ -34,7 +39,6 @@ const PLACEHOLDER_CLASS_NAME = 'cq-placeholder'; @Directive({ selector: '[aemComponent]' }) - /** * The current directive provides advanced capabilities among which are * @@ -82,17 +86,47 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, */ @Input() itemAttrs: any; + @Input() loaded: boolean; + @Input() aemComponent; + constructor( private renderer: Renderer2, private viewContainer: ViewContainerRef, + private compiler: Compiler, + private injector: Injector, private factoryResolver: ComponentFactoryResolver, private _changeDetectorRef: ChangeDetectorRef) { } - ngOnInit(): void { - this.renderComponent(ComponentMapping.get(this.type)); + async ngOnInit() { + + if (this.type) { + const mappedFn = ComponentMapping.get(this.type); + + if (mappedFn) { + this.renderComponent(mappedFn); + } else { + await this.initializeAsync(); + } + } else { + console.warn('no type on ' + this.cqPath); + } + + } + + async initializeAsync() { + const lazyMappedPromise: Promise = ComponentMapping.lazyGet(this.type); + + try { + const LazyResolvedComponent = await lazyMappedPromise; + this.renderComponent(LazyResolvedComponent); + this.loaded = true; + this._changeDetectorRef.detectChanges(); + } catch (err) { + console.warn(err); + } } ngOnChanges(): void { @@ -115,12 +149,18 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, if (componentDefinition) { const factory = this.factoryResolver.resolveComponentFactory(componentDefinition); - this.viewContainer.clear(); - this._component = this.viewContainer.createComponent(factory); - this.updateComponentData(); + this.renderWithFactory(factory); + } else { + throw new Error('No component definition!!'); } } + private renderWithFactory(factory: ComponentFactory) { + this.viewContainer.clear(); + this._component = this.viewContainer.createComponent(factory); + this.updateComponentData(); + } + /** * Updates the data of the component based the data of the directive */ diff --git a/src/lib/layout/component-mapping.spec.ts b/src/lib/layout/component-mapping.spec.ts index b113ab3..1fb0c5e 100644 --- a/src/lib/layout/component-mapping.spec.ts +++ b/src/lib/layout/component-mapping.spec.ts @@ -10,10 +10,11 @@ * governing permissions and limitations under the License. */ -import { ComponentMapping, MapTo } from './component-mapping'; +import { ComponentMapping, LazyMapTo, MapTo } from './component-mapping'; +import LazyComponent from '../test/lazy-component-wrapper/lazy.component'; describe('Component Mapping', () => { - it('stores configuration', () => { + it('stores configuration', async () => { const Component1 = function() { /* void */ }; const Component2 = function() { /* void */ }; const editConfig1 = { some: 1 }; @@ -21,9 +22,24 @@ describe('Component Mapping', () => { MapTo('component1')(Component1, editConfig1); MapTo('component2')(Component2, editConfig2); + LazyMapTo('lazycomponent3')(() => new Promise((resolve, reject) => { + import('../test/lazy-component-wrapper/lazy.component') + .then((Module) => { resolve(Module.LazyComponent); }) + .catch(reject); + })); + LazyMapTo('lazycomponent4')(() => import('../test/lazy-component-wrapper/lazy.component').then((m) => m.LazyComponent)); expect(ComponentMapping.get('component1')).toBe(Component1); expect(ComponentMapping.get('component2')).toBe(Component2); + + const LoadedComp = await ComponentMapping.lazyGet('lazycomponent3'); + + expect(LoadedComp).toBe(LazyComponent); + + const DefaultComp = await ComponentMapping.lazyGet('lazycomponent4'); + + expect(DefaultComp).toBe(LazyComponent); + expect(ComponentMapping.getEditConfig('component1')).toBe(editConfig1); expect(ComponentMapping.getEditConfig('component2')).toBe(editConfig2); }); diff --git a/src/lib/layout/component-mapping.ts b/src/lib/layout/component-mapping.ts index 1db037c..465b8ae 100644 --- a/src/lib/layout/component-mapping.ts +++ b/src/lib/layout/component-mapping.ts @@ -23,7 +23,7 @@ export class ComponentMappingWithConfig { */ private editConfigMap = {}; - constructor(private spaMapping:SPAComponentMapping) {} + constructor(private spaMapping: SPAComponentMapping) {} /** * Stores a component class for the given resource types and also allows to provide an EditConfig object @@ -34,36 +34,80 @@ export class ComponentMappingWithConfig { map(resourceTypes, clazz, editConfig = null) { const innerClass = clazz; - if (editConfig) { - this.editConfigMap[resourceTypes] = editConfig; - } + if (editConfig) { + this.editConfigMap[resourceTypes] = editConfig; + } + this.spaMapping.map(resourceTypes, innerClass); + } - this.spaMapping.map(resourceTypes, innerClass); - } + /** + * Stores a clazz the lazy way for dynamic imports / code splitting.function that returns a promise + * @param resourceTypes - List of resource types + * @param lazyClassFunction - A function that returns a promise that resolves a Component class + * @param [editConfig] - Edit configuration to be stored for the given resource types + */ + lazyMap(resourceTypes, lazyClassFunction: () => Promise, editConfig = null) { + const innerFunction = lazyClassFunction; + + if (editConfig) { + this.editConfigMap[resourceTypes] = editConfig; + } + this.spaMapping.lazyMap(resourceTypes, innerFunction); + } /** * Returns the component class for the given resourceType * @param resourceType - Resource type for which the component class has been stored */ - get(resourceType: string): any { + get(resourceType: string): unknown { return this.spaMapping.get(resourceType); } + /** + * Returns the component class for the given resourceType + * @param resourceType - Resource type for which the component class has been stored + */ + lazyGet(resourceType: string): Promise { + return this.spaMapping.getLazy(resourceType); + } + /** * Returns the EditConfig structure for the given type * @param resourceType - Resource type for which the configuration has been stored */ - getEditConfig(resourceType) { - return this.editConfigMap[resourceType]; + getEditConfig(resourceType: string) { + return this.editConfigMap[resourceType]; } } const componentMapping = new ComponentMappingWithConfig(SPAComponentMapping); +/** + * Stores a component class for the given resource types and also allows to provide an EditConfig object + * @param resourceTypes - List of resource types + */ function MapTo(resourceTypes) { + /** + * @param clazz - Component class to be stored + * @param [editConfig] - Edit configuration to be stored for the given resource types + */ return (clazz, editConfig = null): any => { return componentMapping.map(resourceTypes, clazz, editConfig); }; } -export { componentMapping as ComponentMapping, MapTo }; +/** + * Stores a clazz the lazy way for dynamic imports / code splitting.function that returns a promise + * @param resourceTypes - List of resource types + */ +function LazyMapTo(resourceTypes) { + /** + * @param lazyClassPromise - Function that returns a promise resolving a class + * @param [editConfig] - Edit configuration to be stored for the given resource types + */ + return (lazyClassFunction: () => Promise, editConfig = null) => { + return componentMapping.lazyMap(resourceTypes, lazyClassFunction, editConfig); + }; +} + +export { componentMapping as ComponentMapping, MapTo, LazyMapTo }; diff --git a/src/lib/test/lazy-component-wrapper/lazy.component.ts b/src/lib/test/lazy-component-wrapper/lazy.component.ts new file mode 100644 index 0000000..3b36d05 --- /dev/null +++ b/src/lib/test/lazy-component-wrapper/lazy.component.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'lazy-comp', + template: `
{{ some }}
` +}) +/** + * The current class carries the base presentational logic of the AEM Layout Container (aka. Responsive grid) + */ +export class LazyComponent { + @Input() cqPath: string; + @Input() isInEditor: boolean; + @Input() some: string; + +} + +export default LazyComponent; diff --git a/src/lib/test/lazy-component-wrapper/lazy.entry.ts b/src/lib/test/lazy-component-wrapper/lazy.entry.ts new file mode 100644 index 0000000..06ab75a --- /dev/null +++ b/src/lib/test/lazy-component-wrapper/lazy.entry.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +export * from './lazy.module'; +export * from './lazy.component'; diff --git a/src/lib/test/lazy-component-wrapper/lazy.module.ts b/src/lib/test/lazy-component-wrapper/lazy.module.ts new file mode 100644 index 0000000..9dac66e --- /dev/null +++ b/src/lib/test/lazy-component-wrapper/lazy.module.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { LazyComponent } from './lazy.component'; + +@NgModule({ + imports: [ CommonModule, BrowserModule, RouterModule ], + declarations: [ + LazyComponent + ] +}) +export class LazyModule { +} diff --git a/src/lib/test/test-comp1.component.ts b/src/lib/test/test-comp1.component.ts index 98a8c9a..0402607 100644 --- a/src/lib/test/test-comp1.component.ts +++ b/src/lib/test/test-comp1.component.ts @@ -21,7 +21,7 @@ import { Component, Input } from '@angular/core'; }) export class Test1Component { - @Input() title: any; + @Input() title: string; constructor() { // empty diff --git a/src/lib/test/test-comp2.component.ts b/src/lib/test/test-comp2.component.ts index ed25fb4..7f1ac76 100644 --- a/src/lib/test/test-comp2.component.ts +++ b/src/lib/test/test-comp2.component.ts @@ -21,7 +21,7 @@ import { Component, Input } from '@angular/core'; }) export class Test2Component { - @Input() title:any; + @Input() title: string; constructor() { // empty diff --git a/src/lib/test/test-comp3.component.ts b/src/lib/test/test-comp3.component.ts index 162b2bc..37ec128 100644 --- a/src/lib/test/test-comp3.component.ts +++ b/src/lib/test/test-comp3.component.ts @@ -21,7 +21,7 @@ import { Component, Input } from '@angular/core'; }) export class Test3Component { - @Input() title: any; + @Input() title: string; constructor() { // empty }