Out of the box, Angular provides a way for you to put together forms that don't rely on the template hierarchy for definition. Instead, you can use FormBuilder
to explicitly define how you want to structure the form objects and then manually attach them to each input.
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 { constructor() {} saveArticle():void {} }
Your objective is to collect all of the form data and submit it using Angular's form constructs.
FormBuilder
is included in ReactiveFormsModule
, so you will need to import these targets into the application module:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {ReactiveFormsModule} from '@angular/forms'; import {ArticleEditorComponent} from './article-editor.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ ArticleEditorComponent ], bootstrap: [ ArticleEditorComponent ] }) export class AppModule {}
Additionally, you will need to inject it into your component to make use of it. In Angular 2, this can simply be accomplished by listing it as a typed constructor parameter. The FormBuilder
uses the group()
method to return the top-level FormGroup
, which you should assign to your component instance. For now, you will pass an empty object as its only argument.
With all this, you can integrate the articleGroup FormGroup
into the template by attaching it inside a form
tag using the formGroup
directive:
[app/article-editor.component.ts] import {Component, Inject} from '@angular/core'; import {FormBuilder, FormGroup} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form [formGroup]="articleGroup" (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 { articleGroup:FormGroup; constructor(@Inject(FormBuilder) formBuilder:FormBuilder) { this.articleGroup = formBuilder.group({}); } saveArticle():void {} }
With all this, you have successfully created the structure for your form, but FormGroup
is still not connected to the multiple input. For this, you will first set up the structure of the controls inside the builder and consequently attach them to each input
tag with formControlName
, as follows:
[app/article-editor.component.ts] import {Component, Inject} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form [formGroup]="articleGroup" (ngSubmit)="saveArticle()"> <p><input formControlName="title" placeholder="Article title"></p> <p><textarea formControlName="text" placeholder="Article text"></textarea></p> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { articleGroup:FormGroup; constructor(@Inject(FormBuilder) formBuilder:FormBuilder) { this.articleGroup = formBuilder.group({ title: [null, Validators.required], text: [null, Validators.required] }); } saveArticle():void { console.log(this.articleGroup); } }
With this, your form will have two FormControl
objects instantiated inside it, and they will be associated with proper input
elements. When you click on Submit, you will be able to see the input FormControls
inside FormGroup
. However, you may prefer to namespace these FormControl
objects inside an article
designation, and you can easily do this by introducing an ngFormGroup
and a corresponding level of indirection inside the formBuilder
definition:
[app/article-editor.component.ts] import {Component, Inject} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form [formGroup]="articleGroup" (ngSubmit)="saveArticle()"> <div formGroupName="article"> <p><input formControlName="title" placeholder="Article title"></p> <p><textarea formControlName="text" placeholder="Article text"></textarea></p> </div> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { articleGroup:FormGroup; constructor(@Inject(FormBuilder) formBuilder:FormBuilder) { this.articleGroup = formBuilder.group({ article: formBuilder.group({ title: [null, Validators.required], text: [null, Validators.required] }) }); } saveArticle():void { console.log(this.articleGroup); } }
Now, the title
and text FormControl
objects will exist nested inside an article FormGroup
and they can be successfully validated and inspected in the submit handler.
As you might suspect, the arrays living inside the formBuilder.group
definitions will be applied as arguments to a FormControl
constructor. This is nice since you can avoid the new FormControl()
boilerplate when creating each control. The string that keys the FormControl
is linked to it with formControlName
. Because you are using formControlName
and formGroupName
, you will need to have the formBuilder
nested structure match exactly to what is there in the template.
It is totally understandable that having to duplicate the structure in the template and the FormBuilder
definition is a little annoying. This is especially true in this case, as the presence of formGroup
doesn't really add any valuable behavior since it is attached to an inert div
element. Instead, you might want to be able to do this article namespace grouping without modifying the template. This behavior can be accomplished with formControl
, whose behavior is similar to formModel
(it binds to an existing instance on the component).
Note the paradigm that is being demonstrated with these different kinds of form directives. With things such as ngForm
, formGroup
, formArray
, and formControl
, Angular is implicitly creating and linking these instances. If you choose to not use FormBuilder
to define how FormControls
behave, this can be accomplished by adding validation directives to the template. On the other hand, you also have formModel
and formControl
, which bind to the instances of these control objects that you must manually create on the component.
[app/article-editor.component.ts] import {Component, Inject} from '@angular/core'; import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <form [formGroup]="articleGroup" (ngSubmit)="saveArticle()"> <p><input [formControl]="titleControl" placeholder="Article title"></p> <p><textarea [formControl]="textControl" placeholder="Article text"></textarea></p> <p><button type="submit">Save</button></p> </form> ` }) export class ArticleEditorComponent { titleControl:FormControl = new FormControl(null, Validators.required); textControl:FormControl = new FormControl(null, Validators.required); articleGroup:FormGroup; constructor(@Inject(FormBuilder) formBuilder:FormBuilder) { this.articleGroup = formBuilder.group({ article: formBuilder.group({ title: this.titleControl, text: this.textControl }) }); } saveArticle():void { console.log(this.articleGroup); } }
Importantly, note that you have created an identical output of the one you created earlier. title
and text
are bundled inside an article FormGroup
. However, the template doesn't need to have any reference to this intermediate FormGroup
.