Angular forms are powerful and, well known, can be used in a way that greatly simplify your own forms by also improving the reusability of your components.
We will see how by implementing our own form with our own custom field and see how we can retrieve its value.
Scenario
We will be creating a reclamation form.
From our page, a customer will be able to specify its invoice id which has some validation and logic such as:
-
A prefix since all of our invoice ids start with
INV-
- A length our invoice id is at most 8 chars long
Creating our initial form
Let's write our first version only with reactive forms.
We will need a FormGroup
that is wrapping our invoiceId
field:
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
form: FormGroup<{ invoiceId: FormControl<string | null> }>;
constructor(fb: FormBuilder) {
this.form = fb.group({
invoiceId: ['', [Validators.pattern(/^INV-.*/), Validators.maxLength(8)]],
});
}
}
and our associated HTML:
<h1>Invoice reclamation</h1>
<form [formGroup]="form">
<label for="invoice-id">Invoice ID: </label>
<input
type="text"
formControlName="invoiceId"
id="invoice-id"
placeholder="INV-xxxx"
/>
<button type="submit" [disabled]="!form.valid">Send</button>
</form>
If you want to see what's going on internally, you may want to add this bit of code at the end of your template:
<hr /> <p>Value:</p> <pre>{{ form.value | json }}</pre> <p>Errors:</p> <pre>{{ form.controls.invoiceId.errors | json }}</pre>
So far so good:
Enhancing the field
We might want to help the user filling this form
For example, if we could specify ourselves the prefix it could be great. Even better, we could show him the subject of his invoice when he types it to offer him some context.
Let's modify our code a bit to achieve this:
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
form: FormGroup<{ invoiceId: FormControl<string | null> }>;
constructor(fb: FormBuilder) {
this.form = fb.group({
invoiceId: ['', Validators.maxLength(4)],
});
}
}
<h1>Invoice reclamation</h1>
<form [formGroup]="form">
<label for="invoice-id">Invoice ID: </label>
<span style="font-family: 'Courier New', Courier, monospace">INV-</span>
<input
type="text"
formControlName="invoiceId"
id="invoice-id"
placeholder="xxxx"
/>
<p *ngIf="form.controls.invoiceId.value && form.controls.invoiceId.valid">INVOICE n°...</p>
<button type="submit" [disabled]="!form.valid">Send</button>
</form>
That's a bit better for the user:
However for us, not so much
By doing this we:
- Introduced more complexity in our form even tho this behavior is only related to a specific field
- We changed the expected value of our
invoiceId
field: if somebody that is dealing with it elsewhere is used to see it starts withINV-
, he probably won't get at first glance why we aren't enforcing it in our validators
Fortunately, Angular offers a way for developers to write their own form fields and encapsulate them, let's do that!
The Control Value Accessor
Introduction
The ControlValueAccessor
interface is a very handy interface that helps us writing our form fields
It exposes four methods (including with an optional one):
-
writeValue(obj: any): void
That is called whenever a new value is provided to our field from the forms API -
registerOnChange(fn: any): void
Provides the function that we will have to call when we will be making any change to our field -
registerOnTouched(fn: any): void
Same as the previous one but for thetouched
behavior -
setDisabledState(isDisabled: boolean)?: void
Called whenever the forms API informs our field that the disabled state has changed so that we can apply it to our field
Applied to our field
Creating a new component
In a new InvoiceIdFormField
component that will be holding our field, let's move our logic there.
We will just replace the whole form by a simple FormControl
since we will only be manipulating the invoice id here:
@Component({
selector: 'app-invoice-id-form-field',
templateUrl: './invoice-id-form-field.component.html',
})
export class InvoiceIdFormFieldComponent {
readonly invoiceIdControl: FormControl<string | null>;
constructor() {
this.invoiceIdControl = new FormControl<string | null>(
null,
Validators.maxLength(4)
);
}
}
<label for="invoice-id">Invoice ID: </label>
<span style="font-family: 'Courier New', Courier, monospace">INV-</span>
<input
type="text"
[formControl]="invoiceIdControl"
id="invoice-id"
placeholder="xxxx"
/>
<p *ngIf="invoiceIdControl.value && invoiceIdControl.valid">INVOICE n°...</p>
Implementing ControlValueAccessor
Now that our component is created, we can implement the interface:
@Component({
selector: 'app-invoice-id-form-field',
templateUrl: './invoice-id-form-field.component.html',
})
export class InvoiceIdFormFieldComponent implements ControlValueAccessor {
+ private _onChange: (invoiceId: string | null) => any = noop;
+ private _onTouched: () => any = noop;
readonly invoiceIdControl: FormControl<string | null>;
constructor() {
this.invoiceIdControl = new FormControl<string | null>(
null,
Validators.maxLength(4)
);
}
+ writeValue(invoiceId: string | null): void {
+ this.invoiceIdControl.setValue(invoiceId);
+ }
+ registerOnChange(fn: any): void {
+ this._onChange = fn;
+ }
+ registerOnTouched(fn: any): void {
+ this._onTouched = fn;
+ }
+ setDisabledState?(isDisabled: boolean): void {
+ isDisabled
+ ? this.invoiceIdControl.disable()
+ : this.invoiceIdControl.enable();
+ }
}
Since everything is ready for Angular to accept our component as a field, we now have to propagate the value when it is set:
export class InvoiceIdFormFieldComponent implements OnDestroy, ControlValueAccessor {
// ...
private readonly _componentDestroyed$ = new Subject();
constructor() {
this.invoiceIdControl = new FormControl<string | null>(
null,
Validators.maxLength(4)
);
this.invoiceIdControl.valueChanges.pipe(
map(invoiceId => (invoiceId ? 'INV-' : '') + invoiceId),
takeUntil(this._componentDestroyed$),
).subscribe(invoiceId => {
this._onChange(invoiceId);
this._onTouched();
});
}
ngOnDestroy(): void {
this._componentDestroyed$.next('');
this._componentDestroyed$.complete();
}
// ...
Since we are manipulating our value internally, we can now prepend the
INV-
prefix ourselves
Last but not least, we now have to indicate to Angular's form API how to use our component as a field:
@Component({
selector: 'app-invoice-id-form-field',
templateUrl: './invoice-id-form-field.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => InvoiceIdFormFieldComponent),
+ multi: true
+ }
+ ]
})
Using our form field
Great! Now that this field and its logic is encapsulated, we can use it from our main form.
Let's start by simplifying the validators:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
form: FormGroup<{ invoiceId: FormControl<string | null> }>;
constructor(fb: FormBuilder) {
this.form = fb.group({ invoiceId: '' });
}
}
And then we can just replace our previous template simply by our field:
<h1>Invoice reclamation</h1>
<form [formGroup]="form">
<app-invoice-id-form-field
formControlName="invoiceId"
></app-invoice-id-form-field>
<button type="submit" [disabled]="!form.valid">Send</button>
</form>
Let's test it:
We can see that our component is recognized by Angular as a form field and can be bound to our form value.
Even if we are using a
FormGroup
there, you can also use[(ngModel)]
if you only have a single value, no need to change anything
Propagating the errors
This is great, however, you might have noticed that breaking a validator of our invoiceIdControl
does not propagate the error to the englobing form.
This is surprisingly harder that what we achieved so far and we will need to change the way our control value accessor is defined to do so.
For this to work, we will need to retrieve the NgControl
Angular is using for this component by capturing it using the dependency injection and reroute its accessor to the component itself:
constructor(
+ @Self()
+ private readonly control: NgControl
) {
+ this.control.valueAccessor = this;
this.invoiceIdControl = new FormControl<string | null>(
null,
- Validators.maxLength(4)
+ Validators.maxLength(8)
);
this.invoiceIdControl.valueChanges
.pipe(
map((invoiceId) => (invoiceId ? 'INV-' : '') + invoiceId),
takeUntil(this._componentDestroyed$)
)
.subscribe((invoiceId) => {
this._onChange(invoiceId);
this._onTouched();
});
}
However, by doing so, the component will be injecting himself in its constructor, resulting in a circular dependency. In order to break it, we will have to remove it from the NG_VALUE_ACCESSOR
provider:
@Component({
selector: 'app-invoice-id-form-field',
templateUrl: './invoice-id-form-field.component.html',
- providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => InvoiceIdFormFieldComponent),
- multi: true,
- },
- ],
})
Finally, we will need to assign the validators of the captured control to the ones used by the component internally:
// ...
ngOnInit(): void { this.control.control?.setValidators([this.invoiceIdControl.validator!]);
this.control.control?.updateValueAndValidity();
}
// ...
And voilà, Angular is now able to interact with the control to retrieve its value and its errors:
Take aways
In this post we saw:
- How to extract the logic and the template bound to a specific field out of a form
- How to make a component act like a form field for the Angular form API
- How to propagate errors from our custom form field to the main form
Final words
Extracting custom form fields can really help you to keep your codebase clean and straightforward. By doing so, you will end up with clearer forms that does not deal with the extra complexity of every fields, and components that are clearly dealing only with what they are supposed to.
Our example is pretty simple but this can also result in a better user experience if you are designing your field adequately. For example, for this invoice, we could have added a link to an external service when identifying an invoice, a PDF preview of it or extra validation to help the user using this field: using a dedicated component helps you to unleash the full power of Angular inside it.
However, keep in mind that this is still a form field and you should be able to establish boundaries: making everything a form field might be as painful as not having any since you will deal with a lot of new layers.
Photo by Romain Dancre on Unsplash
Top comments (4)
Sorry but the way to implement validation is much easier. You should provide NG_VALIDATORS the same way you did with NG_VALUE_ACCESSOR and implement the validate function in your component and it will automatically propagate validation to the parent form, no need to inject anything.
Thanks for your feedback!
Indeed I have also been told to do so, unfortunately I did not find any proper documentation to explain it
Would you mind providing a link or the adapted example using this approach? I would gladly update the post to show both ways!
Sorry for the delay, I don't receive messages from this platform in my inbox with comments. Here you have an example I have just made:
[(stackblitz.com/edit/stackblitz-sta...]
You have this article from Vasco, pretty straightforward :
blog.angular-university.io/angular...