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

1
import { CommonModule } from '@angular/common';
2
import { Component } from '@angular/core';
3
import { FormsModule } from '@angular/forms';
4
import { 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-input
15
[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.status
32
} | json
33
}}</pre>
34
`,
35
})
36
export 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:

1
import { Component, forwardRef } from '@angular/core';
2
import {
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 NgModel
20
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 component
26
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 for
33
the form to be template-driven -->
34
35
<input
36
class="block"
37
type="text"
38
[ngModel]="value"
39
(ngModelChange)="onChange($event)"
40
[disabled]="disabled"
41
/>
42
`,
43
})
44
export 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 component
2
template: `
3
<form #form="ngForm">
4
<h1>Example form</h1>
5
6
<sample-form-input
7
[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