diff --git a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts new file mode 100644 index 0000000000..5636f095b6 --- /dev/null +++ b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts @@ -0,0 +1,155 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* tslint:disable:component-selector */ + +import { + Component, + Input, + SimpleChanges, + OnChanges, + SimpleChange, + ComponentFactoryResolver +} from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { DynamicExtensionComponent } from './dynamic.component'; +import { ComponentRegisterService } from '../../services/component-register.service'; +import { HttpClientModule } from '@angular/common/http'; + +@Component({ + selector: 'test-component', + template: '
Hey I am the mighty test component!
' +}) +export class TestComponent implements OnChanges { + @Input() data: any; + public onChangesCalled = 0; + ngOnChanges(changes: SimpleChanges) { this.onChangesCalled ++; } +} + +describe('DynamicExtensionComponent', () => { + + let fixture: ComponentFixture; + let componentRegister: ComponentRegisterService; + let component: DynamicExtensionComponent; + let componentFactoryResolver: ComponentFactoryResolver; + + beforeEach(async(() => { + componentRegister = new ComponentRegisterService(); + componentRegister.setComponents({'test-component': TestComponent}); + + TestBed.configureTestingModule({ + imports: [ HttpClientModule ], + declarations: [ DynamicExtensionComponent, TestComponent ], + providers: [ { provide: ComponentRegisterService, useValue: componentRegister } ] + }); + + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { entryComponents: [ TestComponent ] } + }); + + TestBed.compileComponents(); + })); + + describe('Sub-component creation', () => { + + beforeEach(() => { + fixture = TestBed.createComponent(DynamicExtensionComponent); + componentFactoryResolver = TestBed.get(ComponentFactoryResolver); + spyOn(componentFactoryResolver, 'resolveComponentFactory').and.callThrough(); + component = fixture.componentInstance; + component.id = 'test-component'; + component.data = { foo: 'bar' }; + + fixture.detectChanges(); + component.ngOnChanges({}); + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + it('should load the TestComponent', () => { + const innerElement = fixture.debugElement.query(By.css('[data-automation-id="found-me"]')); + expect(innerElement).not.toBeNull(); + }); + + it('should load the TestComponent only ONCE', () => { + component.ngOnChanges({}); + fixture.detectChanges(); + component.ngOnChanges({}); + fixture.detectChanges(); + + expect(( componentFactoryResolver.resolveComponentFactory).calls.count()).toBe(1); + }); + + it('should pass through the data', () => { + const testComponent = fixture.debugElement.query(By.css('test-component')).componentInstance; + + expect(testComponent.data).toBe(component.data); + }); + + it('should update the subcomponent\'s input parameters', () => { + const data = { foo: 'baz' }; + + component.ngOnChanges({ data: new SimpleChange(component.data, data, false) }); + + const testComponent = fixture.debugElement.query(By.css('test-component')).componentInstance; + expect(testComponent.data).toBe(data); + }); + }); + + describe('Angular life-cycle methods in sub-component', () => { + + let testComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(DynamicExtensionComponent); + component = fixture.componentInstance; + component.id = 'test-component'; + + fixture.detectChanges(); + component.ngOnChanges({}); + testComponent = fixture.debugElement.query(By.css('test-component')).componentInstance; + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + it('should call through the ngOnChanges', () => { + const params = {}; + + component.ngOnChanges(params); + + expect(testComponent.onChangesCalled).toBe(2); + }); + + it('should NOT call through the ngOnChanges if the method does not exist (no error should be thrown)', () => { + testComponent.ngOnChanges = undefined; + const params = {}; + const execution = () => { + component.ngOnChanges(params); + }; + + expect(execution).not.toThrowError(); + }); + }); +}); diff --git a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts index 4dd00a488b..ef7afd3c84 100644 --- a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts +++ b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts @@ -19,33 +19,49 @@ import { Component, Input, ComponentRef, - OnInit, ComponentFactoryResolver, ViewChild, ViewContainerRef, - OnDestroy + OnDestroy, + OnChanges, + SimpleChanges } from '@angular/core'; import { ExtensionService } from '../../services/extension.service'; +import { ExtensionComponent } from '../../services/component-register.service'; +// cSpell:words lifecycle @Component({ selector: 'adf-dynamic-component', template: `
` }) -export class DynamicExtensionComponent implements OnInit, OnDestroy { +export class DynamicExtensionComponent implements OnChanges, OnDestroy { @ViewChild('content', { read: ViewContainerRef }) content: ViewContainerRef; @Input() id: string; + @Input() data: any; - private componentRef: ComponentRef; + private componentRef: ComponentRef; + private loaded: boolean = false; - constructor( - private extensions: ExtensionService, - private componentFactoryResolver: ComponentFactoryResolver - ) {} + constructor(private extensions: ExtensionService, private componentFactoryResolver: ComponentFactoryResolver) {} - ngOnInit() { - const componentType = this.extensions.getComponentById(this.id); + ngOnChanges(changes: SimpleChanges) { + if (!this.loaded) { + this.loadComponent(); + this.loaded = true; + } + + if (changes.data) { + this.data = changes.data.currentValue; + } + + this.updateInstance(); + this.proxy('ngOnChanges', changes); + } + + private loadComponent() { + const componentType = this.extensions.getComponentById(this.id); if (componentType) { const factory = this.componentFactoryResolver.resolveComponentFactory( componentType @@ -53,15 +69,34 @@ export class DynamicExtensionComponent implements OnInit, OnDestroy { if (factory) { this.content.clear(); this.componentRef = this.content.createComponent(factory, 0); - // this.setupWidget(this.componentRef); } } } ngOnDestroy() { - if (this.componentRef) { + if (this.componentCreated()) { this.componentRef.destroy(); this.componentRef = null; } } + + private updateInstance() { + if (this.componentCreated()) { + this.componentRef.instance.data = this.data; + } + } + + private proxy(lifecycleMethod, ...args) { + if (this.componentCreated() && this.lifecycleHookIsImplemented(lifecycleMethod)) { + this.componentRef.instance[lifecycleMethod].apply(this.componentRef.instance, args); + } + } + + private componentCreated(): boolean { + return !!this.componentRef && !!this.componentRef.instance; + } + + private lifecycleHookIsImplemented(lifecycleMethod: string): boolean { + return !!this.componentRef.instance[lifecycleMethod]; + } } diff --git a/lib/extensions/src/lib/services/component-register.service.ts b/lib/extensions/src/lib/services/component-register.service.ts new file mode 100644 index 0000000000..54a5079b34 --- /dev/null +++ b/lib/extensions/src/lib/services/component-register.service.ts @@ -0,0 +1,41 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable, Type } from '@angular/core'; + +export interface ExtensionComponent { + data: any; +} + +@Injectable({ providedIn: 'root' }) +export class ComponentRegisterService { + components: { [key: string]: Type<{}> } = {}; + + setComponents(values: { [key: string]: Type<{}> }) { + if (values) { + this.components = Object.assign({}, this.components, values); + } + } + + getComponentById(id: string): Type { + return > this.components[id]; + } + + hasComponentById(id: string): boolean { + return !!this.getComponentById(id); + } +} diff --git a/lib/extensions/src/lib/services/extension.service.spec.ts b/lib/extensions/src/lib/services/extension.service.spec.ts index 79f495d9fe..ba39f94c96 100644 --- a/lib/extensions/src/lib/services/extension.service.spec.ts +++ b/lib/extensions/src/lib/services/extension.service.spec.ts @@ -21,6 +21,7 @@ import { ExtensionConfig } from '../config/extension.config'; import { RuleRef } from '../config/rule.extensions'; import { RouteRef } from '../config/routing.extensions'; import { ActionRef } from '../config/action.extensions'; +import { ComponentRegisterService } from './component-register.service'; describe('ExtensionService', () => { const blankConfig: ExtensionConfig = { @@ -33,11 +34,13 @@ describe('ExtensionService', () => { }; let loader: ExtensionLoaderService; + let componentRegister: ComponentRegisterService; let service: ExtensionService; beforeEach(() => { loader = new ExtensionLoaderService(null); - service = new ExtensionService(loader); + componentRegister = new ComponentRegisterService(); + service = new ExtensionService(loader, componentRegister); }); it('should load and setup a config', async () => { diff --git a/lib/extensions/src/lib/services/extension.service.ts b/lib/extensions/src/lib/services/extension.service.ts index d968a9c9c0..58e86f2954 100644 --- a/lib/extensions/src/lib/services/extension.service.ts +++ b/lib/extensions/src/lib/services/extension.service.ts @@ -22,6 +22,7 @@ import { ExtensionLoaderService } from './extension-loader.service'; import { RouteRef } from '../config/routing.extensions'; import { ActionRef } from '../config/action.extensions'; import * as core from '../evaluators/core.evaluators'; +import { ComponentRegisterService } from './component-register.service'; @Injectable({ providedIn: 'root' @@ -35,10 +36,12 @@ export class ExtensionService { actions: Array = []; authGuards: { [key: string]: Type<{}> } = {}; - components: { [key: string]: Type<{}> } = {}; evaluators: { [key: string]: RuleEvaluator } = {}; - constructor(private loader: ExtensionLoaderService) {} + constructor( + private loader: ExtensionLoaderService, + private componentRegister: ComponentRegisterService + ) {} async load(): Promise { const config = await this.loader.load( @@ -79,9 +82,7 @@ export class ExtensionService { } setComponents(values: { [key: string]: Type<{}> }) { - if (values) { - this.components = Object.assign({}, this.components, values); - } + this.componentRegister.setComponents(values); } getRouteById(id: string): RouteRef { @@ -125,8 +126,8 @@ export class ExtensionService { return false; } - getComponentById(id: string): Type<{}> { - return this.components[id]; + getComponentById(id: string) { + return this.componentRegister.getComponentById(id); } getRuleById(id: string): RuleRef { diff --git a/lib/extensions/src/public_api.ts b/lib/extensions/src/public_api.ts index 19998280ad..828094cd8e 100644 --- a/lib/extensions/src/public_api.ts +++ b/lib/extensions/src/public_api.ts @@ -28,6 +28,7 @@ export * from './lib/config/viewer.extensions'; export * from './lib/services/extension-loader.service'; export * from './lib/services/extension.service'; +export * from './lib/services/component-register.service'; export * from './lib/store/states/navigation.state'; export * from './lib/store/states/profile.state';