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. ngFor
m 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 theNG_VALUE_ACCESSOR
tokenuseExisting
: What we’re providing is our component (whyuseExisting
withforwardRef
instead ofuseClass
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 forNG_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!
[…] Angular Component – Part 2: ngModel (Domenic Helfenstein) […]
[…] it for part 1 of this mini-series about angular components. Stay tuned for the second part, where we learn how and why to build our component compatible with the ngModel […]