Forms and custom input fields in Angular 2+

  • Tutorial
imageMy name is Pavel, I am a front-end developer at Tinkoff.ru. Our team is developing Internet banking for legal entities . The frontend of our projects was implemented using AngularJS, from which we switched, partly using Angular Upgrade , to the new Angular (previously positioned as Angular 2).

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

In this article, we will take a look “under the hood” of implementing forms in Angular and see 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, data binding and the basics of Angular in general, including working with forms, can be found here . There was already an article on Habrahabr about dependency injection 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 , that is controlled by a template (template-driven forms) and jet -driven model (model-driven / reactive forms) .

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

Template Shapes


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 ​​through [ngModel], two-way - through [(ngModel)], and also tracking changes through (ngModelChange):

The form is specified by the NgForm directive . This directive is created when we simply use a tag
or attribute ngForminside our template (without forgetting to include FormsModule).

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

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

The form can be structured by adding groups (which will be represented by objects in the form value) using the ngModelGroup directive:
...


After assigning the NgForm directive in any way, you can handle the send event by (ngSubmit):
...

Live example of a template form

Reactive forms


Reactive forms have earned their name because interaction with them is based on the paradigm of reactive programming .

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

You can only compose template field values ​​into objects. Arrays are also available in reactive - FormArray . Groups are represented by the FormGroup class . Both arrays and groups have the controls property, in which the controls are organized into the corresponding data structure.

Unlike a template form, it is not necessary to represent it in a template for creating and managing a reactive form, 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 the names of controls. If the values ​​are not controls, they will become the values ​​of the new form controls, which makes it convenient to create groups through FormBuilder. If they are, they will simply be added to the group. Array elements in the this.formBuilder.array method are processed in the same way.

To connect the control and the input field in the template, you need to pass the link to the control to the directives formGroup, formArray, formControl. These directives have “brothers”, for whom it is enough to pass a line 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 used together.

The root directive (in this case formGroup) must necessarily get a link to the control. For nested controls or even groups, we have the opportunity to do with names:

It is not necessary to repeat the structure of the form in the template. For example, if the input field is connected to the control via the formControl directive, it does not need to be inside an element with the formGroup directive.

The formGroup directive handles submit and sends out (ngSubmit)just like ngForm:
...

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

Now print the fields:

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

Change the value of the form - Observable , which you can subscribe to:
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. You can learn more about them in the descriptions of the corresponding classes.

Live example of a reactive form

Independent input fields


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

For the already created control of the reactive form, everything is quite simple. Template:

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

The input field for the template form is also independent:

In reactive forms, you can do this:

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

Live example of interacting with independent fields

The 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 a live example of a template form, there is work with an instance of the NgForm directive. We assign it to the local template variable formDir and refer to the value property to get the value. In the same way, we can get the group that the NgForm directive creates.
...
...
{{formDir.form.value | json}}

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 form entities in Angular - controls.

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

The 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 more easily and fully covered by unit tests than template ones. Our team decided to use only reactive forms.

A live example of the reactive nature of the template 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 input element with the model.

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

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

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. Upon initialization, each input field directive (ngModel, formControl or formControlName) receives all registered accessors. There can be several of them on one input field - user-defined and built-in in Angular. A custom accessor takes precedence over built-in ones, but there can only be one.

To register an accessor, a multiprovider 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 into methods as arguments. In order not to lose them, references to callbacks are written to the class properties. Initialization of control can occur later than creating an input field, so you need to write dummy functions to the properties in advance. The registerOnChange and registerOnTouched methods must overwrite them when called:
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 called. The onTouched function is called when the input field loses focus.

The writeValue method is called by the control every time its value is changed. The main objective of the method is to display the changes in the field. Keep in mind 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 every time the disabled state changes, so it is worth implementing it too.
setDisabledState(isDisabled: boolean) {
  this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}

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

In this way, work is organized with the input field in the DefaultValueAccessor directive , which applies to any, including regular, text input fields. If you want to make a component that works with a native input field inside itself, this is a necessary minimum.

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


I ’ll note a few points. The component template consists of one repeatable tag:

The values ​​array is needed for the directive to work correctly *ngForand is formed depending on the parameter maxRate(by default - 5).

Since the component does not have an internal input field, the value is stored simply 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 either a template or a reactive form:
@Input() disabled: boolean;
// ...
setDisabledState(disabled: boolean) {
  this.disabled = disabled;
}


Live example of a custom input field

Conclusion


In the next article, we will examine in detail 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 in my Telegram @ tmsy0 .

Also popular now: