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) }}">
</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],
})
export class AppRoutingModule {}
AppModule
We need to add all the new components to our AppModule.
Typescript
app.module.ts
import { NgModule } from '@angular/core';
import '@angular/compiler';
import { AppRoutingModule } from './routing/app-routing.module';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgxPaginationModule } from 'ngx-pagination';
import { CommonModule } from '@angular/common';
import { RootComponent } from './components/root/app-root.component';
import { LoginComponent } from './components/login/login.component';
import { TasksComponent } from './components/tasks/app-tasks.component';
import { NavigationComponent } from './components/nav/navigation.component';
import { FormComponent } from './components/form/app-form.component';
import { ApiModule } from 'api';
import { FormElementComponent } from './components/form-element/app-form-element.component';
import { ScreenComponent } from './components/screen/app-screen.component';
import { MultiColumnComponent } from './components/form-element/elements/muti-column/app-multi-column.component';
import { InputComponent } from './components/form-element/elements/input/app-element-input.component';
import { AppElementDefaultComponent } from './components/form-element/elements/default/app-element-default.component';
import { ButtonComponent } from './components/form-element/elements/buttons/app-element-button.component';
import { AppBreadcrumbsComponent } from './components/breadcrumbs/app-breadcrumbs.component';
import { AppElementHtmlViewerComponent } from './components/form-element/elements/html-viewer/app-element-html-viewer.component';
import { AppElementImageComponent } from './components/form-element/elements/image/app-element-image.component';
import { AppElementUploadFileComponent } from './components/form-element/elements/upload-file/app-element-upload-file.component';
@NgModule({
declarations: [
RootComponent,
LoginComponent,
TasksComponent,
NavigationComponent,
FormComponent,
FormElementComponent,
ScreenComponent,
MultiColumnComponent,
InputComponent,
ButtonComponent,
AppBreadcrumbsComponent,
AppElementDefaultComponent,
AppElementHtmlViewerComponent,
AppElementImageComponent,
AppElementUploadFileComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
NgxPaginationModule,
FormsModule,
ReactiveFormsModule,
CommonModule,
ApiModule,
],
providers: [],
bootstrap: [RootComponent],
})
export class AppModule {}
Review
After your code finishes compiling, click on the "Next Task" button in the inbox (assuming you have open cases) and you should see your form, just as I have posted a screenshot below.
We recommend playing around with simple forms first, and then adding complexity as you get more comfortable.
If you liked this or found it helpful, please let us know by leaving a rating and sharing on social!
If not, please let us know!
PRs are always welcomed!
Last updated