Angular Component – Part 2: ngModel

In my last article, I’ve explained how to use @Input() and @Output() and how to combine them to offer two-way binding for your component. This time I want to take this component to behave like a proper control within a <form>-element that is using ngForm.

What’s ngModel, and why should I use it?

In a typical Angular application, we often encounter scenarios where we have to handle forms with various input controls and a button to send data or perform some sort of action. The question that often arises is if the form is in a state that allows the user to click the button and therefore perform the action. If the button should be disabled, we surely want to signal to the user what should he/she change in his/her input to make the button clickable.

For this scenario Angular has a concept called ngForm. If we use ngForm in a <form> element, all controls within the <form> that implement the ngModel pattern will automagically subscribe themselves to the parenting ngForm, which allows the developer to observe the state of said ngForm to give the user some feedback about the <form>s state.

Using ngModel

A simple example how to use ngModel within a ngForm:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <form #exampleForm="ngForm" (ngSubmit)="performAction()">
      <input type="text" pattern="a.+" [(ngModel)]="value" name="numberValue" />
      <button type="submit" [disabled]="!exampleForm.valid">perform action</button>
    </form>
  `
})
export class AppComponent  {
  value = "abc";

  performAction() {
    console.log(this.value);
  }
}

In this code example are a few things to unpack:
On line 6, we create an on-the-fly template variable called exampleForm and we take the ngForm attribute of <form> as its value. We also create an event binding to the onSubmit equivalent of angular (ngSubmit), calling performAction as soon as the form gets submitted.
On line 7, we use the standard HTML 5 pattern attribute for <input> fields to specify that we’re only going to accept inputs that start with an “a.” Then, we use the two-way binding version of ngModel to set the <input> value and listen for changes. At the end of line 7, we also set a name for our <input> element; As soon as you use ngModel within a ngForm, you’re obligated to specify a name for the element.
The last line that needs some explanation is line 8, where we use the previously created exampleForm variable to mark the <button> as disabled if the <form> is invalid. As mentioned before; because <input> uses ngModel, it subscribes to ngForm and therefore notifies if the user input matches the pattern, what then changes the valid property of exampleForm.

Quick hint if you can’t make this example work in your Angular project: You have to import FormsModule in your module to include both ngForm and ngModel.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

A naive approach

Ok then, in my last article, we created a time-input component, which supports two-way binding on [(time)]. So to optimally use our component as a control within a ngForm, we have to change that [(time)] into [(ngModel)]. My hyper-optimistic nature could make me believe that it’s not more than a simple renaming we have to perform. I’d say we give it a try:

@Component({
  selector: 'time-input',
  template: `
    <input [ngModel]="ngModel.hours" name="hours" type="number" />
    <input [ngModel]="ngModel.minutes" name="minutes" type="number" />
    <button type="button" name="addHour" (click)="onAddHourButtonClicked()">Add hour</button>
  `
})
export class TimeInputComponent  {
  @Input() ngModel: DayTime;
  @Output() ngModelChange = new EventEmitter<DayTime>();

  onAddHourButtonClicked() {
    const newValue: DayTime = {
      hours: this.ngModel.hours + 1,
      minutes: this.ngModel.minutes
    };
    this.ngModelChange.emit(newValue);
    this.ngModel = newValue;
  }
}
import { Component } from "@angular/core";
import { DayTime } from "./typeDefinitions";

@Component({
  selector: "my-app",
  template: `
    <form #exampleForm="ngForm" (ngSubmit)="performAction()">
      <time-input [(ngModel)]="timeInMyApp" name="time"></time-input>
      <button type="submit" [disabled]="!exampleForm.valid">
        perform action
      </button>
    </form>
    <pre>{{ timeInMyApp | json }}</pre>
  `
})
export class AppComponent {
  timeInMyApp: DayTime = { hours: 8, minutes: 30 };

  performAction() {
    console.log(this.timeInMyApp);
  }
}

To my surprise, when I ran the app, it was actually still functional (yeah, I was not so optimistic after all). However, the running app was accompanied by a pretty huge error message:

ERROR

Error: Uncaught (in promise): Error: No value accessor for form control with name: 'time'
Error: No value accessor for form control with name: 'time'
at _throwError [...]

So the form control with the name “time” (our time-input) has no value accessor. Or in other words: It does not implement the ngModel pattern the form suddenly expects our component to implement because it now has a ngModel attribute.

Implementing the ngModel pattern

In order to let your component support ngModel properly, we basically have to do two things:

  • Implement the ControlValueAccessor interface
  • Set the component as a NG_VALUE_ACCESSOR provider

Implementing the interface

The ControlValueAccessor interface has four methods our time-input component has to implement.

writeValue(obj: any): void { ... }

writeValue gets called by the form every time the ngModel value is getting changed from the outside. This means the method is being called at initialization (with null as value), then another time when AppComponent actually sets the value for the first time and after that simply every time AppComponent overrides its timeInMyApp field.

So basically, writeValue just swaps out our former @Input() time.

Next up is

registerOnChange(fn: any): void { ... }

This method takes callback functions (fn) and promises to call these functions as soon as the inner value of our TimeInputComponent changes. Therefore this method is a substitute for the @Output EventEmitter.

Then there is

registerOnTouched(fn: any): void { ... }

Like registerOnChange before, registerOnTouched also implements the classical event handler pattern by taking a callback function, (hopefully) storing every callback function, and call every single one of these functions as soon as the respective event occurs. In this case, we have to inform if our control has lost focus (is blurred).

Last but not least we have

setDisabledState?(isDisabled: boolean): void { ... }

This one should be pretty self-explanatory. If the outer component wants to disable our control, we have to pass this information through our component’s inner elements.

Set the component as provider for the NG_VALUE_ACCESSOR token

As mentioned in the error message above, our control is not recognized as a value accessor. ngForm sees the ngModel attribute and tries to get our TimeInputComponent class injected by querying the NG_VALUE_ACCESSOR token to have a component instance on which the methods I explained before can be accessed. To declare our component as a provider for NG_VALUE_ACCESSOR, we need to set the providers property of the @Component(...) annotation:

providers: [
        { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimeInputComponent), multi: true }
    ]

Ok, we’re setting a provider. But what do these properties mean?

  • provide: We’re providing something for the NG_VALUE_ACCESSOR token
  • useExisting: What we’re providing is our component (why useExisting with forwardRef instead of useClass could be an article on its own. I think this StackOverflow answer and this blog article by Thoughtram explain it quite well)
  • multi: We’re not the only ones who provide an implementation for NG_VALUE_ACCESSOR (every form control does that)

The whole shebang

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DayTime } from './typeDefinitions';

@Component({
  selector: 'time-input',
  template: `
    <input [ngModel]="value?.hours" name="hours" type="number" [disabled]="disabled" />
    <input [ngModel]="value?.minutes" name="minutes" type="number" [disabled]="disabled" />
    <button type="button" name="addHour" (click)="onAddHourButtonClicked()" [disabled]="disabled" (blur)="onBlur()">Add hour</button>
  `,
    providers: [
        { useExisting: forwardRef(() => TimeInputComponent), provide: NG_VALUE_ACCESSOR, multi: true }
    ]
})
export class TimeInputComponent implements ControlValueAccessor  {
  value: DayTime;
    
  writeValue(obj: DayTime) {
    this.value = obj;
  }

  private onChangeFunctions = [];
  registerOnChange(fn: (v: DayTime) => void) {
    this.onChangeFunctions.push(fn);
  }

  private onTouchedFunctions = [];
  registerOnTouched(fn: () => void) {
    this.onTouchedFunctions.push(fn);
  }

  disabled = false;
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onAddHourButtonClicked() {
    const newValue: DayTime = {
      hours: this.value.hours + 1,
      minutes: this.value.minutes
    };
    this.onChangeFunctions.forEach(fn => fn(newValue));
    this.value = newValue;
  }

  onBlur() {
    this.onTouchedFunctions.forEach(fn => fn());
  }
}

That’s how you make your component compatible with ngModel to be used as a control within a ngForm. Of course, the question arises: Is it worth all the effort? To be honest, right now, I’d say it isn’t. We have not gained any extra functionality so far because there are no validations for our control ngForm could react to. But you probably have already guessed it: That’s the topic of my next article in this series. So make sure you don’t miss it!

About the author

Domenic Helfenstein

Add comment

Recent Posts