# Part 5: The Screen & Form Elements

## Overview

Completed code: [live example](https://stackblitz.com/edit/stackblitz-starters-krrqnt) / [download example](https://github.com/ProcessMaker/angular4pm/tree/part-5/src)

## 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

<details>

<summary><code>app-screen.component.html</code></summary>

{% code overflow="wrap" %}

```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>
```

{% endcode %}

</details>

### Typescript

<details>

<summary><code>app-screen.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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);
	}
}

```

{% endcode %}

</details>

## 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

<details>

<summary><code>app-form.component.html</code></summary>

{% code overflow="wrap" %}

```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>
```

{% endcode %}

</details>

### Typescript

<details>

<summary><code>app-form.component.html</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

## 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

* [Input](#input)
* [Image](#image)
* [Multi Column](#multi-column)
* [Upload File](#upload-file)
* [Buttons](#buttons)
* [HTML Viewer](#html-viewer)
* [Default](#default)

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

<details>

<summary><code>app-element-input.component.html</code></summary>

{% code overflow="wrap" %}

```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>
```

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-input.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### Image

#### Template

<details>

<summary><code>app-element-image.component.html</code></summary>

{% code overflow="wrap" %}

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

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-image.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### Multi Column

#### Template

<details>

<summary><code>app-multi-column.component.html</code></summary>

{% code overflow="wrap" %}

````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>

```
````

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-multi-column.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### Upload File

#### Template

<details>

<summary><code>app-element-upload-file.component.html</code></summary>

{% code overflow="wrap" %}

```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>
```

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-upload-file.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### Buttons

#### Template

<details>

<summary><code>app-element-button.component.html</code></summary>

{% code overflow="wrap" %}

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

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-button.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### HTML Viewer

#### Template

<details>

<summary><code>app-element-html-viewer.component.html</code></summary>

{% code overflow="wrap" %}

```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>
```

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-html-viewer.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

### Default

#### Template

<details>

<summary><code>app-element-default.component.html</code></summary>

{% code overflow="wrap" %}

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

{% endcode %}

</details>

#### Typescript

<details>

<summary><code>app-element-default.component.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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;
}
```

{% endcode %}

</details>

## 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

<details>

<summary><code>app-routing.module.ts</code></summary>

{% code overflow="wrap" %}

```typescript
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 {}
```

{% endcode %}

</details>

### AppModule

We need to add all the new components to our AppModule.

#### Typescript

<details>

<summary><code>app.module.ts</code></summary>

```typescript
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 {}

```

</details>

## 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.

<figure><img src="https://1243566085-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPyTHGwZ0VereJkRNmGxE%2Fuploads%2FQumRxQSCTooIPza5Vp5O%2Fimage.png?alt=media&#x26;token=4099b95f-77e6-41a6-8bef-7a1e3b770b45" alt=""><figcaption><p>Form rendered via API</p></figcaption></figure>

{% hint style="info" %}
We recommend playing around with simple forms first, and then adding complexity as you get more comfortable.
{% endhint %}

{% hint style="info" %}
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!
{% endhint %}
