l

Request A Quote

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]
        });
    }
}
Here is the resulting form showing the validation checks are working for both fields:

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 appears and is pre-loaded with the default values specified and ready to test:

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, Blackslate Software 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.