Содержание
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:
AbstractControlis an abstract base class for three derivatives:FormControl,FormGroup, andFormArray. It implements their common methods and propertiesFormControlkeeps track of the value and validity of an individual control. It corresponds to HTML form controls such as<input>or<select>FormGroupkeeps track of the value and validity of a group ofAbstractControlinstances. Child controls are group properties. For example, the form in the component will be an instance ofFormGroupFormArraykeeps track of the value and validity of an array ofAbstractControlinstances
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:
valuereturns the value of the control (read-only)validatorcontrol validatorasyncValidatorasynchronous validatorparentreturns the parent control (read-only)statusreturns 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
validreturnstrueifstatus === VALID(read-only)invalidreturnstrueifstatus === INVALID(read-only)pendingreturnstrueifstatus === PENDING(read-only)disabledreturnstrueifstatus === DISABLED(read-only)enabledreturnstrueifstatus !== DISABLED(read-only)errorsreturns errors (aValidationErrorsobject) generated if the control fails validation. If there are no errors, returnsnull. Only for readingpristinereturnstrueif 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 readingdirtyoppositepristinetouchedreturnstrueif the field received and lost focus at least once (more precisely, if theblurevent fired on it). Only for readinguntouchedopposite oftouchedvalueChanges(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 statusupdateOnreturns 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. Theoptsargument 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 asuntouchedmarkAsDirty(opts)marks an element asdirtymarkAsPristine(opts)marks an element aspristinemarkAsPending(opts)marks an element aspendingdisable(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 specifiedpathcontains an error with the specifiederrorCode. Otherwise, returnsnullorundefined. Ifpathis not set, returns information for the current controlhasError(errorCode, path)similar to the above method checks for an error and returnstrueorfalserootreturns 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.
