⬆️ ⬇️

Reactive forms (reactive forms) Angular 5 (2+). Part 2





At the moment, Angular is one of the most popular and rapidly developing frameworks. One of its strengths is a large built-in toolkit for working with forms.



Reactive forms - a module that allows you to work with forms in a reactive style , creating a tree of objects in a component and associating them with a template, and gives you the opportunity to subscribe to a change in a form or a separate control from a component.

')

The first part dealt with how to start working with reactive forms. In this article, we consider the validation of forms, the dynamic addition of validation, the writing of our own synchronous and asynchronous validators.



Code examples are attached .



Beginning of work



To work, we write a reactive user creation form consisting of four fields:

- type (administrator or user);

- name;

- address;

- password.



Component:



import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; @Component({ selector: 'my-app', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { userForm: FormGroup; userTypes: string[]; constructor(private fb: FormBuilder) { } ngOnInit() { this.userTypes = ['', '']; this.initForm(); } private initForm(): void { this.userForm = this.fb.group({ type: null, name: null, address: null, password: null }); } } 


Template:



 <form class="user-form" [formGroup]="userForm"> <div class="form-group"> <label for="type"> :</label> <select id="type" formControlName="type"> <option disabled value="null"></option> <option *ngFor="let userType of userTypes">{{userType}}</option> </select> </div> <div class="form-group"> <label for="name"> :</label> <input type="text" id="name" formControlName="name" /> </div> <div class="form-group"> <label for="address"> :</label> <input type="text" id="address" formControlName="address" /> </div> <div class="form-group"> <label for="password"> :</label> <input type="text" id="password" formControlName="password" /> </div> </form> <hr/> <div>{{userForm.value|json}}</div> 


Add standard validators (work with them was described in the first part ):



 private initForm(): void { this.userForm = this.fb.group({ type: [null, [Validators.required]], name: [null, [ Validators.required, Validators.pattern(/^[A-z0-9]*$/), Validators.minLength(3)] ], address: null, password: [null, [Validators.required]] }); } 


Dynamic Addition of Validators



Sometimes it is necessary to check the field only under certain conditions. In reactive forms, you can add and remove validators using control methods.



Let's make the field “address” optional for the administrator and mandatory for all other types of users.



In the component create a subscription to change the user type:



 private userTypeSubscription: Subscription; 


Via the get method of the form, we will get the necessary control and subscribe to the valueChanges property:



 private subscribeToUserType(): void { this.userTypeSubscription = this.userForm.get('type') .valueChanges .subscribe(value => console.log(value)); } 


Add a subscription to ngOnInit after form initialization:



 ngOnInit() { this.userTypes = ['', '']; this.initForm(); this.subscribeToUserType(); } 


And unsubscribe in ngOnDestroy :



  ngOnDestroy() { this.userTypeSubscription.unsubscribe(); } 


Adding validators to the control is done with the setValidators method, and deleting with the clearValidators method. After validator manipulations, it is necessary to update the state of the control using the updateValueAndValidity method:



 private toggleAddressValidators(userType): void { /**   */ const address = this.userForm.get('address'); /**   */ const addressValidators: ValidatorFn[] = [ Validators.required, Validators.min(3) ]; /**   ,    */ if (userType !== this.userTypes[0]) { address.setValidators(addressValidators); } else { address.clearValidators(); } /**    */ address.updateValueAndValidity(); } 


Add the toggleAddressValidators method to the subscription:



 private subscribeToUserType(): void { this.userTypeSubscription = this.userForm.get('type') .valueChanges .subscribe(value => this.toggleAddressValidators(value)); } 


Creating a custom validator



The validator is a function , to the input of which the control is applied to which it is attached, an output of type ValidationErrors is returned at the output if the validation error is returned, and if the validation passes successfully, null is returned.



In addition to the validators provided by Angular, the developer has the opportunity to write a validator to fit your needs.



Create a password validator with a check on the following conditions:

- the password must contain capital letters;

- the password must contain capital letters;

- the password must contain numbers;

- The length must be at least eight characters.



 /**   */ private passwordValidator(control: FormControl): ValidationErrors { const value = control.value; /**     */ const hasNumber = /[0-9]/.test(value); /**      */ const hasCapitalLetter = /[AZ]/.test(value); /**      */ const hasLowercaseLetter = /[az]/.test(value); /**      */ const isLengthValid = value ? value.length > 7 : false; /**   */ const passwordValid = hasNumber && hasCapitalLetter && hasLowercaseLetter && isLengthValid; if (!passwordValid) { return { invalidPassword: '   ' }; } return null; } 


In this example, the text with a validation error is one, but if you wish, you can make several answers.



Add a validator in the form to the password:



 private initForm(): void { this.userForm = this.fb.group({ type: [null, [Validators.required]], name: [null, [ Validators.required, Validators.pattern(/^[A-z0-9]*$/), Validators.minLength(3)] ], address: null, password: [null, [ Validators.required, /**   */ this.passwordValidator] ] }); } 


You can access the validation control error using the getError method. Add an error display in the template:



 <div class="form-group"> <label for="password"> :</label> <input type="text" id="password" formControlName="password" /> </div> <div class="error" *ngIf="userForm.get('password').getError('invalidPassword') && userForm.get('password').touched"> {{userForm.get('password').getError('invalidPassword')}} </div> 


Creating an asynchronous validator



The asynchronous validator performs validation using server data. It is a function , the input of which is supplied to the control to which it is associated, returns Promise or Observable (depending on the type of HTTP request) with the type ValidationErrors on error and type null upon successful validation.



Check if the username is busy.



Let's create a service with a validation request (instead of an http request, we will return Observable with checking the user array specified in the service):



 import { Injectable } from '@angular/core'; import { ValidationErrors } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; @Injectable() export class UserValidationService { private users: string[]; constructor() { /** ,    */ this.users = ['john', 'ivan', 'anna']; } /**   */ validateName(userName: string): Observable<ValidationErrors> { /**     */ return new Observable<ValidationErrors>(observer => { const user = this.users.find(user => user === userName); /**     ,    */ if (user) { observer.next({ nameError: '     ' }); observer.complete(); } /**   ,    */ observer.next(null); observer.complete(); }).delay(1000); } } 


The delay method sets the response delay, emulating asynchrony.



Now in the component we will create the validator itself:



 /**   */ nameAsyncValidator(control: FormControl): Observable<ValidationErrors> { return this.userValidation.validateName(control.value); } 


In this case, the validator returns a method call, but if the server returns non-null in the case of validation, then the map method can be used for the Observable.



An asynchronous validator is added to the control description array by the third element:



 /**   */ private initForm(): void { this.userForm = this.fb.group({ type: [null, [Validators.required]], name: [null, [ Validators.required, Validators.pattern(/^[A-z0-9]*$/), Validators.minLength(3)], /**    */ [this.nameAsyncValidator.bind(this)] ], address: null, password: [null, [ Validators.required, this.passwordValidator] ] }); } 


The first part said that Angular adds css classes to the elements of the form. When using asynchronous validators, another css class appears - ng-pending , indicating that the response from the server for the validation request has not yet been received.



Add css styles to indicate that the validation request is being processed:



 input.ng-pending{ border: 1px solid yellow; } 


Loss of context validator



The function of the validator, regardless of whether it is synchronous or asynchronous, is only added to the control, and not called. The call occurs during validation outside the component, so the context is lost, and if this is used in the validator, then it will no longer point to the component, and an error will occur. You can save the context using the bind method, or by wrapping the validator in a switch function.



Links



The sample code is here .

More information can be obtained from the official documentation .

Anyone interested in Angular can join the Russian-speaking Angular community group in Telegram.

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



All Articles