Links

Part 5: The Screen & Form Elements

Overview

Completed code: live example / download example

Step 1: ScreenComponent

The ScreenComponent serves as the primary component for our form interactions. While ProcessMaker permits several forms within a single Screen, each task should associate with one Screen that can encompass multiple forms. Any task should have a single Screen, but can have multiple forms. They will be contained within an array in the response payload when making the API request.
When we get the form part, we will use the FormComponent as well as others to ultimately render the form.
The ScreenComponent is passed the required data that will be passed down through to the individual elements on the form.

Template

app-screen.component.html
<!-- Use app-form-element to render form elements -->
<app-form
*ngFor="let screen of screens?.config"
[form]="screen"
[request]="request"
[css]="screens.custom_css"
[computed]="screens.computed">
</app-form>

Typescript

app-screen.component.ts
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { TasksService, ProcessRequestsService } from 'api';
import { DbService } from 'src/app/services/db.service';
@Component({
selector: 'app-screen',
templateUrl: './app-screen.component.html',
})
export class ScreenComponent implements OnInit {
// Define the request object, type can be further specified
request: any;
// Define properties with appropriate types
exists: any; // Define the type as per your requirements
processRequestId: number | null = null; // Define the type as number or null
taskId: number | null = null; // Define the type as number or null
@Output() screens: any; // Define the type as per your requirements
data: any; // Define the type as per your requirements
response: any; // Define the type as per your requirements
screenConfig: any;
@Output() screenEvent: EventEmitter<any> = new EventEmitter(); // Define the type as per your requirements
constructor(
private route: ActivatedRoute,
private router: Router,
public requestApi: ProcessRequestsService,
public tasksApi: TasksService,
private db: DbService
) {}
ngOnInit(): void {
console.log(this);
// Parse the values as numbers, and handle the possibility of null values
// Convert processRequestId and taskId from string to number, handle null values
this.processRequestId =
Number(this.route.snapshot.paramMap.get('processRequestId')) || null;
this.taskId = Number(this.route.snapshot.paramMap.get('taskId')) || null;
// Check if taskId is not null before proceeding
if (this.taskId !== null) {
// Set the credentials for the tasks API
this.tasksApi.configuration.credentials['pm_api_bearer'] =
this.db.load('access_token') || '';
// Fetch the task by ID and include specific related data
this.tasksApi
.getTasksById(
this.taskId,
'processRequest,user,data,screen,definition,screenRef'
)
.subscribe(
(response) => {
// Assign the response to the request object
this.screens = response.screen;
this.request = response.data;
//console.log(this);
},
(error) => {
// Log any errors
}
);
}
}
handleScreenEvent(event: any) {
// Handle any screen-related event here
this.screenEvent.emit(event);
}
}

Step 2: FormComponent

Given that a Screen can associate with multiple forms, our design ensures minimal code adjustments to accommodate this.
The FormComponent passes through to the form element the request data and definitions for the different elements.

Template

app-form.component.html
<div class="container-fluid">
<div class="table-container mt-4">
<!-- create an html stylesheet block -->
<style>
{{ css }}
</style>
<form (ngSubmit)="submitForm()">
<!-- Use app-form-element to render form elements -->
<h4>{{ form.name }}</h4>
<div class="container">
<ng-container *ngFor="let element of form.items">
<app-form-element
[element]="element"
[request]="request"
[calcPropsValues]="calcPropsValues"></app-form-element>
</ng-container>
</div>
</form>
</div>
</div>

Typescript

app-form.component.html
import { Component, Input, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { TasksService, ProcessRequestsService } from 'api';
import { DbService } from 'src/app/services/db.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-form',
templateUrl: './app-form.component.html',
})
export class FormComponent implements OnInit {
// Define the request object, type can be further specified
@Input() request: any;
// Define properties with appropriate types
exists: any; // Define the type as per your requirements
processRequestId: number | null = null; // Define the type as number or null
taskId: number | null = null; // Define the type as number or null
@Input() screens: any; // Define the type as per your requirements
@Input() data: any; // Define the type as per your requirements
@Input() response: any; // Define the type as per your requirements
@Input() element: any; // Define the type as per your requirements
isMultiColumn: boolean = false;
@Input() cols: any;
@Input() form: any;
@Input() css: any;
@Input() computed: any;
calcPropsValues: any = {};
// Constructor with necessary dependencies
constructor(
private route: ActivatedRoute,
private router: Router,
public requestApi: ProcessRequestsService,
public tasksApi: TasksService,
private db: DbService
) {}
ngOnInit() {
//console.log(this);
this.calcPropsValues = this.executeJavascripts(this.computed);
this.processRequestId =
Number(this.route.snapshot.paramMap.get('processRequestId')) || null;
this.taskId = Number(this.route.snapshot.paramMap.get('taskId')) || null;
if (environment.customCss === true) {
const styleEl = document.createElement('style');
styleEl.innerHTML = this.css;
document.head.appendChild(styleEl);
}
// add any custom css
// if (this.css.length > 0) {
// const sanitizedCSS = this.sanitizeCSS(this.css);
// if (sanitizedCSS) {
// // Inject CSS into the page
// const styleEl = document.createElement('style');
// styleEl.innerHTML = sanitizedCSS;
// document.head.appendChild(styleEl);
// } else {
// console.warn('CSS was sanitized out, nothing was injected.');
// }
// }
}
executeJavascripts(computed: any[]): { [key: string]: any } {
const result: { [key: string]: any } = {};
computed.forEach((computed) => {
if (computed.type === 'javascript') {
try {
const fn = new Function(computed.formula);
result[computed.property] = fn();
} catch (e) {
console.error(e);
}
}
});
return result;
}
/**
* Given a raw CSS string, sanitize it by stripping out
* non-conforming characters and properties.
*
* This approach uses a whitelist methodology, allowing only
* specific properties, values, and characters.
*
* @param {string} css Raw CSS string
* @returns {string} Sanitized CSS string
*/
sanitizeCSS(css: string) {
// 1. Strip out any comments
let cleanedCSS = css.replace(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g, '');
// 2. Split by braces to extract selectors and properties
const chunks = cleanedCSS.split('}');
cleanedCSS = chunks
.map((chunk) => {
const [selector, properties] = chunk.split('{');
// 2.1. Sanitize the selector
const sanitizedSelector = selector
.replace(/[^\w\s\.,\[\]='-]+/g, '')
.trim();
// 2.2. Sanitize the properties
let sanitizedProperties = '';
if (properties) {
const propList = properties.split(';');
propList.forEach((prop) => {
const [property, value] = prop.split(':').map((p) => p.trim());
sanitizedProperties += `${property}: ${value}; `;
});
}
return sanitizedSelector
? `${sanitizedSelector} { ${sanitizedProperties}}`
: '';
})
.join(' ');
return cleanedCSS;
}
// Function to handle form submission
submitForm() {
// Load access token from the database
const accessToken = this.db.load('access_token') as string | undefined;
// If access token exists, set it in the task API configuration
if (accessToken)
this.requestApi.configuration.credentials['pm_api_bearer'] = accessToken;
let payLoad = {
data: this.request,
status: 'COMPLETED',
};
// Call getTasks method from tasksApi with parameters null and 'ACTIVE'
this.tasksApi.updateTask(Number(this.taskId), payLoad).subscribe(
(response: any) => {
// Handle successful response
console.log(response); // Log the response to the console
this.router.navigate(['tasks']);
},
(error) => {
// Handle error response
console.log(error); // Log the error to the console
}
);
//console.log(this);
}
}
interface Javascript {
id: number;
name: string;
type: string;
formula: string;
property: string;
}

Step 3: FormElementComponent

Our application design emphasizes easy extensibility and customization. As of the time of this writing, the form elements are supported and shipped with the app. However, as you will see, it is relatively easy to add more elements.

Form Elements

The FormElementComponent is utilized to render varied components, managing logic via ngSwitch.
The main FormElementComponent class should be placed at src/app/components/form-element/app-form-element.component.ts and src/app/components/form-element/app-form-element.component.html, respectively.
The various form elements are contained with src/app/components/form-element/elements.
Example: src/app/components/form-element/elements/app-element-input.component.ts

Input

Template

app-element-input.component.html
<div class="input">
<label>{{ element?.config?.label }}</label>
<input
type="text"
class="form-control"
[readonly]="element?.config?.readonly"
[name]="element?.config?.name"
[(ngModel)]="request[element?.config?.name]" />
</div>

Typescript

app-element-input.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-element-input',
templateUrl: './app-element-input.component.html',
})
export class InputComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() data: any;
@Input() calcPropsValues: any;
constructor() {}
ngOnInit(): void {
//console.log(this);
}
}
interface Option {
value: string;
content: string;
}

Image

Template

app-element-image.component.html
<ng-container *ngIf="element.config.image">
<img [src]="safeUrl(element.config.image)" />
</ng-container>

Typescript

app-element-image.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Component({
selector: 'app-element-image',
templateUrl: './app-element-image.component.html',
})
export class AppElementImageComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() data: any;
@Input() calcPropsValues: any;
constructor(private sanitizer: DomSanitizer) {}
ngOnInit(): void {
//console.log(this);
}
safeUrl(url: string): SafeUrl {
// Sanitize the URL string
return this.sanitizer.bypassSecurityTrustUrl(url);
}
}
interface Option {
value: string;
content: string;
}

Multi Column

Template

app-multi-column.component.html
<div class="row">
<ng-container *ngFor="let item of element.items; let outerIndex = index">
<ng-container *ngIf="item.length == 0">
<div class="form-group col-{{ getColSize(element, outerIndex) }}">
&nbsp;
</div>
</ng-container>
<ng-container *ngFor="let control of item; let innerIndex = index">
<ng-container *ngIf="control">
<app-form-element
class="form-group col-{{ getColSize(element, outerIndex) }}"
[element]="control"
[request]="request"
[calcPropsValues]="calcPropsValues"></app-form-element>
<!-- </div> -->
</ng-container>
</ng-container>
</ng-container>
</div>
```

Typescript

app-multi-column.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-multi-column',
templateUrl: './app-multi-column.component.html',
})
export class MultiColumnComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() control: any;
@Input() cols: any;
columnSize: any;
shadow: any;
@Input() options: any;
@Input() calcPropsValues: any;
constructor(private cdRef: ChangeDetectorRef) {}
ngAfterViewChecked() {
this.cdRef.detectChanges();
}
isArrayOfArrays(): boolean {
if (!Array.isArray(this.element.items)) {
return false;
}
for (const item of this.element.items) {
if (!Array.isArray(item)) {
return false;
}
}
return true;
}
ngOnInit(): void {
console.log(this.element.items);
//console.log(this.isArrayOfArrays());
//this.element.push(this.computedClasses);
//console.log('computed classes: ', this.computedClasses);
}
getColSize(control: any, idx: number): number {
// Make sure you're getting the options from the correct element
let colSize = control.config?.options[idx]?.content || 12;
//console.log('colSize: ', control);
return colSize; // defaulting to 12 if no config found
}
}
interface Option {
value: string;
content: string;
}

Upload File

Template

app-element-upload-file.component.html
<div class="input">
<label>{{ element?.config?.label }}</label>
<input
type="file"
[readonly]="element?.config?.readonly"
[name]="element?.config?.name"
[(ngModel)]="request[element?.config?.name]" />
</div>

Typescript

app-element-upload-file.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-element-upload-file',
templateUrl: './app-element-upload-file.component.html',
})
export class AppElementUploadFileComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() data: any;
@Input() calcPropsValues: any;
constructor() {}
ngOnInit(): void {
//console.log(this);
}
}
interface Option {
value: string;
content: string;
}

Buttons

Template

app-element-button.component.html
<br />
<input
type="submit"
class="btn btn-primary w-100"
value="{{ element.config.label }}" />

Typescript

app-element-button.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-element-button',
templateUrl: './app-element-button.component.html',
})
export class ButtonComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() calcPropsValues: any;
constructor() {}
ngOnInit(): void {
//console.log(this);
}
}
interface Option {
value: string;
content: string;
}

HTML Viewer

Template

app-element-html-viewer.component.html
<ng-container *ngIf="element.config.calculatedContent">
<div [innerHTML]="element.config.calculatedContent"></div>
</ng-container>
<ng-container *ngIf="!element.config.calculatedContent">
<div [innerHTML]="element.config.content"></div>
</ng-container>

Typescript

app-element-html-viewer.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-element-html-viewer',
templateUrl: './app-element-html-viewer.component.html',
})
export class AppElementHtmlViewerComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() data: any;
@Input() calcPropsValues: any;
constructor() {}
ngOnInit(): void {
try {
this.element.config.calculatedContent = this.injectContent(
this.element.config.content,
this.calcPropsValues
);
} catch (e) {
console.error(e);
}
//console.log(this);
}
injectContent(htmlString: string, dynamicData: any): string {
return htmlString.replace(/\{\{(\w+)\}\}/g, (match, key) => {
// Return the dynamic data if the key exists, else return the original match
return dynamicData.hasOwnProperty(key) ? dynamicData[key] : match;
});
}
}
interface Option {
value: string;
content: string;
}

Default

Template

app-element-default.component.html
<ng-container>
Unsupported Component <strong>{{ element.component }}</strong>
</ng-container>

Typescript

app-element-default.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-element-default',
templateUrl: './app-element-default.component.html',
})
export class AppElementDefaultComponent implements OnInit {
computedClasses: any;
@Input() request: any;
@Input() element: any;
@Input() formElement: any;
@Input() control: any;
@Input() data: any;
@Input() calcPropsValues: any;
constructor() {}
ngOnInit(): void {
//console.log(this);
}
}
interface Option {
value: string;
content: string;
}

Step 4: Updates

Now that our app is almost complete, we just need to update the router so it knows where to go when we click on the "View Task" button.

The Router

app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from '../components/login/login.component';
import { TasksComponent } from '../components/tasks/app-tasks.component';
import { ScreenComponent } from '../components/screen/app-screen.component';
import { AuthGuard } from '../guards/auth.guard';
const routes: Routes = [
{
path: 'login',
component: LoginComponent,
runGuardsAndResolvers: 'always',
title: 'Login',
},
{
path: 'oauth/callback',
component: LoginComponent,
runGuardsAndResolvers: 'always',
},
{
path: 'tasks',
component: TasksComponent,
canActivate: [AuthGuard],
runGuardsAndResolvers: 'always',
title: 'Inbox',
},
{
path: 'screen',
component: ScreenComponent,
canActivate: [AuthGuard],
runGuardsAndResolvers: 'always',
title: 'Screen',
data: { title: 'Screen' },
},
{
path: '',
redirectTo: 'tasks',
pathMatch: 'full',
runGuardsAndResolvers: 'always',
},
{
path: '**',
redirectTo: 'tasks',
runGuardsAndResolvers: 'always',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
})