--- title: "Part 5: The Screen & Form Elements" slug: "part-5-the-screen-form-elements" updated: 2025-10-21T03:01:46Z published: 2025-10-21T03:01:46Z canonical: "docs.processmaker.com/part-5-the-screen-form-elements" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.processmaker.com/llms.txt > Use this file to discover all available pages before exploring further. # Part 5: The Screen & Form Elements Completed code: [live example](https://stackblitz.com/edit/stackblitz-starters-krrqnt) / [download example](https://github.com/ProcessMaker/angular4pm/tree/part-5/src) ## Step 1: Screen Component 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** ```xml ``` ### Typescript **app-screen.component.ts** ```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 = 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** ```xml

{{ form.name }}

``` ### Typescript **app-form.component.ts** ```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; } ``` ## 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](/apidocs/input) - [Image](/apidocs/image) - [Multi Column](/apidocs/multi-column) - [Upload File](/apidocs/upload-file) - [Buttons](/apidocs/buttons) - [HTML Viewer](/apidocs/html-viewer) - [Default](/apidocs/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 **app-element-input.component.html** ```xml
``` #### Typescript **app-element-input.component.ts** ```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; } ``` ### Image #### Template **app-element-image.component.html** ```xml ``` #### Typescript **app-element-image.component.ts** ```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; } ``` ### Multi Column #### Template **app-multi-column.component.html** ```xml
 
``` ``` #### Typescript **app-multi-column.component.ts** ```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; } ``` ### Upload File #### Template **app-element-upload-file.component.html** ```xml
``` #### Typescript **app-element-upload-file.component.ts** ```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; } ``` ### Buttons #### Template **app-element-button.component.html** ```xml
``` #### Typescript **app-element-button.component.ts** ```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; } ``` ### HTML Viewer #### Template **app-element-html-viewer.component.html** ```xml
``` #### Typescript **app-element-html-viewer.component.ts** ```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; } ``` ### Default #### Template **app-element-default.component.html** ```xml Unsupported Component {{ element.component }} ``` #### Typescript **app-element-default.component.ts** ```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; } ``` ## 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** ```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 {} ``` ### AppModule We need to add all the new components to our AppModule. #### Typescript **app.module.ts** ```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 {} ``` ## 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. ![](https://cdn.document360.io/2d311614-fcb7-4424-8b4c-d4d3091eebeb/Images/Documentation/Form rendered via API.avif) > [!NOTE] > We recommend playing around with simple forms first, and then adding complexity as you get more comfortable. > [!NOTE] > 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!