Reactive forms in Angular 2+

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:

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:

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:

Main classes of reactive forms

The main classes of reactive forms include:

  • AbstractControl is an abstract base class for three derivatives: FormControl, FormGroup, and FormArray. It implements their common methods and properties
  • FormControl 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 of AbstractControl instances. Child controls are group properties. For example, the form in the component will be an instance of FormGroup
  • FormArray keeps track of the value and validity of an array of AbstractControl instances

FormGroup

Usually, several FormControls are combined inside the parent FormGroup:

The component template will now look like this:

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.

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.

The benefits of FormBuilder are noticeable on a more expanded form.

The template in this case would look like this:

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.

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):

To access an individual FormControl within a FormGroup, you can use the .get() method:

To access nested properties, they must be listed separated by a dot:

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 validator
  • asyncValidator asynchronous validator
  • parent returns the parent control (read-only)
  • status returns one of 4 possible control validation statuses:
    • VALID: The control passed all validation checks
    • INVALID: The control failed at least one validation check
    • PENDING: The control is in the process of being validated
    • DISABLED: element excluded from validation
      All statuses are mutually exclusive. The property is read-only
  • valid returns true if status === VALID (read-only)
  • invalid returns true if status === INVALID (read-only)
  • pending returns true if status === PENDING (read-only)
  • disabled returns true if status === DISABLED (read-only)
  • enabled returns true if status !== DISABLED (read-only)
  • errors returns errors (a ValidationErrors object) generated if the control fails validation. If there are no errors, returns null. Only for reading
  • pristine returns true 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 reading
  • dirty opposite pristine
  • touched returns true if the field received and lost focus at least once (more precisely, if the blur event fired on it). Only for reading
  • untouched opposite of touched
  • valueChanges (observable) fires an event each time the control’s value changes (via the UI or programmatically) and returns the new value
  • statusChanges (observable) emits an event every time the field’s validation status changes and returns the new status
  • updateOn 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 validators
  • clearAsyncValidators() removes all asynchronous field validators
  • markAsTouched(opts) marks the control and all of its children (if any) as touched. The opts argument is an object containing a single property, onlySelf , which, if true, will not change the status of the child elements. You can pass an empty object.
  • markAsUntouched(opts) like above marks the element as untouched
  • markAsDirty(opts) marks an element as dirty
  • markAsPristine(opts) marks an element as pristine
  • markAsPending(opts) marks an element as pending
  • disable(opts) makes the control unavailable. This means that its status will be DISABLED, it will be excluded from validation, and its value will be excluded from the parent model
  • enable(opts) is the opposite of disable(opts)
  • setParent(parent) allows you to set the parent group
  • setValue(value) sets the value of the control
  • patchValue(value) makes partial changes to the value of the control
  • reset(value, options) resets the control to its initial settings. Both parameters are optional
  • updateValueAndValidity(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 specified path contains an error with the specified errorCode. Otherwise, returns null or undefined. If path is not set, returns information for the current control
  • hasError(errorCode, path) similar to the above method checks for an error and returns true or false
  • 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.

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.

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.

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.

The template will undergo the following changes:

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.