Reactive Forms - aakash14goplani/FullStack GitHub Wiki
-
Reactive Approach - you actually define the structure of the form in Typescript code, you also set up the HTML code and then you manually connect it. This gives you greater control over form, you can fine tune every little piece about your form.
-
It is dependent on
ReactiveFormsModule
, so import this inapp.module
fileimport { ReactiveFormsModule} from '@angular/forms'; ... imports: [ ReactiveFormsModule ]
-
Topics Covered:
-
I will create a new property which will hold my form in the end and name it
signupForm
which will be of typeFormGroup
inngOnInit()
. -
In a template driven approach, we had
ngForm
that was automatically creating wrapper for form similarly for Reactive approachFormGroup
creates the wrapper and provides a group of controls. Therefore, the overall form also is just a form group. -
You should initialize property before rendering the template, since property is of type
FormGroup
, default initialization should also be ofFormGroup
signupForm: FormGroup; ngOnInit() { this.signupForm = new FormGroup({ }); }
-
This is JavaScript object and we can configure it to have controls. Controls are basically just key-value pairs in this object we pass to the overall FormGroup. Controls are set using
FormControl
this.signupForm = new FormGroup({ username: new FormControl(null), email: new FormControl(null), gender: new FormControl('male') });
-
In
FormControl
constructor, we can pass a couple of arguments,- the first argument is the initial state, the initial/default value of this control.
- the second argument will be a single validator or an array of validators we want to apply to this control.
- the third argument will be potential asynchronous validators.
-
For now, I want to set an initial state for username and email to null to have an empty field but you could also pass a string like default username. For gender, lets set the default values as 'male'.
-
Actual form is in the HTML template, so we somehow need to synchronize our HTML inputs and our own form. Right now, Angular doesn't know which of our TypeScript controls here relates to which input in our template code, it actually doesn't even know that our form, signupForm here should be attached to this form.
-
Right now it is auto-detecting that this is a form and it creates a form for us. We don't want it to do that, so we have to add some directives to overwrite this default behavior to give Angular different instructions.
-
The first directive we need to add via property binding is the
formGroup
directive. This simply tells Angular, hey please take myformGroup
, don't infer one, don't create a form for me, use myformGroup
and we need to set up property binding here because we need to pass our form as an argument to the directive. So here we should reference oursignupForm
, the property we created here which stores our form. We're passing this via property binding to theformGroup
.<form [formGroup]="signupForm">
-
Now this form is actually synchronized with the form we created in TypeScript but we still need to tell Angular which controls should be connected to which inputs in the template code, for this we get another directive.
-
On this input, for example, the username, we add the
formControlName
directive to tell Angular, hey the name of this input in my TypeScript form is username, and that's the control I want to connect to this input, so I simply pass username here.<input type="text" id="username" formControlName="username" class="form-control">
-
If you're wondering why I'm not using property binding, I'm passing a string here. So if you want to use property binding, you can do this by wrapping this in square brackets and then enclosing the username in single quotation marks otherwise it would search for a property named username but this is overly complicated, if you just want to pass a string, simply omit the square brackets and you're good to go.
<input type="text" id="username" [formControlName]="'username'" class="form-control">
-
So with this we're telling Angular hey my form should be connected to the form stored in the signupForm property and in this form, this input here should be connected to the control with the name username.
-
In the template driven approach, we used
ngSubmit()
directive on form element. We still do the same here because we still want to react to this default submit event which is fired by HTML, by JavaScript. So we still addngSubmit()
here and we could execute anonSubmit()
method.<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
onSubmit(): void { console.log('signupForm: ', this.signupForm); }
-
The difference to the template driven approach is that we don't need to get the form via local reference, that actually wouldn't work anymore because we're not using Angular's auto-creation mechanism and we don't need to get this reference because we created the form on our own, we already got access to it here in our TypeScript code,
-
This is the cool thing about the reactive approach, whatever you set up here as an object you pass to the FormGroup which makes up your form, that is what you get out as a value of the form. So you can bind it to your own model of your application and easily make sure that the form structure matches the structure of your model.
-
This is how you can submit the form, you can still access the value as you did before (using
value
property) but now using your own form, the form you created in TypeScript.
-
In the template driven approach, we would simply add
required
attribute to make this field required. In the reactive approach, it doesn't work like this because you're not configuring the form in the template, you're only synchronizing it with the directivesformControlName
andformGroup
, but you're configuring it in the TypeScript code. -
That is why
FormControl
takes more than one argument in the constructor that allows you to specify some validators. So you can either only pass one validator or multiple validators in Array of Validators. Herevalidators
is build-in object imported from@angular/forms
which has couple of most frequently used validators.this.signupForm = new FormGroup({ username: new FormControl(null, Validators.required), email: new FormControl(null, [Validators.required, Validators.email]), gender: new FormControl('male') });
-
Note: Make sure to not call it, to not execute it, (i.e.
Validators.required()
) because you don't want to execute this method, it is a static method made available by validators, you only want to pass the reference to this method. Angular will execute the method whenever it detects that the input of thisFormControl
changed, it just needs to have a reference on what it should execute at this point of time.
-
As in a template driven approach, we can now use this form status to display helper/error messages but it works a bit differently because we access the controls differently.
-
In the template driven approach, we would use
NgModel
to get the reference but this doesn't work here because form is not set up viaNgModel
. -
Here we have a
get()
method that allows us to get access to our controls easily, here you can either specify the control name (e.g. username) or the path to the control (e.g. objA.objB.username). -
Along with
get()
method we use CSS classesng-touched
,ng-invalid
etc to display error/helper messages:<span *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched" class="help-block"> Please enter valid username! </span>
input.ng-invalid.ng-touched { border: 1px solid red; }
-
You can specify a path here (username => objA.username) because you might have a nested form. Let's say username and e-mail should be inside a
FormGroup
, so here, we could create aFormGroup
nameduserData
.FormGroup
is not only there to be used on the overall form, you can still have form groups in the form groups.this.signupForm = new FormGroup({ username: new FormControl(null, Validators.required), email: new FormControl(null, [Validators.required, Validators.email]), gender: new FormControl('male') });
CHANGES TO...
this.signupForm = new FormGroup({ userData: new FormGroup({ username: new FormControl(null, Validators.required), email: new FormControl(null, [Validators.required, Validators.email]) }), gender: new FormControl('male') });
-
We need to reflect this in our HTML template for that we need to update our synchronization and we easily do this by adding the
formGroupName
directive. Here, theformGroupName
isuserData
. One last change - We need to update get to point to the path or to contain the path to that username and that would beuserData.username
.<div formGroupName="userData"> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" formControlName="username" class="form-control"> <span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched" class="help-block"> Please enter valid username </span> </div> <div class="form-group"> <label for="email">Email</label> <input type="text" id="email" [formControlName]="'email'" class="form-control"> <span *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched" class="help-block"> Please enter valid email </span> </div> </div>
Allowing user to add dynamic input fields
Use Case: When the user clicks the button, I want to dynamically add a control to my form.
- The new field should be synonym to an array, for this we could use
FormArray
.FormArray
holds an array of controls, so you pass an array here to initialize it. In this array, you could already initialize some form controls with new FormControl or leave it empty to not have any hobbies at the beginning.
this.signupForm = new FormGroup({
...
hobbies: new FormArray([])
});
<div formArrayName="hobbies">
<h4>Your Hobbies</h4>
<button class="btn btn-default" (click)="onHobby()" type="button">Add Hobby</button>
<div class="form-group">
<!-- replicate this on every button click -->
<input type="text" class="form-control">
</div>
</div>
-
Now when we click
onAddHobby()
, I want to add a new hobby to that array. For that I need to access my form and I need to tell TypeScript that this is of type FormArray to not get an error and now I can push a new control on this array, if we would have not casted this, we would get an error.onHobby(): void { const hobbyControl = new FormControl(null, Validators.required); (this.signupForm.get('hobbies') as FormArray).push(hobbyControl); }
-
We've created this with no default value, you could also change this behavior to pass an argument to
onAddHobby()
and then pre-populate it in place ofnull
. -
We can also add validators. Finally we need to synchronize it with our HTML code. For this add a directive,
formArrayName
- this tells Angular that somewhere in thisdiv
, our array will live. -
Now I now somehow need to loop through all the controls which are in this array. I will add an
ngFor
loop to loop through all my hobby controls and I also want to extract the index of the current iteration, I will need this to assign this input to one of these dynamically created controls because on this input I want to add the form-controls CSS class and very important, I need to addformControlName
because we still need to synchronize this input with the dynamically created input. -
Now this dynamically created input will not have a name chosen by us but it is an array, so the name will simply be the index in this array, which is why I'm retrieving it here. So I can simply bind
formControlName
<div class="form-group" *ngFor="let hobbyControl of controls; let i = index"> <input type="text" class="form-control" [formControlName]="i"> </div>
In the following code:
*ngFor="let hobbyControl of signupForm.get('hobbies').controls; let i = index"
This code will fail as of the latest Angular version. You can fix it easily though. Outsource the "get the controls" logic into a method of your component code (the .ts
file):
getControls() {
return (<FormArray>this.signupForm.get('hobbies')).controls;
}
In the template, you can then use:
*ngFor="let hobbyControl of getControls(); let i = index"
Alternatively, you can set up a getter and use an alternative type casting syntax:
get controls() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
and then in the template:
*ngFor="let hobbyControl of controls; let i = index"
This adjustment is required due to the way TS works and Angular parses your templates (it doesn't understand TS there).
-
Use Case: We have some usernames we don't want to allow the user to use.
-
So now I want to create my own validator which checks whether the username the user entered is one of the two usernames it specified here in the array:
forbiddenUserNames: string[] = ['Aakash', 'Goplani'];
-
A validator in the end is just a function which gets executed by Angular automatically when it checks the validity of the
FormControl
and it checks that validity whenever you change that control. -
Now for a validator to work correctly needs to receive an argument which is the control it should check, so this will be of type FormControl, a validator also needs to return something for Angular to be able to handle the return value correctly, this will be a JavaScript object having any key which can be interpreted as a string and value that would be a boolean.
forbiddenNames(control: FormControl): {[s: string]: boolean} { ... }
-
So this function here should return something like let's say an object where we have name is forbidden, this would be the key name which is interpreted as a string and it could be true.
return {'nameIsForbidden': true};
-
If the username is forbidden, I want to return an object where I say name is forbidden, any short error code you want, is true. Now in the other case, I want to return null, if validation is successful, you have to pass null, you should not pass this object with false. This might sound counter-intuitive but that's just how it works, it should be null or you simply omit the return statement.
forbiddenNames(control: FormControl): {[s: string]: boolean} { if (this.forbiddenUserNames.indexOf(control.value) !== -1) { return {'nameIsForbidden': true}; } return null; }
-
Now want to assign this to username validators, so I'll change this appropriately and I will now add a reference to my
forbiddenNames()
function again, don't execute it, only pass a reference (Angular will execute this)username: new FormControl(null, [Validators.required, this.forbiddenNames])
-
Now if we save this, we get an error. Now this can be a tough one to spot, what's going wrong here? This error has something to do with the way JavaScript handles this.
- In
forbiddenNames()
, everything might look all right because I'm in this class and I access thisforbiddenUsernames
but think about who is calling theseforbiddenNames()
. - We're not calling it from inside this class, Angular will call it when it checks the validity, at this point of time, this will not refer to our class here.
- So to fix this, I actually need to
bind
this, the good old JavaScript trick to make sure that this refers to what we want it to refer to.
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)])
- In
-
To access the error code (
nameIsForbidden
) - -
Let's say we want to say the username is required if the field is empty and we want to say this is an invalid username if it is invalid. Also if form has
nameIsForbidden
code, it is invalid.<span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched" class="help-block"> <span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']" class="help-block">Invalid username!</span> <span *ngIf="signupForm.get('userData.username').errors['required']" class="help-block">Please enter valid username</span> </span>
-
This is how we can use these error codes and of course you could also use
ngSwitch
here or any other set up. The key thing is to understand that these error codes can be used to show the right error messages!
-
Use Case: Validator should process the input and return the response if it is valid or not
-
It may take some time for validator to process input data, meanwhile we cannot hangup on our customers and make them wait till status is displayed so we have to create an asynchronous validator.
-
Let's create one for email validation, I'll name it
forbiddenEmails
- This asynchronous validator takes the control as an argument.
- We also need to return something here but this will not be an object with an error code and a boolean, instead this will be a promise which wraps anything or an observable which wraps anything.
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> { ... }
-
Just to mimic that response in coming from server, in promise function, I now want to set a timeout, after few seconds I one to return a response and simulate asynchronous behavior.
-
If validation fails and as in the synchronous validator case, this is when I will return an object with a key-value pair with this error code i.e.
emailIsForbidden
and set this to true. -
If validation pass, so in that case that we have a valid input, that we have a valid e-mail address, I will simply resolve null and return this promise in the end.
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> { const promise = new Promise<any>((resolve, reject) => { setTimeout(() => { if (control.value === '[email protected]') { resolve({'emailIsForbidden': true}); } else { resolve(null); } }, 2000); }); return promise; }
-
Now we can add it to email validator, it is bit different from synchronous validators, here we make use of the third argument, this is an asynchronous validator or an array of such validators, just like the normal validators but reserved for the asynchronous ones.
- (again don't execute it, simply pass the reference and you need to bind this if you plan on using that in synchronous but since we are working with asynchronous argument, we can skip that)
email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
-
With Asynchronous Validators, the status of this input switches from invalid to pending to valid for example.
-
To track each state, you can listen to Observables, since Asynchronous Validators returns Observables. You have two observables you can listen to:
-
statusChanges
- this will be the status (valid/invalid/pending) of input field we're listening to. -
valueChanges
- this will be the value of input field we're listening to.
-
-
For both these Observables, If I type something in input field, with every keystroke, this Observable will emit value
this.signupForm.get('userData.email').valueChanges.subscribe( (value) => { console.log('email value changed: ', value); } ); this.signupForm.get('userData.email').statusChanges.subscribe( (status) => { console.log('email status changed: ', status); } );
<span class="help-block-pending" *ngIf="signupForm.controls.userData.controls.email.pending"> Checking Email Validity... </span> <span *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched && !signupForm.controls.userData.controls.email.pending" class="help-block"> Please enter valid email </span>
-
Not only can you listen to the updates in your form, you can also update the form on your own just like in the template driven approach,
setValue()
andpatchValue()
are there for you. -
You can on your form as a whole call
setValue()
and pass a JavaScript object which should now resemble the object structure up here (e.g. userData)this.signupForm.setValue({ userData: { username: 'Aakash', email: '[email protected]' }, gender: 'male', hobbies: ['cooking'] });
-
So if we do this, we should see that now our form is pre-populated with some values because we immediately call
setValue()
and of course you can also call this upon the click of a button. -
And as in the template driven approach, you also have
patchValue()
if you only want to update a part of the form, like for example change the hobby values(this.signupForm.get('hobbies') as FormArray).patchValue(['Game_1', 'Game_2']);
-
Just like template driven approach, in Reactive approach,
patchValue()
andsetValue()
are also available and the same is true forreset()
, so if we want to reset the form after submitting it, we can simply callreset()
.onSubmit(): void { console.log('signupForm: ', this.signupForm); this.signupForm.reset(); }
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()" #form="ngForm">
<div formGroupName="userData">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" formControlName="username" class="form-control">
<span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched" class="help-block">
<span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']" class="help-block">Invalid username!</span>
<span *ngIf="signupForm.get('userData.username').errors['required']" class="help-block">Please enter valid username</span>
</span>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" id="email" [formControlName]="'email'" class="form-control">
<span class="help-block-pending" *ngIf="signupForm.controls.userData.controls.email.pending">Checking Email Validity...</span>
<span *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched && !signupForm.controls.userData.controls.email.pending" class="help-block">Please enter valid email</span>
</div>
</div>
<!-- <div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" formControlName="username" class="form-control">
<span *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched" class="help-block">Please enter valid username</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input type="text" id="email" [formControlName]="'email'" class="form-control">
<span *ngIf="!signupForm.get('email').valid && signupForm.get('email').touched" class="help-block">Please enter valid email</span>
</div> -->
<div class="form-group">
<label for="gender">Gender</label>
<div class="radio" *ngFor="let item of genders">
<input type="radio" formControlName="gender" [value]="item">
<label>{{ item }}</label>
</div>
</div>
<div formArrayName="hobbies">
<h4>Your Hobbies</h4>
<button class="btn btn-default" (click)="onHobby()" type="button">Add Hobby</button>
<div class="form-group" *ngFor="let hobbyControl of controls; let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
<span *ngIf="!signupForm.get('hobbies').valid && signupForm.get('hobbies').touched" class="help-block">Please enter your hobbies</span>
</div>
<button class="btn btn-primary" type="submit" [disabled]="!signupForm.valid">Submit</button>
</form>
</div>
</div>
</div>
import { Component, OnInit, ViewChild, AfterViewChecked } from '@angular/core';
import { FormGroup, FormControl, NgForm, Validators, FormArray, AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs';
@Component({
selector: 'app-reactive-forms',
templateUrl: './reactive-forms.component.html',
styleUrls: ['./reactive-forms.component.css']
})
export class ReactiveFormsComponent implements AfterViewChecked, OnInit {
genders: string[] = ['male', 'female', 'others'];
nationalityArray = ['indian', 'american', 'african'];
signupForm: FormGroup;
@ViewChild('form', {static: false}) formData: NgForm;
forbiddenUserNames: string[] = ['Aakash', 'Goplani'];
constructor() { }
ngAfterViewChecked(): void {
this.addCheckboxes();
}
ngOnInit() {
this.signupForm = new FormGroup({
userData: new FormGroup({
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
}),
gender: new FormControl('male'),
/*
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email]),
*/
hobbies: new FormArray([])
});
this.signupForm.get('userData.username').valueChanges.subscribe(
(value) => {
console.log('username value changed: ', value);
}
);
this.signupForm.get('userData.email').statusChanges.subscribe(
(status) => {
console.log('email status changed: ', status);
}
);
this.signupForm.setValue({
userData: {
username: 'Aakash',
email: '[email protected]'
},
gender: 'male',
hobbies: ['cooking'],
nationality: []
});
(this.signupForm.get('hobbies') as FormArray).patchValue(['Game_1', 'Game_2']);
}
onHobby(): void {
const checkboxControl = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(checkboxControl);
}
get controls(): AbstractControl[] {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if (this.forbiddenUserNames.indexOf(control.value) !== -1) {
// tslint:disable-next-line: object-literal-key-quotes
return {'nameIsForbidden': true};
}
return null;
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value === '[email protected]') {
// tslint:disable-next-line: object-literal-key-quotes
resolve({'emailIsForbidden': true});
} else {
resolve(null);
}
}, 2000);
});
return promise;
}
onSubmit(): void {
console.log('signupForm: ', this.signupForm);
console.log('ngForm: ', this.formData);
this.signupForm.reset();
}
}
.container {
margin-top: 30px;
}
.help-block {
color: red;
}
input.ng-invalid.ng-touched {
border: 1px solid red;
}
input.ng-pending {
border: 2px solid yellow;
}
.help-block-pending {
color: blue;
}
input[type='radio'] {
margin-left: 0;
}
input[type="checkbox"] {
margin-right: 4px;
}
<div class="row">
<div class="card margin-b30">
<form [formGroup]="formData" (ngSubmit)="saveUpdates()">
<div class="card-header">Update {{ userToUpdate.displayName + '\'s' }} Details</div>
<div class="card-body">
<div class="card-text" formArrayName="user_data">
<ng-container *ngFor="let userDataControl of getUserDataControl(formData); let i=index" [formGroupName]="i">
<!-- update user role -->
<div class="form-group">
<label for="roleType">Update User Role:</label>
<select class="form-control" formControlName="access_level">
<option *ngFor="let type of roleTypes" [ngValue]="type">{{type}}</option>
</select>
</div>
<!-- update assignd projects -->
<div class="form-group" formArrayName="user_project_data">
<label for="roleType">Update Assigned Projects to User:</label>
<ng-container *ngFor="let projectDataControl of getProjectDataControl(userDataControl); let j=index" [formGroupName]="j">
<div class="checkbox">
<input type="checkbox" [formControl]="projectDataControl" (change)="getProjectValue()">
<label for="{{ userToUpdate.pid[j] }}">{{ userToUpdate.pid[j] }}</label>
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<div class="card-footer">
<button (click)="cancelUpdates()" class="btn btn-dark">Cancel</button>
<button type="submit" [disabled]="!formData.valid" class="btn btn-dark pull-right">Save</button>
</div>
</form>
</div>
</div>
@Input() userToUpdate: User;
formData: FormGroup;
selectedProjects: string[];
private userProjectDataControl: FormControl[];
readonly roleTypes: Array<string> = [
AppConstants.SYSTEM_ADMIN,
AppConstants.ADVANCE_USER,
AppConstants.READ_ONLY
];
ngOnInit(): void {
this.formData = new FormGroup({
user_data: new FormArray([
new FormGroup({
access_level: new FormControl(this.userToUpdate.accessLevel, Validators.required),
user_project_data: new FormArray(
this.userToUpdate.pid.map(project => {
return new FormControl(true); // every project should be selected
})
)
})
])
});
}
getUserDataControl(form) {
return form.controls.user_data.controls;
}
getProjectDataControl(form) {
this.userProjectDataControl = form.controls.user_project_data.controls;
/**
* this value is equivalent to:
* this.formData.controls.user_data['controls'][0]['controls'].user_project_data['controls']
*/
return form.controls.user_project_data.controls;
}
getProjectValue(): void {
this.selectedProjects = this.userProjectDataControl.map((project, i) => {
return project.value && this.userToUpdate.pid[i];
});
this.selectedProjects = this.selectedProjects.filter((project) => {
if (!!project) {
return project;
}
});
}
saveUpdates(): void {
...
this.getProjectValue();
...
}
cancelUpdates(): void {
...
}