Содержание
Angular 2 offers developers two technologies for building forms: template-driven forms and reactive forms. Both technologies are part of the @angular/forms
library and contain the same set of form control classes. At the same time, they offer different programming styles and techniques and refer to different modules: FormsModule
and ReactiveFormsModule
, respectively.
Reactive forms vs. template-driven forms
Reactive forms in Angular follow a reactive programming style that explicitly controls the flow of data between a data model that is isolated from the user interface and a form model that is directly connected to the user interface and stores the state and values of HTML elements. on-screen controls.
In reactive forms, we create a tree of objects in the component class that represent the form controls provided by Angular and bind them to the HTML controls in the component template as described below. Those. we will create and manipulate objects that represent form controls directly in the component class. Because the component class has direct access to both the data model and the form model, we can pass data model values to controls and retrieve user-edited values back. Thus, the component, as it were, observes changes in the controls and reacts to these changes.
Following the reactive programming paradigm, the component keeps the data model unchanged, interacting with it as with a direct source of initial values. Instead of changing the model directly, the component retrieves the changes made by the user and passes them to an external component or service, which in turn does some manipulation with these changes and returns a new data model to the original component, which represents the new changed state of the model.
Template-driven forms behave differently in this regard. We place HTML controls (such as <input>
or <select>
) in the component’s template and bind them to data model properties in the component using directives like ngModel
. We do not create objects that represent the form controls provided by Angular. Angular’s directives do it for us. We do not pass values from the model to the controls and vice versa, Angular again does it for us. Angular updates the mutable data model according to user changes as soon as they occur.
Reactive Form Component
Let’s create a reactive form component class:
1 2 3 4 5 | import { FormControl } from '@angular/forms'; export class ReactiveFormComponent { name = new FormControl(); } |
In the first line, we have imported the FormControl
directive, which allows you to directly create and manage an instance of FormControl
.
On line 4, we create a FormControl
instance named name
. It will be bound in the template to the HTML <input>
control. The FormControl
constructor can take three optional arguments: an initial value, an array of validators, and an array of asynchronous validators.
Now let’s create a component template:
1 | <input [formControl]="name"> |
Here, to bind the <input>
to the FormControl
instance, we use [formControl]="name"
.
And for all this to work, we need to import the ReactiveFormsModule
into our application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { ReactiveFormComponent } from './reactive-form.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent, ReactiveFormComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } |
Main classes of reactive forms
The main classes of reactive forms include:
AbstractControl
is an abstract base class for three derivatives:FormControl
,FormGroup
, andFormArray
. It implements their common methods and propertiesFormControl
keeps track of the value and validity of an individual control. It corresponds to HTML form controls such as<input>
or<select>
FormGroup
keeps track of the value and validity of a group ofAbstractControl
instances. Child controls are group properties. For example, the form in the component will be an instance ofFormGroup
FormArray
keeps track of the value and validity of an array ofAbstractControl
instances
FormGroup
Usually, several FormControls
are combined inside the parent FormGroup
:
1 2 3 4 5 6 7 8 | import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm = new FormGroup ({ name: new FormControl() }); } |
The component template will now look like this:
1 2 3 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> </form> |
We have placed a separate <input>
inside the <form>
element. The novalidate
attribute prevents the browser from performing standard HTML validation.
The formGroup
directive allows you to associate an existing FormGroup
instance with an HTML element.
As you can see, the syntax in the template has changed somewhat: if earlier, to associate a separate <input>
with an instance of FormControl
, we used [formControl]="name"
, now we use formControlName="name"
to associate an element inside the form. Those to bind individual controls we use the syntax [formControl]="name"
and to bind a control within a group (FormGroup
) we use formControlName="name"
.
FormBuilder
The FormBuilder
class provides a more concise syntax for creating form element objects, thereby reducing repetitive code and making it more readable.
1 2 3 4 5 6 7 8 9 10 11 12 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor (private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: '' }); } } |
FormBuilder.group
is a factory method that allows you to create a FormGroup
. As an argument, this method takes an object whose keys correspond to the names of the created FormControl
objects, and whose values correspond to their definitions.
In the example above, the name
object is initialized with an empty string as its initial value. An object definition can consist not only of its initial value, but also contain validators for the control. In this case, the value of the object will be an array: its first member is the initial value of the control, the second is the validator.
1 2 3 4 5 6 7 8 9 10 11 12 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor (private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ] }); } } |
The benefits of FormBuilder
are noticeable on a more expanded form.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; states = ['Russia', 'USA', 'Germany']; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], street: '', city: '', state: '', zip: '', sex: '', hasChildren: '' }); } } |
The template in this case would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
As you can see, the declaration of controls in the component class does not depend on their representation in the template (text, select, radio, checkbox). The way to bind them in the template is also the same.
FormControl
objects can be grouped into nested FormGroups
. This allows you to replicate the hierarchical structure of the data model, and makes it easier to keep track of the state and validity of a group of related controls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; states = ['Russia', 'USA', 'Germany']; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], address: this.fb.group({ street: '', city: '', state: '', zip: '' }), sex: '', hasChildren: '' }); } } |
The template in this case should also reflect the hierarchy of our model by wrapping the nested controls in a parent element (like a div):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <div formGroupName="address"> <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> </div> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
To access an individual FormControl
within a FormGroup
, you can use the .get()
method:
1 | reactiveForm.get('name').value |
To access nested properties, they must be listed separated by a dot:
1 | reactiveForm.get('address.street').value |
Properties and Methods of AbstractControl
AbstractControl
is the base class for FormControl
, FormGroup
and FormArray
. It includes the following properties and methods:
value
returns the value of the control (read-only)validator
control validatorasyncValidator
asynchronous validatorparent
returns the parent control (read-only)status
returns one of 4 possible control validation statuses:VALID
: The control passed all validation checksINVALID
: The control failed at least one validation checkPENDING
: The control is in the process of being validatedDISABLED
: element excluded from validation
All statuses are mutually exclusive. The property is read-only
valid
returnstrue
ifstatus === VALID
(read-only)invalid
returnstrue
ifstatus === INVALID
(read-only)pending
returnstrue
ifstatus === PENDING
(read-only)disabled
returnstrue
ifstatus === DISABLED
(read-only)enabled
returnstrue
ifstatus !== DISABLED
(read-only)errors
returns errors (aValidationErrors
object) generated if the control fails validation. If there are no errors, returnsnull
. Only for readingpristine
returnstrue
if the user has not yet changed the value of the control via the user interface (changing the value via code does not change this status). Only for readingdirty
oppositepristine
touched
returnstrue
if the field received and lost focus at least once (more precisely, if theblur
event fired on it). Only for readinguntouched
opposite oftouched
valueChanges
(observable) fires an event each time the control’s value changes (via the UI or programmatically) and returns the new valuestatusChanges
(observable) emits an event every time the field’s validation status changes and returns the new statusupdateOn
returns an event after which the control changes. This can be'change'
(default),'blur'
or'submit'
setValidators(newValidator)
allows you to set new synchronous validators for the field (overwrites the existing one)setAsyncValidators(newValidator)
allows you to set new asynchronous validators for the field (overwrites the existing one)clearValidators()
removes all synchronous field validatorsclearAsyncValidators()
removes all asynchronous field validatorsmarkAsTouched(opts)
marks the control and all of its children (if any) astouched
. Theopts
argument is an object containing a single property,onlySelf
, which, iftrue
, will not change the status of the child elements. You can pass an empty object.markAsUntouched(opts)
like above marks the element asuntouched
markAsDirty(opts)
marks an element asdirty
markAsPristine(opts)
marks an element aspristine
markAsPending(opts)
marks an element aspending
disable(opts)
makes the control unavailable. This means that its status will beDISABLED
, it will be excluded from validation, and its value will be excluded from the parent modelenable(opts)
is the opposite ofdisable(opts)
setParent(parent)
allows you to set the parent groupsetValue(value)
sets the value of the controlpatchValue(value)
makes partial changes to the value of the controlreset(value, options)
resets the control to its initial settings. Both parameters are optionalupdateValueAndValidity(opts)
recalculates the value and validity status for the control (and its ancestors)setErrors(errors, opts)
during manual validation allows you to set control errors (the field status also changes)get(path)
returns the child control (detailed above)getError(errorCode, path)
returns error data if the control with the specifiedpath
contains an error with the specifiederrorCode
. Otherwise, returnsnull
orundefined
. Ifpath
is not set, returns information for the current controlhasError(errorCode, path)
similar to the above method checks for an error and returnstrue
orfalse
root
returns the parent element of the root level
Data Model and Form Model
The relationship between the data model and the form model in reactive forms needs to be implemented by the developer himself. Unlike template-based forms, Angular won’t automatically do this. Changes made by the user through the user interface will only change the form model, not the data model. Form controls cannot change the data model. The structures of the form model and the data model may not even completely match.
Populating the form model with setValue and patchValue
You can set the values of controls not only during their initialization, but also later using the setValue
and patchValue
methods.
Using the setValue
method, we can set the values of all controls at once by passing an object as an argument to the method, which in structure must fully correspond to the form model inside the FormGroup
.
1 2 3 4 | this.reactiveForm.setValue({ name: 'Vasya', address: new Address() }); |
The setValue
method carefully checks the passed object before assigning values to the form controls. If the passed object differs in structure from the FormGroup
being filled in, or it lacks values for some controls within the group, then the setValue
method will not accept such an object. In this case, the method will throw an exception with a detailed description of the error (unlike patchValue
, which does not throw messages).
The argument of the setValue
method can also be a data model if it is completely identical in structure to the form model.
Using the patchValue
method, you can set the value of specific controls within a FormGroup
by passing an object to the method whose key/value pairs should match only the controls you want to insert.
1 2 3 | this.reactiveForm.patchValue({ name: 'Vasya' }); |
The patchValue
method gives us more freedom, but unlike the setValue method, it does not check the structure of the model and does not throw informative exceptions.
Form Reset
You can reset the form using the .reset()
method. The values of the controls are cleared, and the field statuses are set to the pristine
state.
1 | this.reactiveForm.reset(); |
You can pass two optional parameters to the .reset()
method, using them to set new values for controls, as well as statuses.
FormArray
Sometimes it becomes necessary to have an indefinite number of repeating fields or their groups in the form. So, in the example above, there can be zero, one, or more sets of fields representing an address. The FormArray
class just allows you to display an array of fields or their groups in a form.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import { Component, Input, OnChanges } from '@angular/core'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Address, Hero, states } from '../data-model'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], addresses: this.fb.array([]), sex: '', hasChildren: '' }); } } |
The template will undergo the following changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <div formArrayName="address"> <div *ngFor="let address of addresses.controls; let i=index" [formGroupName]="i" > <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> </div> </div> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
Here, first of all, we need to wrap the part of the template representing the address in an additional <div>
and apply the *ngFor
directive to this <div>
.
We then wrap the *ngFor
<div>
in another outer <div>
and set its formArrayName
directive to "addresses"
. This will set the FormArray
addresses as the context for the controls inside the repeating part of the template.
We use addresses.controls
as the source of duplicate addresses in the *ngFor
directive, not addresses
itself. It is the controls
property that is a reference to the array of fields.
Each repeated FormGroup
needs a unique formGroupName
directive value, which we use as its index in the array.
You can add new elements to the FormArray
using the .push(control)
methods (adds an element to the end of the array) and .insert(index, control)
(adds an element to the position corresponding to the index). The .removeAt(index)
method removes the element at the specified index.
Change Tracking
You can track changes in the form model using the valueChanges
property. It returns an RxJS Observable
.