📜 ⬆️ ⬇️

Forms and custom input fields in Angular 2+

image My name is Pavel, I am a frontend developer Tinkoff.ru. Our team is developing an online bank for legal entities . The frontend of our projects was implemented using AngularJS, from which we switched, in part using Angular Upgrade , to the new Angular (previously positioned as Angular 2).

Our product is intended for legal entities. Such a subject requires many forms with complex behavior. Input fields include not only standard ones implemented in browsers, but also fields with masks (for example, for phone input), fields for working with tags, sliders for entering numeric data, various drop-down lists.

In this article, we will look under the hood of the implementation of forms in Angular and figure out how to create custom input fields.
')
It is assumed that the reader is familiar with the basics of Angular, in particular, with data binding and dependency injection (links to official guides in English). In Russian with data binding and the basics of Angular as a whole, including working with forms, you can find it here . On Habrahabr, there was already an article about the introduction of dependencies in Angular, but you need to consider that it was written long before the release of the release version.

Introduction to Forms


When working with a large number of forms, it is important to have powerful, flexible and convenient tools for creating and managing forms.

The possibilities of working with forms in Angular are much wider than in AngularJS. Two types of forms are defined: template-based , that is, template-driven (template-based forms) and reactive , model-driven / model-reactive forms.

Detailed information can be obtained in the official guide . Here we analyze the main points, with the exception of validation, which will be discussed in the next article.

Template Forms


In template forms, the behavior of the field is controlled by the attributes set in the template. As a result, you can interact with the form in ways familiar from AngularJS .

To use template forms, you need to import the FormsModule module:
import {FormsModule} from '@angular/forms'; 

The NgModel directive from this module makes available for input fields one-way binding of values ​​via [ngModel] , two-sided through [(ngModel)] , and change tracking via (ngModelChange) :
 <input type="text" name="name" [(ngModel)]="name" (ngModelChange)="countryModelChange($event)" /> 

The form is specified by the NgForm directive . This directive is created when we simply use the <form></form> or the ngForm attribute inside our template (without forgetting to connect the FormsModule).

Input fields with NgModel directives that are inside the form will be added to the form and reflected in the form value .

The NgForm directive can also be assigned using the #formDir="ngForm" — in this way we will create a local variable for the formDir template that will contain an instance of the NgForm directive. Its value property, inherited from the AbstractControlDirective class, contains the value of the form. This may be necessary to obtain the value of the form (shown in the live example).

A form can be structured by adding groups (which will be represented by objects in the form value) using the ngModelGroup directive:
 <div ngModelGroup="address"> <input type="text" name="country" ngModel /> <input type="text" name="city" ngModel /> ... </div> 


After assigning the NgForm directive in any way, you can handle the send event via (ngSubmit) :
 <form #formDir="ngForm" (ngSubmit)="submit($event)"> ... </form> 

A live example of a template form.

Reactive forms


Reactive forms deserve their name for the fact that interaction with them is built on the paradigm of reactive programming .

The structural unit of the reactive form is the control - the model of the input field or a group of fields, the successor of the base class AbstractControl . The control of one input field ( form control ) is represented by the FormControl class.

Compose the values ​​of template form fields can only be in objects. In the reactive we also have arrays - FormArray . Groups are represented by the FormGroup class. Both arrays and groups have a controls property, in which the controls are organized into an appropriate data structure.

Unlike the template form, to create and manage a reactive one, it is not necessary to represent it in a template, which makes it easy to cover such forms with unit tests.

Controls are created either directly through the constructors, or using the FormBuilder tool .
 export class OurComponent implements OnInit { group: FormGroup; nameControl: FormControl; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.nameControl = new FormControl(''); this.group = this.formBuilder.group({ name: this.nameControl, age: '25', address: this.formBuilder.group({ country: '', city: '' }), phones: this.formBuilder.array([ '1234567', new FormControl('7654321') ]) }); } } 

The this.formBuilder.group method accepts an object whose keys will become control names. If the values ​​are not controls, they will become the values ​​of the new form controls, which makes it easy to create groups via FormBuilder. If they are, they will simply be added to the group. Array elements in this.formBuilder.array method are handled in the same way.

To link the control and the input field in the template, you need to pass a link to the control to the directives formGroup, formArray, formControl. These directives have “brothers” who just need to pass a string with the name of the control: formGroupName, formArrayName, formControlName.

To use reactive form directives, you must connect the ReactiveFormsModule module. By the way, it does not conflict with FormsModule, and directives from them can be applied together.

The root directive (in this case, the formGroup) should definitely get a link to the control. For nested controls or even groups, we have the opportunity to get by with the names:
 <form [formGroup]="personForm"> <input type="text" [formControl]="nameControl" /> <input type="text" formControlName="age" /> <div formGroupName="address"> <input type="text" formControlName="country" /> <input type="text" formControlName="city" /> </div> </form> 

The structure of the form in the pattern is not necessary to repeat. For example, if an input field is associated with control through a formControl directive, it does not need to be inside an element with a formGroup directive.

The formGroup directive handles submit and sends out (ngSubmit) in the same way as ngForm:
 <form [formGroup]="group" (ngSubmit)="submit($event)"> ... </form> 

Interaction with arrays in a template is a little different than with groups. To display an array, we need to get for each form control either its name or a link. The number of elements in the array can be any, so you have to *ngFor over it with the *ngFor directive. We write a getter to get an array:
 get phonesArrayControl(): FormArray { return <FormArray>this.group.get('phones'); } 

Now we will display the fields:
 <input type="text" *ngFor="let control of phonesArrayControl.controls" [formControl]="control" /> 

For an array of fields, the user sometimes requires add and delete operations. FormArray has corresponding methods, from which we will use deletion by index and insertion into the end of the array. The corresponding buttons and methods for them can be seen in a live example.

Changing the form value is an Observable to which you can subscribe:
 this.group.valueChanges.subscribe(value => { console.log(value); }); 

Each type of control has methods for interacting with it, both inherited from the AbstractControl class and unique. More information can be found in the descriptions of the respective classes.

Living example of reactive form

Independent input fields


The input field does not have to be linked to the form. We can interact with one field in much the same way as with the whole form.

For the already created reactive form control, everything is completely simple. Template:
 <input type="text" [formControl]="nameControl" /> 

In the code of our component, you can subscribe to its changes:
 this.nameControl.valueChanges.subscribe(value => { console.log(value); }); 

The template form input field is also independent:
 <input type="text" [(ngModel)]="name" /> 

In reactive forms, you can do this:
 <input type="text" [formControl]="nameControl" [(ngModel)]="name" /> 

Everything related to ngModel will be processed by the formControl directive , and the ngModel directive will not be used: the input field with the formControl attribute does not fall under the last selector .

Living example of interaction with independent fields.

Reactive nature of all forms


Template forms are not a completely separate entity. When creating any template form , a reactive one is actually created . In the live example of the template form, there is an operation with an instance of the NgForm directive. We assign it to the local variable of the formDir template and access the value property to get the value. In the same way, we can get the group that the NgForm directive creates.
 <form #formDir="ngForm" (ngSubmit)="submit($event)"> ... </form> ... <pre>{{formDir.form.value | json}}</pre> 

The form property is an instance of the FormGroup class. Instances of the same class are created when the NgModelGroup directive is assigned. The NgModel directive creates a FormControl .

Thus, all directives assigned to input fields, both “template” and “reactive”, serve as an auxiliary mechanism for interacting with the main entities of the forms in Angular - controls.

When creating a reactive form, we create the controls ourselves. If we work with a template form, directives assume this work. We can get access to the controls, but this way of interacting with them is not the most convenient. In addition, a template-based, template-based approach does not give complete control over the model: if we take control of the model structure, conflicts will arise. Nevertheless, it is possible to receive data from controls if necessary, and this is in the living example.

Reactive form allows you to create a more complex data structure than the template, provides more ways to interact with it. Also, reactive forms can be easier and more fully covered with unit tests than template ones. Our team decided to use only reactive forms.

A living example of a reactive nature of patterned form.

Form Interaction with Fields


Angular has a set of directives that work with most standard (browser) input fields. They are assigned invisibly to the developer, and it is thanks to them that we can immediately associate any element of input with the model.

When the possibilities of the required input field go beyond the standard, or the logic of its work requires reuse, we can create a custom input field.

First we need to get acquainted with the features of the interaction of the input field and control.

The controls, as mentioned above, are mapped to each input field explicitly or implicitly. Each control interacts with its field through its ControlValueAccessor interface.

ControlValueAccessor


ControlValueAccessor (in this text I will call it simply an accessor ) is an interface that describes the interaction of a field component with a control. At initialization, each input field directive (ngModel, formControl or formControlName) gets all registered accessors. There can be several of them on one input field - custom and embedded in Angular. The user accessor has priority over the built-in ones, but there can only be one.

To register an accessor, a multiplayer with the NG_VALUE_ACCESSOR token is used. It should be added to the list of providers of our component:
 @Component({ ... providers: [ ... { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputField), multi: true } ] }) export class CustomInputField implements ControlValueAccessor { ... } 

In the component, we must implement the registerOnChange, registerOnTouched, and writeValue methods, and we can also implement the setDisabledState method.

The registerOnChange, registerOnTouched methods register callbacks used to send data from the input field to the control. Callbacks themselves come to the methods as arguments. In order not to lose them, references to callbacks are recorded in class properties. The initialization of the control can occur later than the creation of the input field, so you need to pre-write dummy functions in the properties. The registerOnChange and registerOnTouched methods when calling should overwrite them:
 onChange = (value: any) => {}; onTouched = () => {}; registerOnChange(callback: (change: any) => void): void { this.onChange = callback; } registerOnTouched(callback: () => void): void { this.onTouched = callback; } 

The onChange function sends a new value to the control when it is called. The onTouched function is called when the input field loses focus.

The writeValue method is called by the control each time its value changes. The main task of the method is to display the changes in the field. Note that the value can be null or undefined. If there is a native field tag inside the template, Renderer is used for this (in Angular 4+ - Renderer2 ):
 writeValue(value: any) { const normalizedValue = value == null ? '' : value; this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue); } 

The setDisabledState method is called by the control each time the disabled state changes, so it should also be implemented.
 setDisabledState(isDisabled: boolean) { this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); } 

It is called only by the reactive form: in template forms for ordinary input fields, the binding is disabled . Therefore, if our component will be used in a template form, we need to additionally handle the disabled attribute.

Thus, work with the input field in the DefaultValueAccessor directive is applied, which applies to any, including ordinary, text input fields. If you want to make a component that works with a native input field within itself, this is the minimum necessary.

In the live example, I created the simplest implementation of a rating input component without a built-in native input field:


I will note a few points. The component template consists of one repeated tag:
 <span class="star" *ngFor="let value of values" [class.star_active]="value <= currentRate" (click)="setRate(value)"></span> 

The values ​​array is needed for the correct operation of the *ngFor directive and is formed depending on the maxRate parameter (by default - 5).

Since the component does not have an internal input field, the value is simply stored in the class property:
 setRate(rate: number) { if (!this.disabled) { this.currentRate = rate; this.onChange(rate); } } writeValue(newValue: number) { this.currentRate = newValue; } 


The disabled state can be assigned in both template and reactive form:
 @Input() disabled: boolean; // ... setDisabledState(disabled: boolean) { this.disabled = disabled; } 


A live example of a custom input field.

Conclusion


In the next article we will take a closer look at the statuses and validation of forms and fields, including custom ones. If you have questions about creating custom input fields, you can write in the comments or in person to my Telegram @ tmsy0 .

Source: https://habr.com/ru/post/323270/


All Articles