Custom form controls in Template Driven Forms
Updated: 15 February 2024
Problem
Given the below form I am able to manage the data and validation state of my component automatically as driven by the underlying Angular and HTML implementation
1import { CommonModule } from '@angular/common';2import { Component } from '@angular/core';3import { FormsModule } from '@angular/forms';4import { SampleInput } from './sample-input';5
6@Component({7 standalone: true,8 selector: 'example-form',9 imports: [CommonModule, FormsModule, SampleInput],10 template: `11 <form #form="ngForm">12 <h1>Example form</h1>13
14 <sample-form-input15 [required]="true"16 [(ngModel)]="data['isActive']"17 name="Name"18 label="This is a name input"19 />20
21 <div>22 <button type="submit" [disabled]="!form.form.valid">23 Is Form Valid: {{ form.form.valid }}24 </button>25 </div>26 </form>27 <pre>{{28 {29 data: data,30 value: form.form.value,31 status: form.status32 } | json33 }}</pre>34 `,35})36export class ExampleFormComponent {37 data: Record<string, any> = {};38}
However, as soon as I move my input
into a different component I am usually forced to do lots of weird things that steer me further away from a more simplified HTML-directed form. For the sake of simplicity and maintainability however I would like to be able to define a component that is able to take advantage of the Angular/HTML form integration while also providing me with the benefits of a component-based form input
Implementing a ControlValueAccessor
In order to do this, I can move the input into a new component provided that the component implements the ControlValueAccessor
interface and the Validation
interface if I would also like to use Angular Validation with my component
The basic implementation of a component that meets this requirement can be seen below:
1import { Component, forwardRef } from '@angular/core';2import {3 AbstractControl,4 ControlValueAccessor,5 FormsModule,6 NG_VALIDATORS,7 NG_VALUE_ACCESSOR,8 ValidationErrors,9 Validator,10 Validators,11} from '@angular/forms';12
13@Component({14 standalone: true,15 selector: 'sample-form-input',16 imports: [FormsModule],17 providers: [18 {19 // Tell Angular that we can handle the value management by way of NgModel20 provide: NG_VALUE_ACCESSOR,21 useExisting: forwardRef(() => SampleInput),22 multi: true,23 },24 {25 // Tell angular that we also want to enable validation on our component26 provide: NG_VALIDATORS,27 useExisting: forwardRef(() => SampleInput),28 multi: true,29 },30 ],31 template: `32 <!-- Below is an example implementation that meets the UI requirements for33 the form to be template-driven -->34
35 <input36 class="block"37 type="text"38 [ngModel]="value"39 (ngModelChange)="onChange($event)"40 [disabled]="disabled"41 />42 `,43})44export class SampleInput implements ControlValueAccessor, Validator {45 value?: any;46 disabled: boolean = false;47 focused: boolean = false;48
49 onChange: any = () => {};50 onTouched: any = () => {};51
52 writeValue(value: any): void {53 this.value = value;54 }55
56 registerOnChange(fn: any): void {57 this.onChange = fn;58 }59
60 registerOnTouched(fn: any): void {61 this.onTouched = fn;62 }63
64 setDisabledState(isDisabled: boolean): void {65 this.disabled = isDisabled;66 }67
68 onBlur(): void {69 this.focused = false;70 }71
72 onFocus(): void {73 this.focused = true;74 }75
76 getValidator() {77 return Validators.maxLength(10);78 }79
80 validate(control: AbstractControl<any, any>): ValidationErrors | null {81 const validator = this.getValidator?.();82 if (!validator) {83 return null;84 }85
86 return validator(control);87 }88}
Now that we have this component, we can simply swap out the use of input
to use our new component in our example form:
1// rest of component2template: `3 <form #form="ngForm">4 <h1>Example form</h1>5
6 <sample-form-input7 [required]="true"8 [(ngModel)]="data['isActive']"9 name="Name"10 label="This is a name input"11 />12
13<-- rest of template -->14`15// rest of component
The above provides a basis for any input component we want. It is also possible to define the above as a base class that can then be extended by other components to provide a more specific implementation such as working with a generic value
to be a bit more type safe or to allow more specific variations or any additional styling to be contained to a specific component