Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.
Overview
Angular’s reactive forms are a great way to quickly build complex and robust forms. However, the design tends to force developers to build large components that encompass the entire form and can make it challenging to re-use or nest sub-forms efficiently. By changing the way you think about sections of forms and leveraging ControlValueAccessor, it can become easy to build customizable form sections that can be shared between many forms in an application. This article heavily relies upon the information in the Control Value Accessor article, so take the time to review that information first.
To follow along with the articles there a two repositories created: a starting point and final solution.
Use Case
In many applications, there are often sets of fields that appear across many different forms. For example, an e-commerce application may have address fields appear in user registration, billing information, shipping information, etc. For this example, we will create a sub-form of user information that we can add into the testing form created in the Control Value Accessor article.
To begin, we can create a component that will represent our sub form. We want the form to have three inputs: first name, last name, and email address. We will implement some validation by requiring the last name and validating the email address pattern. I have added some validation styling using Bootstrap to help clearly show the status of the controls at all times:
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="userForm" class="was-validated">
<div class="form-group">
<label for="firstName">First Name</label>
<input id="firstName" type="text" class="form-control" formControlName="firstName">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input id="lastName" type="text" class="form-control" formControlName="lastName" required>
<div class="invalid-feedback">
Last Name is required
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input id="email" type="email" class="form-control" formControlName="email">
<div class="invalid-feedback">
Not a valid email address
</div>
</div>
</form>
`
})
export class UserFormComponent implements OnInit {
public userForm: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.userForm = this.formBuilder.group({
firstName: null,
lastName: [null, Validators.required],
email: [null, Validators.email]
});
}
}
Making It Reusable with ControlValueAccessor
ControlValueAccessor is not just limited to single value controls, we can use it to return objects of data. As long as the ControlValueAccessor interface is implemented we can use any method we want to manipulate and display the data, so why not another form? Let’s do this!
Implement ControlValueAccessor
Just as with a single value control, we can implement ControlValueAccessor for our sub-form. The forms API makes some of this quite easy to implement, the relevant changes are show below:
Template
<ng-container [formGroup]="userForm">
<input id="firstName" type="text" class="form-control" formControlName="firstName" (blur)="formTouchFn()">
<input id="lastName" type="text" class="form-control" formControlName="lastName" required (blur)="formTouchFn()">
<input id="email" type="email" class="form-control" formControlName="email" (blur)="formTouchFn()">
</ng-container>
Component
public formTouchFn;
constructor(private formBuilder: FormBuilder, private ngForm: FormGroupDirective) { }
writeValue(obj: any): void {
const emtptyForm = {
firstName: null,
lastName: null,
email: null
};
const data = Object.assign(emtptyForm, obj);
this.userForm.patchValue(data);
}
registerOnChange(fn: any): void {
this.userForm.valueChanges.subscribe(fn);
}
registerOnTouched(fn: any): void {
this.formTouchFn = fn;
}
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.userForm.disable();
} else {
this.userForm.enable();
}
}
- writeValue – there is some extra considerations to take here besides just setting the value. There are two methods available to us for setting the value of a form, setValue and patchValue. They work in a similar manner with the distinction that setValue expects a mapping for every property in the form whereas patchValue ignores any non-matching values. Each can successfully be used here, but you’ll need to be aware of the consequences of each choice. The other consideration is handling a null parameter to writeValue – which is exactly what happens when using the reset form method. In the code above that is using patch value, passing null will not clear the existing values as every control is skipped. By merging with an empty object we can ensure that the control value is cleared when writeValue is not given a property for the control.
- registerOnChange – forms have an Observable that is fired every time a change is made to the form, all we need to do is bind the callback to the Observable callback. Easy!
- registerOnTouched – this is unfortunately not a simple as registerOnChange because there is nothing Angular provides to subscribe to touch events in a form (at least until #10887 is addressed). The best option is to bind the callback to the blur event for each input in order for the touch to cascade up the form properly. If your application doesn’t rely on the form level touched then this can generally be omitted.
- setDisabledState – we can leverage the disable and enable methods on the form that will cascade the disabled value down the line for us
- form – note that the wrapper element around the form is no longer a form tag. You can see unexpected results when nesting form elements so it is good practice to leave the form definition for the high level forms only. If you need access to the form element (e.g. if using the submitted state) you can access it for the consuming form by injecting FormGroupDirective into the sub-form as shown in the constructor.
Implement Validator
We also want any validation errors on our form to propagate to the consuming parent level form. In order to do this, we need to implement the Validator interface.
validate(control: AbstractControl): ValidationErrors {
return this.userForm.valid ? null : { invalidForm: { valid: false, errors: this.userForm.errors } };
}
registerOnValidatorChange?(fn: () => void): void { }
validate – returns an object when the form is not valid. Note – errors do not roll up in Angular, so only form level errors are displayed here. If you want to see all errors for all controls you can implement something that loops through them all like the following:
validate(control: AbstractControl): ValidationErrors {
const form = this.userForm;
if (form.valid) {
return null;
}
const errors = {};
Object.keys(form.controls).forEach(k => {
if (form.controls[k].invalid) {
errors[k] = form.controls[k].errors;
}
});
return errors;
}
registerOnValidatorChange – as the validators never change for this form we don’t need to implement anything. If validators were determined by component inputs, we would just need to call the callback function in ngOnChanges.
Register Providers
Lastly – we just need to register the component as a value accessor and validator provider:
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserFormComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => UserFormComponent),
multi: true
}
]
Testing the Form
To test the form, all we need to do is add it to an existing form like any other control:
Template
<app-user-form formControlName="user"></app-user-form>
Component
this.testForm = this.formBuilder.group({
testInput: 'abc',
rating: [3, Validators.required],
user: {firstName: 'Steve', email: 'steve@test.com'}
});
The sub form works as expected to populate the user data, validation on the individual controls in the sub form carry to the parent form validity, and all controls follow the disabled and reset commands just as the native input on the parent level of the form.
Conclusion
ControlValueAccessor is a powerful tool to extend Angular forms and design compact and re-usable forms. In applications that have many re-usable forms, extracting an abstract base class to provide a default ControlValueAccessor implementation is fairly trivial and can make for extremely fast development of contained and re-usable form elements that interact seamlessly with parent forms.
About Intertech
Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.
Related Articles In This Series
Great article, thanks.
do not need to unsubscribe?
this.userForm.valueChanges.subscribe (fn);
No, there is no need to unsubscribe in this case. A memory leak from a subscription occurs when the source of the observable is alive but the subscriber is destroyed. This usually happens with a root level service that stays alive for the life of the application which a component subscribes to and is later destroyed. Garbage collection will not then fully clean up the destroyed object instance of the component and the handling of the subscription stays active. In this case the source of the observable gets destroyed so there are no further emitted values and the source and subscriber get cleaned up since they are both destroyed. With all that said, it is never bad practice to unsubscribe if you are not sure if the subscription will cause a problem to be safe!