If you’re still guessing which method to use to update a Reactive Form value in Angular, then this post is for you.
It’s a comprehensive deep dive that will uncover the similarities and differences between patchValue
and setValue
. I’ve also highlighted key areas of the Angular source code for you whilst explaining the differences. This level of deep knowledge will ensure you’re using the right approach!
Table of contents
Reactive Form Setup
Let’s assume we’re setting up some kind of Event Feedback Form that first accepts our user credentials, followed by the event title and location.
For us to create a new Event Feedback Form is easy, as FormBuilder
will initialise specific values, but how would we set a form value should this component also be reused for displaying data already created and stored in the database.
First, assume the following form setup, in real life it would likely involve more form controls to get all the feedback for your particular event, however we’re merely diving into the APIs here to understand how to apply them to anything FormControl
related. If you’ve not used FormControl
, FormBuilder
and friends before I’d highly recommend checking out the aforementioned reactive forms article to understand what’s happening below.
Have a skim of the code and then we’ll progress below.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { SurveyService } from '../../services/survey.service';
@Component({
selector: 'event-form',
template: `
<form novalidate (ngSubmit)="onSubmit(form)" [formGroup]="form">
<div>
<label>
<span>Full name</span>
<input type="text" class="input" formControlName="name">
</label>
<div formGroupName="event">
<label>
<span>Event title</span>
<input type="text" class="input" formControlName="title">
</label>
<label>
<span>Event location</span>
<input type="text" class="input" formControlName="location">
</label>
</div>
</div>
<div>
<button type="submit" [disabled]="form.invalid">
Submit
</button>
</div>
</form>
`,
})
export class EventFormComponent implements OnInit {
form: FormGroup;
constructor(
public fb: FormBuilder,
private survey: SurveyService
) {}
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.required],
event: this.fb.group({
title: ['', Validators.required],
location: ['', Validators.required]
})
});
}
onSubmit({ value, valid }) {
this.survey.saveSurvey(value);
}
}
The usual suspects are present here, and we’re also introducing the SurveyService
to provide the saveSurvey
method inside the submit callback. So this is great, however let’s assume we have the following routes:
const routes: Routes = [{
path: 'event',
component: EventComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'new', pathMatch: 'full' },
{ path: 'new', component: EventFormComponent },
{ path: 'all', component: EventListComponent },
{ path: ':id', component: EventFormComponent },
]
}];
Specifically, the child route of /event
contains this:
{ path: ':id', component: EventFormComponent }
This will allow us to essentially achieve a URL such as this (with a unique id
hash):
localhost:4200/event/-KWihhw-f1kw-ULPG1ei
If you’ve used firebase before these keys will likely look somewhat familar. So let’s assume we just hit the above route, and want to update the form’s value. This can be done with a route resolve, however for these purposes - we’re not going to use one as we’ll be using an observable which will allow us to subscribe to route param changes and fetch new data and render it out.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
So let’s introduce the router code to the initial component. First we’ll import this:
import 'rxjs/add/operator/switchMap';
import { Observable } from 'rxjs/Observable';
import { Router, ActivatedRoute, Params } from '@angular/router';
We’re importing Observable
and adding switchMap
to ensure it’s available. From here we can inject the ActivatedRoute
inside the constructor:
constructor(
public fb: FormBuilder,
private survey: SurveyService,
private route: ActivatedRoute
) {}
Now we can jump back inside ngOnInit
and add a subscription:
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.required],
event: this.fb.group({
title: ['', Validators.required],
location: ['', Validators.required]
})
});
this.route.params
.switchMap((params: Params) => this.survey.getSurvey(params['id']))
.subscribe((survey: any) => {
// update the form controls
});
}
So anytime the route params change, we can use our getSurvey
method, pass in the current param in the URL (the unique :id
) and go fetch that unique Object. In this case, I’ve been using AngularFire2 which returns a FirebaseObjectObservable
, therefore I can pipe it through switchMap
and get the data through the subscribe
.
The next question: patchValue
or setValue
? Before using an API I’ve gotten into the good habit of looking through the source code, so let’s quickly run over the difference between the two:
patchValue
We’ll start with patchValue
and then move onto setValue
. Firstly “patch” sounds a bit off-putting, like it’s an API name that I shouldn’t really be using - but that’s not the case! Using patchValue
has some benefits over setValue
, and vice versa. These will become apparent after digging into the source…
There are actually two things happening when updating a
FormGroup
versusFormControl
, aspatchValue
has two implementations which we’ll look at below
So, the source code for the FormGroup
implementation:
patchValue(value: {[key: string]: any}, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
Object.keys(value).forEach(name => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
}
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
All this patchValue
really is, is just a wrapper to loop child controls
and invoke the actual patchValue
method. This is really the piece you need to be interested in:
Object.keys(value).forEach(name => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
}
});
Firstly, Object.keys()
will return a new Array collection of Object keys, for example:
const value = { name: 'Todd Motto', age: 26 };
Object.keys(value); // ['name', 'age']
The forEach
block that follows simply iterates over the FormGroup
keys and does a hash lookup using the name
(each string key) as a reference inside the current FormGroup
instance’s controls
property. If it exists, it will then call .patchValue()
on the current this.controls[name]
, which you might be wondering how does it call patchValue
on a single control
as we’re actually calling it from the FormGroup
level. It’s just a wrapper to loop and invoke model updates the child FormControl
instances.
Let’s loop back around before we get lost to understand the cycle here. Assume our initial FormGroup
:
this.form = this.fb.group({
name: ['', Validators.required],
event: this.fb.group({
title: ['', Validators.required],
location: ['', Validators.required]
})
});
All we have here really in Object representation is:
{
name: '',
event: {
title: '',
location: ''
}
}
So to update these model values we can reference our FormGroup
instance, this.form
and use patchValue()
with some data:
this.form.patchValue({
name: 'Todd Motto',
event: {
title: 'AngularCamp 2016',
location: 'Barcelona, Spain'
}
});
This will then perform the above loop, and update our FormControl
instances, simple!
So, now we’re caught up on the full cycle let’s look at the FormControl
specific implementation:
patchValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
this.setValue(value, options);
}
Ignoring all the function arguments and types, all it does is call setValue
, which - sets the value.
So, why use patchValue
? I came across the use case for this when I was also using firebase. I actually get $exists() {}
and $key
returned as public Object properties from the API response, to which when I pass this straight from the API, patchValue
throws no error:
this.form.patchValue({
$exists: function () {},
$key: '-KWihhw-f1kw-ULPG1ei',
name: 'Todd Motto',
event: {
title: 'AngularCamp 2016',
location: 'Barcelona, Spain'
}
});
It throws no errors due to the if
check inside the Object.keys
loop. Some might say it’s a safe $apply
, just kidding. It’ll allow you to set values that exist and it will ignore ones that do not exist in the current iterated control
.
setValue
So now we’ve checked patchValue
, we’ll look into setValue
. You may have guessed by now, that it’s a “more safe” way to do things. It’ll error for props that do not exist.
The FormGroup
implementation for setValue
:
setValue(value: {[key: string]: any}, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._checkAllValuesPresent(value);
Object.keys(value).forEach(name => {
this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
Just like before, we have the Object.keys
iteration, however before the loop the values are all checked a _checkAllValuesPresent
method is called:
_checkAllValuesPresent(value: any): void {
this._forEachChild((control: AbstractControl, name: string) => {
if (value[name] === undefined) {
throw new Error(`Must supply a value for form control with name: '${name}'.`);
}
});
}
This just iterates over each child control and ensures that the name
also exists on the Object by a lookup with value[name]
. If the control value does not exist on the Object you’re trying to setValue
, it will throw an error.
Providing your FormControl
exists, Angular moves onto the Object.keys
loop, however will first check that the control is missing for that value also via _throwIfControlMissing
:
_throwIfControlMissing(name: string): void {
if (!Object.keys(this.controls).length) {
throw new Error(`
There are no form controls registered with this group yet. If you're using ngModel,
you may want to check next tick (e.g. use setTimeout).
`);
}
if (!this.controls[name]) {
throw new Error(`Cannot find form control with name: ${name}.`);
}
}
First it’ll check if the this.controls
even exists, and then it’ll ensure - i.e. the FormControl
instances inside FormGroup
- and then it’ll check if the name
passed in even exists on the said FormControl
. If it doesn’t - you’re getting an error thrown at you.
If you’ve reached this far, the following gets invoked and your value is set:
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
Finally, we’ll check the source code of the individual FormControl
’s implementation of setValue
:
setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
this._value = value;
if (this._onChange.length && emitModelToViewChange !== false) {
this._onChange.forEach((changeFn) => changeFn(this._value, emitViewToModelChange !== false));
}
this.updateValueAndValidity({onlySelf, emitEvent});
}
This function alone doesn’t tell you anything of what’s happening internally as the changeFn
are dependent from elsewhere, depending on what code is using the setValue
internally. For instance, here’s how a changeFn
gets set via a public method (note the .push(fn)
being the changeFn
):
registerOnChange(fn: Function): void { this._onChange.push(fn); }
This will be from various other places from within the source code.
Looping back round again to updating our FormGroup
, we can make a quick setValue
call like so:
this.form.setValue({
name: 'Todd Motto',
event: {
title: 'AngularCamp 2016',
location: 'Barcelona, Spain'
}
});
This would then update the this.form
perfectly without errors, however when we invoke this next piece, the errors are thrown:
this.form.setValue({
$exists: function () {},
$key: '-KWihhw-f1kw-ULPG1ei',
name: 'Todd Motto',
event: {
title: 'AngularCamp 2016',
location: 'Barcelona, Spain'
}
});
Hopefully this answered a few questions on the differences between the two implementations.
FormControl patchValue / setValue
By diving through the source code we’ve also learned that you can call these methods directly to update particular FormControl
instances, for example:
this.survey.controls['account'].patchValue(survey.account);
this.survey.controls['account'].setValue(survey.account);
These are in the Angular docs, but the source code often makes more sense of what’s really happening.
Source code
If you’d like to dig through the source code yourself, check it out here.