Mind is Software

Ying’s thoughts about software and business

Angular Form Control

This blog describes the concept and implementation of Angular form control.

1 Introduction

Generally speaking, there are two ways to create a reusable component in Angular: as a regular Component, or as a value accessor. A value accessor is a Component that also implements the ControlValueAccessor interface. A value accessor wraps a DOM element and can be bound to a FormControl class intance, called a form control, that tracks the value and the validation status of the DOM element. It can be used alone or in a form.

Angular uses FormControlDirective (selector: [formControl]), FormControlName (selector: [formControlName]), and NgModel directives to create a 2-way binding of a form control to a DOM element via the corresponding value accessor. It means the following:

any values written to the FormControl instance programmatically will be written to the DOM element (model -> view). Conversely, any values written to the DOM element through user input will be reflected in the FormControl instance (view -> model).

A ControlValueAccessor interface acts as a bridge between a forms control and a DOM element.

For example, <input type="text" [formControl]="myControl"> bind the <input> element to a property of myContorl - it is a FormControl instance created in the component. The value accessor for the <input type="text"> element is the DefaultValueAccessor.

FormControlDirective is designed to be used as a standalone control. FormControlName is designed to be used with a parent FormGroupDirective (selector: [formGroup]). NgModel is used in template-driven forms – not recommended for its testing difficulties and synchronous API.

2 The Value Accessor

The FormControlDirective directive, and the other two directives too, uses the NG_VALUE_ACCESSOR injector token to get a ControlValueAccessor instance. A ControlValueAccessor interface wraps around a form element and has four methods:

  • writeValue: to write a value to a native element.
  • registerOnChange: to register a callback on a change in the UI: a value change of the element.
  • registerOnTouch: to register a callback when the element receives a blur event.
  • setDisabledState: to disable or enable the element.

Angular implements default value accessors for Dom element like input(text, number, checkbox, radio and range), textarea, select and select[multiple].

The DefaultValueAccessor for input and textarea has the following code (changed for explanation purpose):

// default_value_accessor.ts
export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true,
}

@Directive({
  selector: 'input:not([type=checkbox])[formControl], textarea[formControl]',
  host: {
    '(input)': 'onChange',
    '(blur)': 'onTouched()',
    providers: [DEFAULT_VALUE_ACCESSOR],
  })
  export class DefaultValueAccessor implements ControlValueAccessor {
  onChange = (_: any) => {}
  onTouched = () => {}

  writeValue(value: any): void {
    const normalizedValue = value == null ? '' : value
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue)
  }
  registerOnChange(fn: (_: any) => void): void {
    this.onChange = fn
  }
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn
  }
  setDisabledState(isDisabled: boolean): void {
    this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled)
  }
}

The DefaultValueAccessor is a good example of wrapping a DOM element. It writes value to the DOM element. It listens to input and blur events of the home element and call the registered callbacks.

Any component or directive can implement ControlValueAccessor interface and register itself as a NG_VALUE_ACCESSOR proivder.


Share