Construirea unei aplicații unghiulare care implică multe forme poate fi stresantă. Mai ales atunci când trebuie să gestionați mesajele de validare de pe fiecare componentă.

O modalitate de a vă reduce stresul este să scrieți o clasă de validare generică care să gestioneze toate mesajele de validare.

Pe de o parte, acest lucru va reduce semnificativ codul din șablonul dvs. HTML. De asemenea, vă va oferi o sursă de mesaje de eroare, cu flexibilitatea de a suprascrie mesajul de eroare pe fiecare componentă

Pe de altă parte, implică scrierea unui pic mai mult de cod pe componentă și fișiere suplimentare

Dar cred că profesioniștii depășesc dezavantajele atunci când se ocupă de mai multe forme în diferite componente.

Condiții prealabile

  • Cunoștințe de bază despre Angular
  • Cunoașterea de bază a formelor reactive

Ce construim

Angular are două tipuri de forme: forme bazate pe șabloane și forme reactive. În acest post, ne vom concentra pe formele reactive.

Vom învăța cum să validăm un formular simplu de autentificare și înregistrare cu validare generică folosind un formular reactiv. Am folosit cadrul Bulma CSS pentru proiectare.

Valorile de introducere a formularului sunt doar jurnalul consolei atunci când faceți clic pe trimitere. Am făcut acest lucru, astfel încât să ne putem concentra în principal pe validarea formularului, dar puteți face orice doriți cu valorile de intrare a formularului.

Iată linkul demo pe Stackblitz.

Pasul 1: Configurare

Am creat un fișier de pornire pentru acest proiect cu toate HTML, CSS și Bulma realizate. Acest lucru ne permite să ne concentrăm mai mult pe validarea formularului generic. Clonați această repo pe GitHub aici.

Apoi, în terminalul dvs. executați această comandă:

git clone git@github.com:onwuvic/generic-reactive-form-validation.git

Sau puteți face acest lucru:

git clone https://github.com/onwuvic/generic-reactive-form-validation.git
cd generic-reactive-form-validation
git checkout starter
npm install
ng serve

Apoi, vizitați http: // localhost: 4200 / pe browserul dvs.

Deschideți folderul generic-reactive-form-validation din oricare dintre editorii dvs. Structura fișierului ar trebui să arate astfel:

Cum sa construiti un validator de formulare generic in unghiular
structura fișierului

Pasul 2: Importați ReactiveFormsModule

Acum, să importăm ReactiveFormsModule în modulul nostru de aplicații și adăugați-l la imports matrice.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './modules/login/login.component';
import { SignUpComponent } from './modules/sign-up/sign-up.component';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    SignUpComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
src / app / app.module.ts

Pasul 3: Creați o clasă de validare generică și un validator de confirmare a parolei

Clasa de validare generică

Să creăm un folder partajat în dosarul aplicației rădăcină. Apoi, în folderul partajat, creați un fișier generic-validator.ts. Scrieți următorul cod:

import { FormGroup } from '@angular/forms';

// Provide all set of validation messages here
const VALIDATION_MESSAGES = {
  email: {
    required: 'Required',
    email: 'This email is invalid'
  },
  password: {
    required: 'Required',
    minlength: 'The password length must be greater than or equal to 8'
  },
  confirmPassword: {
    required: 'Required',
    match: 'Password does not match'
  },
  firstName: {
    required: 'Required'
  },
  lastName: {
    required: 'Required'
  }
};

export class GenericValidator {
  // By default the defined set of validation messages is pass but a custom message when the class is called can also be passed
  constructor(private validationMessages: { [key: string]: { [key: string]: string } } = VALIDATION_MESSAGES) {}

  // this will process each formcontrol in the form group
  // and then return the error message to display
  // the return value will be in this format `formControlName: 'error message'`;
  processMessages(container: FormGroup): { [key: string]: string } {
    const messages = {};
    // loop through all the formControls
    for (const controlKey in container.controls) {
      if (container.controls.hasOwnProperty(controlKey)) {
        // get the properties of each formControl
        const controlProperty = container.controls[controlKey];
        // If it is a FormGroup, process its child controls.
        if (controlProperty instanceof FormGroup) {
          const childMessages = this.processMessages(controlProperty);
          Object.assign(messages, childMessages);
        } else {
          // Only validate if there are validation messages for the control
          if (this.validationMessages[controlKey]) {
            messages[controlKey] = '';
            if ((controlProperty.dirty || controlProperty.touched) && controlProperty.errors) {
              // loop through the object of errors
              Object.keys(controlProperty.errors).map(messageKey => {
                if (this.validationMessages[controlKey][messageKey]) {
                  messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' ';
                }
              });
            }
          }
        }
      }
    }
    return messages;
  }
}
src / app / shared / generic-validator.ts

În primul rând, importăm FormGroup. Putem scrie toate mesajele noastre de validare în acest fișier sau putem transmite fiecare mesaj de validare a formularului din componenta lor.

Fiecare proprietate de pe VALIDATION_MESSAGES obiect corespunde fiecărui nume de câmp de intrare sau formControlName. De asemenea, fiecare proprietate a câmpului de intrare corespunde numelui de validare de pe acesta. Valoarea sa este ceea ce doriți să afișați ca mesaj de eroare.

De exemplu, numele câmpului de intrare formControlName „e-mail” are validări „obligatorii” și „e-mail”.

În metoda constructorului, putem suprascrie mesajele de eroare implicite de la componenta în care se utilizează validarea noastră generică prin transmiterea mesajului de validare atunci când instanțiem clasa noastră de validare generică.

Metoda processMessages procesează fiecare câmp de introducere a formularului și returnează mesajul de eroare pentru afișare.

Validare confirmare parolă

Acum, să creăm un validator de confirmare a parolei pentru a verifica dacă parola noastră și confirmarea parolei se potrivesc.

În folderul partajat, creați un fișier password-matcher.ts fişier. Scrieți următorul cod:

import { AbstractControl } from '@angular/forms';

export class PasswordMatcher {
  static match(control: AbstractControl): void | null {
    const passwordControl = control.get('password');
    const confirmPasswordControl = control.get('confirmPassword');

    if (passwordControl.pristine || confirmPasswordControl.pristine) {
      return null;
    }

    if (passwordControl.value === confirmPasswordControl.value) {
      return null;
    }

    confirmPasswordControl.setErrors({ match: true });
  }
}
src / app / shared / password-matcher.ts

Pasul 4: Adăugați FormGroup și FormBuilder la fiecare componentă și șablon

Componenta formularului de înscriere

În interiorul aplicației / modulelor / înscrierii, adăugați codul de mai jos la componenta de înscriere:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

import { PasswordMatcher } from '../../shared/password-matcher';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.scss']
})
export class SignUpComponent implements OnInit {
  signupForm: FormGroup;

  // Use with the generic validation message class
  displayMessage: { [key: string]: string } = {};

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.signupForm = this.fb.group({
      firstName: ['', [Validators.required]],
      lastName: ['', [Validators.required]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      confirmPassword: ['', Validators.required]
    }, { validator: PasswordMatcher.match });
  }

  signup() {
    console.log('---form', this.signupForm.value);
  }

}
src / app / modules / sign-up / sign-up.component.ts

Avem validări încorporate angulare pentru fiecare câmp de intrare împreună cu personalizarea noastră PasswordMatcher validare pentru a se asigura că parola și parola confirmată se potrivesc.

Înscrieți-vă șablonul formularului

Acum să aruncăm o privire asupra șablonului formularului de înscriere:

<h1 class="title is-4">Sign Up</h1>
<p class="description">Let's get started!</p>
<form (ngSubmit)="signup()" [formGroup]="signupForm" novalidate autocomplete="false">
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.firstName}" formControlName="firstName" class="input is-medium" type="text" placeholder="First Name">
      <p *ngIf="displayMessage.firstName" class="help is-danger">
        {{ displayMessage.firstName }}
      </p>
    </div>
  </div>
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.lastName}" formControlName="lastName" class="input is-medium" type="text" placeholder="Last Name">
      <p *ngIf="displayMessage.lastName" class="help is-danger">
        {{ displayMessage.lastName }}
      </p>
    </div>
  </div>
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.email}" formControlName="email" class="input is-medium" type="email" placeholder="Email">
      <p *ngIf="displayMessage.email" class="help is-danger">
        {{ displayMessage.email }}
      </p>
    </div>
  </div>
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.password || displayMessage.confirmPassword }" formControlName="password" class="input is-medium" type="password" placeholder="Password">
      <p *ngIf="displayMessage.password" class="help is-danger">
        {{ displayMessage.password }}
      </p>
    </div>
  </div>
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.confirmPassword}" formControlName="confirmPassword" class="input is-medium" type="password" placeholder="Confirm Password">
      <p *ngIf="displayMessage.confirmPassword" class="help is-danger">
        {{ displayMessage.confirmPassword }}
      </p>
    </div>
  </div>
  <br>
  <button type="submit" class="button is-block is-primary is-fullwidth is-medium" [disabled]="signupForm.invalid">Submit</button>
  <br>
  <small class="has-text-centered">
    <em>
      Already have an account
      <a [routerLink]="['']" class="primary-color">Login</a>
    </em>
  </small>

</form>
src / app / modules / sign-up / sign-up.component.html
<form (ngSubmit)="signup()" [formGroup]="signupForm" novalidate autocomplete="false">

Am adăugat-o pe a noastră ngSubmit și formGroup la eticheta formular.

<input [ngClass]="{'is-danger': displayMessage.firstName}" formControlName="firstName" class="input is-medium" type="text" placeholder="First Name">

Am adăugat și noi formControlName la fiecare câmp de intrare. Dacă mesajul de afișare are un mesaj de eroare firstName, acesta va aplica ngClass is-danger câmpului de intrare.

<p *ngIf="displayMessage.firstName" class="help is-danger">
  {{ displayMessage.firstName }}
</p>

Aceasta afișează mesajul nostru de eroare.

<button type="submit" class="button is-block is-primary is-fullwidth is-medium" [disabled]="signupForm.invalid">Submit</button>

Dezactivăm butonul de trimitere dacă formularul nu este valid.

Componentă formular de autentificare

În interiorul aplicației / module / autentificare, adăugați codul de mai jos la componenta de autentificare:


import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit, AfterViewInit {
  loginForm: FormGroup;

  // Use with the generic validation message class
  displayMessage: { [key: string]: string } = {};
  private validationMessages: { [key: string]: { [key: string]: string } };

  constructor(private fb: FormBuilder) {
    // Defines all of the validation messages for the form.
    this.validationMessages = {
      email: {
        required: 'Required',
        email: 'This email is invalid'
      },
      password: {
        required: 'Required',
        minlength: 'The password length must be greater than or equal to 8'
      }
    };
  }

  ngOnInit() {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
    });
  }

  login() {
    console.log('---form', this.loginForm.value);
  }

}
src / app / modules / login / login.component.ts

Singura diferență aici cu componenta de înscriere este că vom suprascrie mesajul de eroare implicit din clasa noastră de validare generică cu mesajul de validare.

Șablon formular de autentificare

Scrieți următorul cod în șablonul de conectare:

<h1 class="title is-4">Login</h1>
<p class="description">Welcome back!</p>
<form (ngSubmit)="login()" [formGroup]="loginForm" novalidate autocomplete="false">
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.email}" class="input is-medium" type="email" placeholder="Email" formControlName="email">
      <p *ngIf="displayMessage.email" class="help is-danger">
        {{ displayMessage.email }}
      </p>
    </div>
  </div>
  <div class="field">
    <div class="control">
      <input [ngClass]="{'is-danger': displayMessage.password}" class="input is-medium" type="password" placeholder="Password" formControlName="password">
      <p *ngIf="displayMessage.password" class="help is-danger">
        {{ displayMessage.password }}
      </p>
    </div>
  </div>
  <button type="submit" class="button is-block is-primary is-fullwidth is-medium" [disabled]="loginForm.invalid">Login</button>
  <br>
  <small class="has-text-centered">
    <em>
      Don't have an account
      <a [routerLink]="['signup']" class="primary-color">Sign Up</a>
    </em>
  </small>
</form>
src / app / modules / login / login.component.html

Pasul 5: utilizați validarea generică în fiecare componentă

Validare generică la Înscriere

Adăugați următorul cod la sign-up.component.ts fişier:

import { Component, OnInit, ViewChildren, ElementRef, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControlName, AbstractControl } from '@angular/forms';
import { Observable, fromEvent, merge } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { GenericValidator } from '../../shared/generic-validator';
import { PasswordMatcher } from '../../shared/password-matcher';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.scss']
})
export class SignUpComponent implements OnInit, AfterViewInit {
  // Access every form input fields in our signup html file
  @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
  signupForm: FormGroup;

  // Use with the generic validation message class
  displayMessage: { [key: string]: string } = {};
  private genericValidator: GenericValidator;

  constructor(private fb: FormBuilder) {
    // Define an instance of the validator for use with this form,
    this.genericValidator = new GenericValidator();
  }

  ngOnInit() {
    this.signupForm = this.fb.group({
      firstName: ['', [Validators.required]],
      lastName: ['', [Validators.required]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      confirmPassword: ['', Validators.required]
    }, { validator: PasswordMatcher.match });
  }

  ngAfterViewInit(): void {
    // Watch for the blur event from any input element on the form.
    const controlBlurs: Observable<any>[] = this.formInputElements
      .map((formControl: ElementRef) => fromEvent(formControl.nativeElement, 'blur'));

    // Merge the blur event observable with the valueChanges observable
    merge(this.signupForm.valueChanges, ...controlBlurs).pipe(
      debounceTime(800)
    ).subscribe(value => {
      this.displayMessage = this.genericValidator.processMessages(this.signupForm);
    });
  }

  signup() {
    console.log('---form', this.signupForm.value);
  }

}
src / app / modules / sign-up / sign-up.component.ts

Aici am importat clasa de validare generică.

@ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];

Am adăugat @ViewChildren pentru a accesa fiecare câmp de introducere a formularului din fișierul nostru HTML de înscriere. Acest lucru ne ajută să ascultăm un eveniment despre ele.

private genericValidator: GenericValidator;

constructor(private fb: FormBuilder) {
   // Define an instance of the validator for use with this form
   this.genericValidator = new GenericValidator();
}

Instanțiem validarea generică în interiorul constructorului.

Apoi, implementăm interfața ngAfterViewInit.

ngAfterViewInit(): void {
   // Watch for the blur event from any 
   // input element on the form.
   const controlBlurs: Observable<any>[] = this.formInputElements
      .map((formControl: ElementRef) =>
         fromEvent(formControl.nativeElement, 'blur')
      );
   // Merge the blur event observable 
   // with the valueChanges observable
   merge(this.signupForm.valueChanges, ...controlBlurs)
    .pipe(debounceTime(800))
    .subscribe(value => {
      this.displayMessage = this.genericValidator
        .processMessages(this.signupForm);
   });
}

Aici urmărim evenimentul de estompare din orice element de intrare din formular.

const controlBlurs: Observable<any>[] = this.formInputElements
      .map((formControl: ElementRef) =>
         fromEvent(formControl.nativeElement, 'blur')
      );
merge(this.signupForm.valueChanges, ...controlBlurs)
    .pipe(debounceTime(800))
    .subscribe(value => {
      this.displayMessage = this.genericValidator
        .processMessages(this.signupForm);
   });

Acum am combinat modificările observabile ale valorii formularului (care se declanșează când se modifică oricare dintre valorile de intrare) și evenimentele de estompare ale oricărui câmp de intrare într-un singur observabil.

Deci, atunci când un utilizator modifică o valoare de intrare sau atinge orice câmp de intrare, această metodă de îmbinare se declanșează.

Apoi adăugăm o întârziere de 800 de milisecunde cu debounceTime(800). Acest lucru oferă utilizatorului timp pentru a face modificări înainte de a declanșa validarea.

În cele din urmă, primim mesajele de eroare afișate apelând metoda validatorului generic.

Validare generică la autentificare

Scrieți următorul cod în login.component.ts fişier:

import { Component, OnInit, ViewChildren, ElementRef, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControlName } from '@angular/forms';
import { Observable, fromEvent, merge } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { GenericValidator } from '../../shared/generic-validator';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit, AfterViewInit {
  // Access every form input fields in our login html file
  @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
  loginForm: FormGroup;

  // Use with the generic validation message class
  displayMessage: { [key: string]: string } = {};
  private validationMessages: { [key: string]: { [key: string]: string } };
  private genericValidator: GenericValidator;

  constructor(private fb: FormBuilder) {
    // Defines all of the validation messages for the form.
    this.validationMessages = {
      email: {
        required: 'Required',
        email: 'This email is invalid'
      },
      password: {
        required: 'Required',
        minlength: 'The password length must be greater than or equal to 8'
      }
    };
    // Define an instance of the validator for use with this form,
    // passing in this form's set of validation messages.
    this.genericValidator = new GenericValidator(this.validationMessages);
  }

  ngOnInit() {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
    });
  }

  ngAfterViewInit(): void {
    // Watch for the blur event from any input element on the form.
    const controlBlurs: Observable<any>[] = this.formInputElements
      .map((formControl: ElementRef) => fromEvent(formControl.nativeElement, 'blur'));

    // Merge the blur event observable with the valueChanges observable
    merge(this.loginForm.valueChanges, ...controlBlurs).pipe(
      debounceTime(800)
    ).subscribe(value => {
      this.displayMessage = this.genericValidator.processMessages(this.loginForm);
    });
  }

  login() {
    console.log('---form', this.loginForm.value);
  }

}
src / app / modules / login / login.component.ts

Singura diferență aici față de codul de înscriere este că anulăm mesajele de validare implicite cu noile noastre mesaje de validare specificate în această componentă. Apoi, îl trecem în clasa de validare generică atunci când o instanțiem.

  constructor(private fb: FormBuilder) {
    // Defines all of the validation messages for the form.
    this.validationMessages = {
      email: {
        required: 'Required',
        email: 'This email is invalid'
      },
      password: {
        required: 'Required',
        minlength: 'The password length must be greater than or equal to 8'
      }
    };
    // Define an instance of the validator for use with this form,
    // passing in this form's set of validation messages.
    this.genericValidator = new GenericValidator(this.validationMessages);
  }

Ne putem aștepta ca acest lucru să funcționeze la fel ca validarea generică de înscriere.

Și asta este tot ce aveți nevoie pentru a construi un validator generic în Angular.

Concluzie

Crearea unui validator generic face mai ușoară gestionarea validărilor de formulare multiple fără a utiliza o tonă de cod redundant în aplicația dvs. Angular.

Sper că acest articol ți-a fost de folos!

Vă puteți conecta cu mine pe LinkedIn și Stare de nervozitate.