Build your own angular forms - ngModel

pardeepr08 - Aug 6 - - Dev Community

If you are an angular developer, you must have used angular form package many a times in your projects to manage your forms.

If you are curious to know, what goes behind the scenes to propel such an awesome form management package?

I am trying to explain and decode angular forms through some series of articles like how angular form package is organized, written and well architected to handle so many different type of form controls and related use cases.

Don't worry if you feel overwhelmed seeing above flow diagram, we will go through it one component at a time.

As you already know, ngModel is used to synchronize the values defined in component with any form elements value.

But how ngModel does so?

So, let's think over some of the use cases, ngModel should have implemented.

  1. It should accept input property ngModel which will be synchronized with underlying host element.

  2. It should support all different types of form elements like text input, number input, checkbox, select etc.

  3. It should also work with the custom form elements.


Complexity involved in implementing ngModel directive with above use cases

As different form elements are dealt differently which means when you write to them, you write to different properties and when you want to track changes in form elemets, you might have to register for different types of events relevant to host element where you apply ngModel directive

For example, for input having types as text | number | email we will write to the value property but for checkbox we write to the checked property


Picking the naive approach to design ngModel

@Directive({
  selector: '[ngModel]',
  exportAs: 'ngModel'
})
export class NgModel implements OnChanges {
  @Input('ngModel') model: any
  @Output('ngModelChange') update = new EventEmitter()

  constructor(private _elementRef: ElementRef) {}

  ngOnChanges(changes: SimpleChanges): void {    
    this._elementRef.nativeElement.value = this.model
  }

  @HostListener("input", ["$event.target.value"]) onInput(value: any) {
    this.update.emit(value)
  }
 }
Enter fullscreen mode Exit fullscreen mode

So, as you can see, we inject the elemetRef to get hold of host element and write to it when ngModel input property is passed during change detection and also, we register on host element for the input event and emit updated input value back to the use case component.

Though simple approach but it has some drawbacks:

  1. If we adopt this approach, it won't scale easily for remaining form controls as different form controls involve some complexity when writing and reading their values.

  2. Putting everything in ngModel makes it less configurable and hard to work with custom form elements.


Instead, how angular does it?

Image description

Referring to the above diagram, ngModel delegate this responsibility of reading and writing to different types of form elements to different set of directives called control value accessors.

What are control value accessors?

These are just simple directives which gets applied to a particular or group of form elements based on their selectors.

Each control value accessor implements below interface

export interface ControlValueAccessor {
    writeValue(obj: any): void;
    registerOnChange(fn: any): void;
}
Enter fullscreen mode Exit fullscreen mode

How it helps ngModel?

Each CVA directive is aware of, how to write to particular host element and how to read it by registering to events emitted by host element and thus it frees ngModel from managing the complexity.

Also these CVA directives expose themselves as a service on a DI token NG_VALUE_ACCESSOR which ngModel can inject and get hold of correct control value accessor to write and read values from host element


When ngModel directive is applied on a form element then not only ngModel instance is created but alongwith it an instance of control value accessor directive is also created which is relevant to host element.

Angular provides different types of CVA directive

  1. DefaultControlValueAccessor, It is created when applying ngModel on inputs expect the input having type as checkbox.

  2. CheckBoxControlValueAccessor, When applying ngModel on checkbox input, instead of default CVA this is created.

  3. SelectControlValueAccessor, Its created for select box and a few more.

How ngModel integerate with CVA ?

Control value accessor directive expose itself as a service on a dependency injection token NG_VALUE_ACCESSOR

const DEFAULT_VALUE_ACCESSOR: Provider  = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => DefaultControlValueAccessor)
}

@Directive({
    selector: "input:not([type=checkbox])[ngModel]",
    providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultControlValueAccessor extends BaseControlValueAccessor implements ControlValueAccessor {    
    writeValue(value: string): void {
        this.setProperty("value", value)
    }

    @HostListener("input", ["$event.target.value"]) onInput(value: string) {
        this.onChange(value)
    }
}
Enter fullscreen mode Exit fullscreen mode

ngModel can inject this DI token and get hold of correct CVA directive created.

constructor(@Inject(NG_VALUE_ACCESSOR) valueAccessor: ControlValueAccessor) {
    this.valueAccessor = valueAccessor;
  }
Enter fullscreen mode Exit fullscreen mode

ngModel register itself using registerOnChange method of the injected control value accessor directive instance so that it gets all updates made by user on the host element and thus emit the updated data back to component using ngModelChange event emitter.

How ngModel works with custom form elements?

@Component({
  selector: 'choose-quantity',
  templateUrl: "choose-quantity.component.html",
  styleUrls: ["choose-quantity.component.scss"],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting: ChooseQuantityComponent
    }
  ]
})
export class ChooseQuantityComponent implements ControlValueAccessor {
Enter fullscreen mode Exit fullscreen mode

Actually, what ngModel cares about is control value accessor and a component representing a custom form element can simply implement control value accessor interface and configure itself as a service on the DI token NG_VALUE_ACCESSOR

Please share your thought and doubts in the comment section.

If you love exploring the angular form internals and recreate it from scratch with me, you can visit my YouTube channel

. . .
Terabox Video Player