The basic denominations of Angular forms are FormControl
, FormGroup
, and FormArray
objects. However, it is often not directly necessary to use these objects at all; Angular provides mechanisms with which you can implicitly create and assign these objects and attach them to the form's DOM elements.
The code, links, and a live example related to this recipe are available at .
Suppose you began with the following skeleton application:
[app/article-editor.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'article-editor', template: ` <p><input placeholder="Article title"></p> <p><textarea placeholder="Article text"></textarea></p> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { saveArticle():void {} }
Your objective is to collect all of the form data and submit it using Angular's form constructs.
You should begin by reorganizing this into an actual browser form. Angular gives you a lot of directives and components for this, and importing the FormsModule
will give you access to all the ones you need most of the time:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {FormsModule} from '@angular/forms'; import {ArticleEditorComponent} from './article-editor.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ ArticleEditorComponent ], bootstrap: [ ArticleEditorComponent ] }) export class AppModule {}
In addition, you should reconfigure the button so it becomes an actual submit
button. The handler should be triggered when the form is submitted, so you can reattach the listener to the form's native submit
event instead of the button's click
event. Angular provides an ngSubmit EventEmitter
on top of this event, so go ahead and attach the listener to this:
[app/article-editor.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'article-editor', template: ` <form (ngSubmit)="saveArticle()"> <p><input placeholder="Article title"></p> <p><textarea placeholder="Article text"></textarea></p> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { saveArticle():void {} }
Next, you should configure the form to pass the form data to the handler through a template variable.
The form element will have an NgForm
object (and inside this, a FormGroup
) automatically associated with it when you import FormsModule
into the encompassing module. Angular creates and associates the NgForm
instance behind the scenes.
One way you can access this instance is by assigning the ngForm
directive as a template variable. It's a bit of syntactical magic, but using #f="ngForm"
signals to Angular that you want to be able to reference the form's NgForm
from the template using the f
variable.
Once you declare the template variable, you are able to pass the ngForm
instance to the submit handler as an argument, specifically as saveArticle(f)
.
This leaves you with the following:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {NgForm} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form #f="ngForm" (ngSubmit)="saveArticle(f)"> <p><input placeholder="Article title"></p> <p><textarea placeholder="Article text"></textarea></p> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { saveArticle(f:NgForm):void { console.log(f); } }
When you test this manually, you should see your browser logging an NgForm
object every time you click on the Save button. Inside this object, you should see a shiny new FormGroup
and also the ngSubmit EventEmitter
that you are listening to. So far, so good!
You may have noticed that none of the form fields have been collected. This, of course, is because Angular has not been instructed to pay attention to them. For this, FormsModule
provides you with ngModel
, which will do certain things for you:
FormControl
object.ngModel
attribute.FormGroup
that the element lives inside and add to it the FormControl
it just created. The string value of the name attribute will be its key inside the FormGroup
.This last bullet is important, as attempting to use ngModel
without an encompassing form control construct to attach itself to will result in errors. This form control construct can be the form's FormGroup
itself, or it can even be a child FormGroup
instance.
With this, go ahead and add ngModel
to each of the text input fields:
import {Component} from '@angular/core'; import {NgForm} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form #f="ngForm" (ngSubmit)="saveArticle(f)"> <p><input ngModel name="title" placeholder="Article title"></p> <p><textarea ngModel name="text" placeholder="Article text"></textarea></p> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { saveArticle(f:NgForm):void { console.log(f); } }
Your form should now be fully functional. In the submit handler, you can verify that FormGroup
has two FormControl
objects attached to it by inspecting f.form.controls
, which should give you the following:
{ text: FormControl { ... }, title: FormControl { ... } }
In essence, you are using the hierarchical nature of the DOM to direct how your FormControl
architecture is structured. The topmost NgForm
instance is coupled with a FormGroup
; inside this, the rest of the form's FormControl
objects will reside.
Each ngModel
directs its referenced FormControl
to the FormGroup
owned by the NgForm
instance. With this nested structure now assembled, it is possible to read and reason the state of the entire form from the NgForm
object. This being the case, passing this object to the submit handler will allow you to manage every aspect of form inspection and validation.
If, instead, you wanted to group some of these fields together, this can be accomplished by simply wrapping them with an ngModelGroup
directive. Similar to ngModel
, this automatically instantiates a FormGroup
and attaches it to the parent FormGroup
; also, it will add any enclosed FormControl
or FormGroup
objects to itself. For example, refer to the following:
[app/article.component.ts] import {Component} from '@angular/core'; import {NgForm} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form #f="ngForm" (ngSubmit)="saveArticle(f)"> <div ngModelGroup="article"> <p><input ngModel name="title" placeholder="Article title"></p> <p><textarea ngModel name="text" placeholder="Article text"></textarea></p> </div> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { saveArticle(f:NgForm):void { console.log(f); } }
Now, inspecting f.form.controls
will reveal that it has a single FormGroup
keyed by article:
{ article: FormGroup: { controls: { text: FormControl { ... }, title: FormControl { ... } }, ... } }
Since this matches the structure you set up in the template, it checks out.