Skip to content
Photo: Martin Adams/Unsplash

Angular: custom form components and the ControlValueAccessor

When we build web applications for our customers, we often do so with Angular. From time to time, we also build a UI library that is based on the corporate identity and is tailored to the requirements of the expected use cases as far as possible.

Knowledge
Software architecture

Of course, this also includes providing standard form components.

In this article, I will compare two ways of building form components. What advantages and disadvantages do they have and when is which method recommended? In order to be able to go into more detail, there is a brief overview of Angular Forms and in particular the function of the ControlValueAccessor interface.

Angular Forms in a nutshell

Angular Forms offers two different APIs for interacting with form elements: Reactive Forms and Template-driven Forms. With reactive forms, application developers use an object of type FormControl to interact programmatically with the form element. This is bound to the object via a directive. With template-driven forms, another directive can be used to bind a variable with the content directly to the form element. Thanks to Angular's two-way binding, changes are automatically passed on both ways in this case.

<!-- template-driven form -->
<input [(ngModel)]="myFormValue" />
 
<!-- reactive form -->
<input [formControl]="myFormControl" />

Example: Template section with both Forms APIs.

export class MyComponent implements OnInit, OnDestroy {
 
    // template-driven form
    public myFormValue = 'initial value';
 
    // reactive form START
    public myFormControl = new FormControl(
        'initial value'
    );
    private _formSubscription: Subscription;
 
    ngOnInit(): void {
        this._formSubscription = this.myFormControl
            .valueChanges.subscribe((value) => {
                // use the form value...
        });
    }
 
    ngOnDestroy(): void {
        this._formSubscription?.unsubscribe();
    }
    // reactive form END
}

Example: The corresponding Typescript snippet with both Forms APIs.

The example clearly shows that the reactive forms are somewhat more complex to use. However, you can access more functions or take advantage of the fact that you receive the input values as observables. Unfortunately, a good example of this would go beyond the scope of this article.

What is not apparent at this point, but will be important later on, is that a FormControl instance is also used under the hood of template-driven forms, hidden by Angular and invisible to the application developer.

The ControlValueAccessor

If you now ask yourself how the content of the FormControl (created in whatever way) is associated with the corresponding DOM element, you should sooner or later come across the ControlValueAccessor interface. This must be implemented in order to develop your own form components - and it is precisely the link at this point.

But where exactly is it used if you don't build your own component but work directly with the corresponding HTML components? This question can be answered with a quick look at the following code snippet, which comes from the Angular Forms package:

@Directive({
  selector:
      'input:not([type=checkbox])[formControlName],' +
      'textarea[formControlName],' +
      'input:not([type=checkbox])[formControl],' +
      'textarea[formControl],' +
      'input:not([type=checkbox])[ngModel],' +
      'textarea[ngModel],' +
      '[ngDefaultControl]',
      //...
})
export class DefaultValueAccessor
        implements ControlValueAccessor {
    // ...
}

An excerpt from the DefaultValueAccessor in the @angular/forms package.

What we see here is an implementation of the ControlValueAccessor that is automatically instantiated for all native text inputs (i.e. inputs that are not checkboxes or textareas). One of the directives of the Angular Forms API (formControlName, formControl, or ngModel) is bound to this. The last selector in the list has nothing to do with automatic instantiation, but will be relevant later.

It is important: A ControlValueAccessor is used not only for your own, but also for native form components.

Spoilt for choice: building your own components

Now that the function of Angular Forms has been highlighted, we will now look at the two API approaches mentioned at the beginning. In one of the two cases, this knowledge of how Angular Forms works can significantly simplify the implementation.

The two approaches can be easily distinguished by the quality objectives they fulfill. The first aims to give application developers as much freedom and flexibility as possible. The second offers a much narrower API, which reduces maintenance effort and limits the possibility of misuse.

The open approach

The first, „open” approach is particularly suitable for situations in which a large number of use cases need to be covered. Angular Material, for example, has opted for this as an open source library:

<mat-form-field>
    <mat-label>Label</mat-label>
    <input
        matInput
        type="text"
        [formControl]="myFormControl" />
</mat-form-field>

Open form component made from Angular Material.

The user has full control here and can, for example, add further accessibility attributes to the input element or use individual text markup in the label. Optional prefix and suffix elements can also be flexibly integrated without having to make changes to the library. This approach works well for large general-purpose libraries that want to cover as many use cases as possible, but often do not even know the individual users and therefore cannot or do not want to respond to individual requests.

In this way, the application retains control over the native input element and no intervention in the form mechanics is necessary. However, this approach is only mentioned for the sake of completeness and will not be described in detail.

The closed approach

In contrast, the second, „closed” approach is more suitable if the use cases are known and, if necessary, individual application requirements can be addressed.

<lib-text-field
    [formControl]="myFormControl"
    label="Label"
></lib-text-field>

Closed form component, as I have already built in customer projects.

The options here are comparatively limited, but experience has shown that this is sufficient in most use cases. As there is only one Angular component that is visible to the application, the flexibility within the library increases. This narrower API simplifies maintenance because it reduces the likelihood of changes to the code being reflected in a changed API.

Third-party libraries can also be used for special components to map their behavior, but the users of the UI library will not notice this.

The catch with this approach is that the native input element is no longer visible to the application. This means that the custom form component, which is actually only intended to influence the display, must now implement the ControlValueAccessor interface itself in order to be used with one of the forms APIs - even if the behavior of the encapsulated form element is not to be influenced at all. Initially, this means that more custom code has to be written and correspondingly more code has to be maintained. So is there a way to keep this additional code to an absolute minimum?

Don’t reinvent the wheel

The first best way to implement the ControlValueAccessor interface would probably be to implement it manually, instantiating another FormControl that is bound to the native input element and somehow attaching the methods of the interface to it. Although this works relatively simply in principle, it also offers opportunities to make mistakes - for example, if an element is to be marked as touched. Angular's built-in ControlValueAccessors are designed and tested for multiple browsers and end devices - an effort that should not be taken on for no reason.

During my original research on how to do this better, I came across the article Don't reinvent the wheel. While the basic idea is good, the implementation unfortunately suffers. To instantiate the ControlValueAccessor, a formControl attribute is used on the input element. However, this is not passed a FormControl, but an empty value. On the one hand, this leads to unsightly errors appearing in the browser console at runtime, and on the other hand, all unit tests of the component will inevitably fail.

Use the selector, Luke

At this point, you should take another look at the listing of the DefaultValueAccessor and look at the selectors. You will notice that you can also create an instance of this ValueAccessor with ngDefaultControl. You can then access it via @ViewChild Decorator and forward all relevant calls.

<label>{{label}}</label>
<input ngDefaultControl />

Template of a closed input component.

@Component({
    selector: 'lib-text-field',
    templateUrl: './text-field.component.html',
    styleUrls: ['./text-field.component.css'],
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(
            () => TextFieldComponent
        ),
        multi: true
    }]
})
export class TextFieldComponent
        implements ControlValueAccessor {
 
    @Input()
    label: string;
 
    @ViewChild(DefaultValueAccessor, {static: true})
    valueAccessor: ControlValueAccessor;
 
    // actual ControlValueAccessor methods,
    // directly forwarding to the view child.
 
    registerOnChange(fn: any): void {
        this.valueAccessor.registerOnChange(fn);
    }
 
    registerOnTouched(fn: any): void {
        this.valueAccessor.registerOnTouched(fn);
    }
 
    writeValue(obj: any): void {
        this.valueAccessor.writeValue(obj);
    }
 
    setDisabledState(isDisabled: boolean): void {
        this.valueAccessor.setDisabledState(isDisabled);
    }
}

Typescript code of a closed input component.

This approach can be further refined by building an abstract class that takes over the passing of the interface methods. In such a superclass, you can also include aspects for displaying error states, for example. I have built an example of this on Github.

Conclusion

I think the closed approach shown here is suitable in many cases and somewhat easier to maintain than the open approach. With the right knowledge of Angular Forms, it can also be implemented without much effort.

Author

Lukas Taake

Developer with a preference for FOSS. Trying to keep an eye on the essentials, always on the lookout for new challenges.

About us

Holisticon in 80 Words

Holisticon is a tech consultancy that operates holistically, - by managing Holisticon is a tech consultancy that operates holistically, - by managing strategy, organization and technology right from the beginning to empower and future-proof the digitalization of innovative business models.

Holisticon was founded in 2007 by Oliver Ihns and Dierk Harbeck. Today, more than 80 employees from Hamburg, Hannover and Munich successfully guide numerous companies from a variety of industries through the transformation process. Holisticon has also partnered with leading tech brands and is a member of the international Nexer Group.