From c1729f136fd4e2e16a8c41c59b0825c2fcbbac5a Mon Sep 17 00:00:00 2001 From: Niek Raaijmakers Date: Mon, 14 Jun 2021 16:15:21 +0200 Subject: [PATCH 1/3] feature(component-directive): make the component directive extensible --- .gitignore | 2 + src/lib/layout/aem-component.directive.ts | 50 ++++++++++++++++------- src/lib/layout/component-mapping.ts | 8 +++- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 0ea9de3..6bdd727 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ .scannerwork/ **/*.log *.tgz +coverage +.idea diff --git a/src/lib/layout/aem-component.directive.ts b/src/lib/layout/aem-component.directive.ts index e1f81db..fbbbc79 100644 --- a/src/lib/layout/aem-component.directive.ts +++ b/src/lib/layout/aem-component.directive.ts @@ -28,7 +28,12 @@ import { ViewContainerRef } from '@angular/core'; -import { ComponentMapping, MappedComponentProperties } from './component-mapping'; +import { + ComponentMapping, + ComponentMappingProvider, + ComponentMappingWithConfig, + MappedComponentProperties +} from './component-mapping'; import { Constants } from './constants'; import { Utils } from './utils'; @@ -93,18 +98,18 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, constructor( - private renderer: Renderer2, - private viewContainer: ViewContainerRef, - private compiler: Compiler, - private injector: Injector, - private factoryResolver: ComponentFactoryResolver, - private _changeDetectorRef: ChangeDetectorRef) { + protected renderer: Renderer2, + protected viewContainer: ViewContainerRef, + protected compiler: Compiler, + protected injector: Injector, + protected factoryResolver: ComponentFactoryResolver, + protected _changeDetectorRef: ChangeDetectorRef) { } async ngOnInit() { - if (this.type) { - const mappedFn:Type = ComponentMapping.get(this.type); + if (this.getType()) { + const mappedFn:Type = this.getComponentMappingProvider().get(this.getType()); if (mappedFn) { this.renderComponent(mappedFn); @@ -118,15 +123,23 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, } async initializeAsync() { - const lazyMappedPromise: Promise> = ComponentMapping.lazyGet(this.type); - try { + const lazyMappedPromise: Promise> = this.getComponentMappingProvider().lazyGet(this.getType()); const LazyResolvedComponent = await lazyMappedPromise; this.renderComponent(LazyResolvedComponent); this.loaded = true; this._changeDetectorRef.detectChanges(); } catch (err) { - console.warn(err); + + if(!!this.getFallbackComponent()){ + this.renderComponent(this.getFallbackComponent()); + this.loaded = true; + this._changeDetectorRef.detectChanges(); + console.info("loaded fallback component. cause:", err); + }else{ + console.warn(err); + } + } } @@ -134,10 +147,17 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, this.updateComponentData(); } + protected getFallbackComponent():Type | null{ + return null; + } + + protected getComponentMappingProvider():ComponentMappingProvider { + return ComponentMapping; + } /** * Returns the type of the cqItem if exists. */ - get type(): string | undefined { + protected getType(): string | undefined { return this.cqItem && this.cqItem[Constants.TYPE_PROP]; } @@ -146,7 +166,7 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, * * @param componentDefinition The component definition to render */ - private renderComponent(componentDefinition: Type) { + private renderComponent(componentDefinition: Type) { if (componentDefinition) { const factory = this.factoryResolver.resolveComponentFactory(componentDefinition); @@ -189,7 +209,7 @@ export class AEMComponentDirective implements AfterViewInit, OnInit, OnDestroy, this._component.instance.cqPath = this.cqPath; this._component.instance.itemName = this.itemName; - const editConfig = ComponentMapping.getEditConfig(this.type); + const editConfig = ComponentMapping.getEditConfig(this.getType()); if (editConfig && Utils.isInEditor) { this.setupPlaceholder(editConfig); diff --git a/src/lib/layout/component-mapping.ts b/src/lib/layout/component-mapping.ts index ceae114..58cc20a 100644 --- a/src/lib/layout/component-mapping.ts +++ b/src/lib/layout/component-mapping.ts @@ -64,12 +64,16 @@ export abstract class AbstractMappedComponent implements MappedComponentProperti @Input() itemName = ''; } +export interface ComponentMappingProvider { + get(resourceType:string):Type + lazyGet(resourceType: string): Promise> +} /** * The current class extends the @adobe/cq-spa-component-mapping#Mapto library and features with Angular specifics such as * * - Storing the editing configurations for each resource type */ -export class ComponentMappingWithConfig { +export class ComponentMappingWithConfig implements ComponentMappingProvider{ /** * Store of EditConfig structures */ @@ -172,4 +176,4 @@ function LazyMapTo(resourceTypes: componentMapping.lazyMap(resourceTypes, lazyClassFunction, editConfig); } -export { componentMapping as ComponentMapping, MapTo, LazyMapTo }; \ No newline at end of file +export { componentMapping as ComponentMapping, MapTo, LazyMapTo }; From d5773ca7a885142e9a7c965ae25cef427873bc4b Mon Sep 17 00:00:00 2001 From: Niek Raaijmakers Date: Tue, 15 Jun 2021 14:21:58 +0200 Subject: [PATCH 2/3] bump --- karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/karma.conf.js b/karma.conf.js index 0153bda..942d55c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -29,6 +29,7 @@ const customLaunchers = { } }; + module.exports = function(config) { config.set({ basePath: '', From dddf57ace18879bdfe45a8ece598c2fc64a0d9bb Mon Sep 17 00:00:00 2001 From: Niek Raaijmakers Date: Tue, 15 Jun 2021 16:03:21 +0200 Subject: [PATCH 3/3] Improve unit test coverage --- .../aem-component.directive.custom.spec.ts | 120 ++++++++++++++++++ .../layout/aem-component.directive.spec.ts | 30 +++++ .../custom-default-renderer.ts | 14 ++ .../customdirective.ts | 20 +++ 4 files changed, 184 insertions(+) create mode 100644 src/lib/layout/aem-component.directive.custom.spec.ts create mode 100644 src/lib/test/aem-component-directive/custom-default-renderer.ts create mode 100644 src/lib/test/aem-component-directive/customdirective.ts diff --git a/src/lib/layout/aem-component.directive.custom.spec.ts b/src/lib/layout/aem-component.directive.custom.spec.ts new file mode 100644 index 0000000..39edc11 --- /dev/null +++ b/src/lib/layout/aem-component.directive.custom.spec.ts @@ -0,0 +1,120 @@ +/* + * 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 { 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, LazyMapTo, AbstractMappedComponent } from './component-mapping'; +import { Utils } from './utils'; +import { LazyComponentType } from "../test/lazy-component-wrapper/lazy.component"; +import {CustomDefaultRenderer} from "../test/aem-component-directive/custom-default-renderer"; +import {CustomDirective} from "../test/aem-component-directive/customdirective"; + + + + + +@Component({ + selector: 'test-component', + template: `` +}) +class AEMDirectiveTestComponent { + @Input() data; +} + + + +@Component({ + selector: 'directive-component', + host: { + '[attr.attr1]': 'attr1', + '[attr.attr2]': 'attr2', + '[class]': 'hostClasses' + }, + template: `
` +}) +class DirectiveComponent extends AbstractMappedComponent { + @Input() attr1; + @Input() attr2; + + get hostClasses() { + return 'component-class'; + } +} +MapTo('directive/comp')(DirectiveComponent); +LazyMapTo('some/lazy/comp')(() => import('../test/lazy-component-wrapper/lazy.component').then((m) => m.LazyComponent)); + +describe('AEMComponentDirective - Custom', () => { + + const EDIT_CONFIG_EMPTY_LABEL = 'Edit config empty label'; + + const TEST_EDIT_CONFIG_EMPTY = { + emptyLabel: EDIT_CONFIG_EMPTY_LABEL, + isEmpty: () => { + return true; + } + }; + + const TEST_EDIT_CONFIG_NOT_EMPTY = { + emptyLabel: EDIT_CONFIG_EMPTY_LABEL, + isEmpty: function() { + return false; + } + }; + + let component: AEMDirectiveTestComponent; + let fixture: ComponentFixture; + + let isInEditorSpy; + let getEditConfigSpy; + + beforeEach(async(() => { + isInEditorSpy = spyOn(Utils, 'isInEditor').and.returnValue(false); + getEditConfigSpy = spyOn(ComponentMapping, 'getEditConfig').and.returnValue(undefined); + + TestBed.configureTestingModule({ + declarations: [ AEMDirectiveTestComponent, DirectiveComponent,CustomDefaultRenderer,CustomDirective, AEMComponentDirective ] + }).overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [ DirectiveComponent, CustomDefaultRenderer,CustomDirective ] + } + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AEMDirectiveTestComponent); + component = fixture.componentInstance; + }); + + it('should render the fallback component', async () => { + const componentData = { + attr1: 'Some value', + attr2: 'Another value', + ':customType': 'missing/directive/comp' + }; + + component.data = componentData; + fixture.detectChanges(); + + await fixture.whenStable(); + + const element = fixture.nativeElement; + const dynamicElement = element.firstElementChild; + + expect(dynamicElement.innerHTML).toEqual("
test
") + }); + + + +}); diff --git a/src/lib/layout/aem-component.directive.spec.ts b/src/lib/layout/aem-component.directive.spec.ts index 44bfd8d..953da26 100644 --- a/src/lib/layout/aem-component.directive.spec.ts +++ b/src/lib/layout/aem-component.directive.spec.ts @@ -106,6 +106,36 @@ describe('AEMComponentDirective', () => { expect(dynamicElement.getAttribute('attr1')).toEqual(componentData['attr1']); expect(dynamicElement.getAttribute('attr2')).toEqual(componentData['attr2']); }); + it('should not render anything', () => { + const componentData = { + attr1: 'Some value', + attr2: 'Another value', + ':type': 'missing/directive/comp' + }; + + component.data = componentData; + fixture.detectChanges(); + + const element = fixture.nativeElement; + const dynamicElement = element.firstElementChild; + + expect(dynamicElement).toBeNull(); + }); + it('should render the fallback component', () => { + const componentData = { + attr1: 'Some value', + attr2: 'Another value', + ':type': 'missing/directive/comp' + }; + + component.data = componentData; + fixture.detectChanges(); + + const element = fixture.nativeElement; + const dynamicElement = element.firstElementChild; + + expect(dynamicElement).toBeNull(); + }); it('should correctly pass the inputs for lazy component', async() => { const componentData = { some: 'Some value', diff --git a/src/lib/test/aem-component-directive/custom-default-renderer.ts b/src/lib/test/aem-component-directive/custom-default-renderer.ts new file mode 100644 index 0000000..8774e4e --- /dev/null +++ b/src/lib/test/aem-component-directive/custom-default-renderer.ts @@ -0,0 +1,14 @@ +import {Component, OnInit} from "@angular/core"; + +@Component({ + selector: 'core-contentfragment-default-renderer-v1', + template: '
test
' +}) +export class CustomDefaultRenderer implements OnInit{ + ngOnInit(): void { + console.log('init?') + } + + + +} diff --git a/src/lib/test/aem-component-directive/customdirective.ts b/src/lib/test/aem-component-directive/customdirective.ts new file mode 100644 index 0000000..8018c3c --- /dev/null +++ b/src/lib/test/aem-component-directive/customdirective.ts @@ -0,0 +1,20 @@ +import {AEMComponentDirective} from "../../layout/aem-component.directive"; +import {Directive, Type} from "@angular/core"; +import {ComponentMappingProvider} from "../../layout/component-mapping"; +import {CustomDefaultRenderer} from "./custom-default-renderer"; +import {ComponentMapping} from "@adobe/aem-spa-component-mapping"; + +@Directive({ + selector: '[CustomDirective]' +}) +export class CustomDirective extends AEMComponentDirective { + + getFallbackComponent():Type | null{ + return CustomDefaultRenderer; + } + + getType(): string | undefined { + return this.cqItem && this.cqItem[":customType"]; + } + +}