Skip to content

Commit

Permalink
AAE-29973 Dynamic component properties (#10540)
Browse files Browse the repository at this point in the history
* [AAE-29973] updated dynamic component inputs and outputs

* [AAE-29973] added check to prevent errors

* [AAE-29973] added unit tests for input and output

* [AAE-29973] fix for dynamic component with absolute position
  • Loading branch information
tomaszhanaj authored Jan 17, 2025
1 parent ada55ae commit 4fb0fdd
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<mat-card>
<mat-card-content>
<mat-card-content [ngStyle]="{'position':'relative'}">
<div #container></div>
</mat-card-content>
<mat-card-actions align="end">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,46 @@

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ScreenRenderingService } from '../../../services/public-api';
import { TaskScreenCloudComponent } from './screen-cloud.component';

@Component({
selector: 'adf-cloud-test-component',
template: `<div class="adf-cloud-test-container">test component</div>`,
template: `
<div class="adf-cloud-test-container">
test component
<div class="adf-cloud-test-container-taskId">{{ taskId }}</div>
<button class="adf-cloud-test-container-complete-btn" (click)="onComplete()">complete</button>
</div>
`,
imports: [CommonModule],
standalone: true
})
class TestComponent {}
class TestComponent {
@Input() taskId = '';
@Output() taskCompleted = new EventEmitter();
onComplete() {
this.taskCompleted.emit();
}
}

@Component({
selector: 'adf-cloud-test-actions-component',
template: `<adf-cloud-task-screen [taskId]="'1'" [appName]="'app-name-test'" [screenId]="'test'">
<div buttons class="adf-cloud-test-buttons">
<button>Test</button>
</div>
</adf-cloud-task-screen> `,
template: `
<adf-cloud-task-screen [taskId]="'1'" [appName]="'app-name-test'" [screenId]="'test'" (taskCompleted)="onTaskCompleted()">
<div buttons class="adf-cloud-test-buttons">
<button>Test</button>
</div>
</adf-cloud-task-screen>
`,
imports: [CommonModule, TaskScreenCloudComponent],
standalone: true
})
class TestWrapperComponent {}
class TestWrapperComponent {
onTaskCompleted() {}
}

describe('TaskScreenCloudComponent', () => {
let fixture: ComponentFixture<TestWrapperComponent>;
Expand All @@ -66,4 +82,19 @@ describe('TaskScreenCloudComponent', () => {
const projectedContent = fixture.debugElement.query(By.css('.adf-cloud-test-buttons'));
expect(projectedContent).toBeTruthy();
});

it('should set input property for dynamic component', () => {
const inputValueFromDynamicComponent = fixture.debugElement.query(By.css('.adf-cloud-test-container-taskId'));
expect((inputValueFromDynamicComponent.nativeElement as HTMLElement).textContent).toBe('1');
});

it('should subscribe to the output of dynamic component', () => {
const onTaskCompletedSpy = spyOn(fixture.componentInstance, 'onTaskCompleted');
const btnComplete = fixture.debugElement.query(By.css('.adf-cloud-test-container-complete-btn'));

expect(btnComplete).toBeDefined();

(btnComplete.nativeElement as HTMLButtonElement).click();
expect(onTaskCompletedSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
*/

import { CommonModule } from '@angular/common';
import { Component, ComponentRef, inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { Component, ComponentRef, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
import { ScreenRenderingService } from '../../../services/public-api';
import { MatCardModule } from '@angular/material/card';
import { UserTaskCustomUi } from './screen-cloud.interface';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
selector: 'adf-cloud-task-screen',
Expand All @@ -35,29 +37,74 @@ export class TaskScreenCloudComponent implements OnInit {
/** Screen id to fetch corresponding screen widget. */
@Input()
screenId: string = '';
@Input()
processInstanceId: string = '';
@Input()
taskName: string = '';
/** Toggle readonly state of the task. */
@Input()
readOnly = false;

/** Emitted when the task is saved. */
@Output()
taskSaved = new EventEmitter();

/** Emitted when the task is completed. */
@Output()
taskCompleted = new EventEmitter();

/** Emitted when there is an error. */
@Output()
error = new EventEmitter<any>();

@ViewChild('container', { read: ViewContainerRef, static: true })
container: ViewContainerRef;
componentRef: ComponentRef<any>;

private destroyRef = inject(DestroyRef);
componentRef: ComponentRef<UserTaskCustomUi>;

private readonly screenRenderingService = inject(ScreenRenderingService);

ngOnInit() {
this.createDynamicComponent();
}

createDynamicComponent() {
if (this.screenId) {
const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId });
this.componentRef = this.container.createComponent(componentType);
if (this.taskId) {
this.componentRef.setInput('taskId', this.taskId);
}
if (this.appName) {
this.componentRef.setInput('appName', this.appName);
}
if (this.screenId) {
this.componentRef.setInput('screenId', this.screenId);
}
this.setInputsForDynamicComponent();
this.subscribeToOutputs();
}
}

setInputsForDynamicComponent(): void {
if (this.taskId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskId')) {
this.componentRef.setInput('taskId', this.taskId);
}
if (this.appName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'appName')) {
this.componentRef.setInput('appName', this.appName);
}
if (this.screenId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'screenId')) {
this.componentRef.setInput('screenId', this.screenId);
}
if (this.processInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'processInstanceId')) {
this.componentRef.setInput('processInstanceId', this.processInstanceId);
}
if (this.taskName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskName')) {
this.componentRef.setInput('taskName', this.taskName);
}
}

subscribeToOutputs() {
if (this.componentRef.instance?.taskSaved) {
this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit());
}
if (this.componentRef.instance?.taskCompleted) {
this.componentRef.instance.taskCompleted.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskCompleted.emit());
}
if (this.componentRef.instance?.error) {
this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* 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 { EventEmitter } from '@angular/core';

export interface UserTaskCustomUi {
processInstanceId: string;
taskName: string;
appName: string;
taskId: string;
screenId: string;
taskCompleted: EventEmitter<string>;
taskSaved: EventEmitter<string>;
error: EventEmitter<any>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
[appName]="appName"
[screenId]="screenId"
[taskId]="taskId"
[taskName]="taskDetails.name"
[processInstanceId]="taskDetails.processInstanceId"
(error)="onError($event)"
(taskCompleted)="onCompleteTask()"
(taskSaved)="onFormSaved()"
>
<ng-template [ngTemplateOutlet]="taskFormCloudButtons" buttons>
</ng-template>
Expand Down

0 comments on commit 4fb0fdd

Please sign in to comment.