Angular - валидация формы и POST запрос

Внимание

Данный материал является частью цикла статей «Frontend приложение на Angular». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Angular - Подготовка проекта для нового приложения
  2. Angular - навигация и маршрутизация
  3. Angular - HttpClient, сервисы и файлы окружения
  4. Angular - Ленивая загрузка данных (lazy loading)
  5. Angular - обработка ошибок
  6. Angular - декораторы и директивы @Input и @Output
  7. Angular - валидация формы и POST запрос
  8. Angular - Работа с запросами PUT
  9. Angular - Работа с DELETE запросами

Подготовка к созданию компонента owner-create

Начнем с создания нашего компонента внутри папки владельца. Для этого выполните команду Angular CLI:

ng g component owner/owner-create --skipTests

Затем мы собираемся изменить модуль владельца, добавив новый маршрут:

{ path: 'create', component: OwnerCreateComponent }

Когда мы щелкаем ссылку «создать» внутри файла owner-list.component.html, мы хотим, чтобы приложение направило нас на страницу создания.

Итак, давайте изменим тег <a> внутри файла owner-list.component.html:

<a [routerLink]="['/owner/create']">Create owner</a>

Валидация формы и модуль ReactiveFormsModule

Теперь мы можем начать писать код для создания нашей сущности и проверки формы. В Angular есть два типа проверки: проверка на основе шаблона и проверка реактивной формы. В нашем проекте мы собираемся использовать проверку активной формы, потому что HTML-файл легче читать. Кроме того, он не делает HTML-файл слишком «грязным» из-за слишком большого количества строк кода, и вся проверка выполняется в компоненте, что упрощает обслуживание.

Непосредственно перед изменением нашего компонента нам нужно изменить файл owner.model.ts. Давайте импортируем ReactiveFormsModule, потому что это модуль, который поддерживает проверку реактивной формы:

import { ReactiveFormsModule } from '@angular/forms';
imports: [
    CommonModule,
    SharedModule,
    ReactiveFormsModule,

Кроме того, нам нужно создать новый интерфейс:

export interface OwnerForCreation {
    name: string;
    dateOfBirth: string;
    address: string;
}

HTML-часть проверки угловой формы

Измените файл owner-create.component.html:

<div class="container-fluid">
    <form [formGroup]="ownerForm" autocomplete="off" novalidate (ngSubmit)="createOwner(ownerForm.value)">
      <div class="form-horizontal card card-body bg-light mb-2 mt-2">
   
        <div class="form-group row">
          <label for="name" class="control-label col-md-2">Name of the owner: </label>
          <div class="col-md-5">
            <input type="text" formControlName="name" id="name" class="form-control" />
          </div>
          <div class="col-md-5">
            <em *ngIf="validateControl('name') && hasError('name', 'required')">Name is required</em>
            <em *ngIf="validateControl('name') && hasError('name', 'maxlength')">Maximum allowed length is 60 characters.</em>
          </div>
        </div>
   
        <div class="form-group row">
          <label for="dateOfBirth" class="control-label col-md-2">Date of birth: </label>
          <div class="col-md-5">
            <input type="text" formControlName="dateOfBirth" id="dateOfBirth" class="form-control" appDatepicker 
                  (change)="executeDatePicker($event)" readonly/>
          </div>
          <div class="col-md-5">
            <em *ngIf="validateControl('dateOfBirth') && hasError('dateOfBirth', 'required')">Date of birth is required</em>
          </div>
        </div>
   
        <div class="form-group row">
          <label for="address" class="control-label col-md-2">Address: </label>
          <div class="col-md-5">
            <input type="text" formControlName="address" id="address" class="form-control" />
          </div>
          <div class="col-md-5">
            <em *ngIf="validateControl('address') && hasError('address', 'required')">Address is required</em>
            <em *ngIf="validateControl('address') && hasError('address', 'maxlength')">Maximum allowed length is 100 characters.</em>
          </div>
        </div>
   
        <br><br>
   
        <div class="form-group row">
            <div class="offset-5 col-md-1">
                <button type="submit" class="btn btn-info" [disabled]="!ownerForm.valid">Save</button>
            </div>
            <div class="col-md-1">
                <button type="button" class="btn btn-danger" (click)="redirectToOwnerList()">Cancel</button>
            </div>
        </div>
        
      </div>
    </form>
   
    <app-success-modal [modalHeaderText]="'Success message'" 
    [modalBodyText]="'Action completed successfully'" [okButtonText]="'OK'" 
    (redirectOnOK)="redirectToOwnerList()"></app-success-modal>
   
    <app-error-modal [modalHeaderText]="'Error message'" 
    [modalBodyText]="errorMessage" [okButtonText]="'OK'"></app-error-modal>
   
  </div>

Теперь давайте обратим внимание на этот код. В теге формы мы создаем formGroup с именем ownerForm. Эта группа форм содержит все элементы управления, которые нам нужно проверить в нашей форме. Более того, с (ngSubmit) мы вызываем функцию, когда пользователь нажимает кнопку отправки. В качестве параметра для этой функции мы отправляем значение ownerForm, которое содержит все элементы управления с данными, которые нам нужны для проверки.

Внутри каждого элемента управления есть атрибут formControlName. Этот атрибут представляет имя элемента управления, которое мы собираемся проверить внутри ownerForm, и это обязательный атрибут. Кроме того, в тегах <em> мы отображаем сообщения об ошибках, если они есть. Ошибки будут записаны на страницу только в том случае, если функции validateControl() и hasError() в результате вернут true.

Функция validateControl() проверяет, является ли элемент управления недействительным, а функция hasError() проверяет, какие правила проверки мы проверяем (обязательная, максимальная длина…). Обе функции validateControl и hasError являются нашими пользовательскими функциями, которые мы собираемся реализовать в файле компонента. Также есть кнопка отправки, которая будет отключена до тех пор, пока форма не станет действительной, и кнопка отмены, которая перенаправит пользователя из формы создания.

Дочерние модальные окна

Кроме того, мы импортируем наши дочерние модальные компоненты внутрь этой формы, чтобы отображать сообщения об успехах и ошибках. Как вы, возможно, помните из предыдущего поста, в модальном окне успеха был декоратор @Output с именем redirectOnOk. Этот декоратор @Output излучает EventEmmiter, и здесь мы подписываемся на него с привязкой к событию и назначаем ему функцию. Функция redirectToOwnerList() будет выполнена, как только пользователь нажмет кнопку ОК в модальном окне успеха.

Компонент валидации формы Angular

Давайте изменим файл owner-create.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { OwnerForCreation } from './../../_interfaces/owner-for-creation.model';
import { ErrorHandlerService } from './../../shared/services/error-handler.service';
import { RepositoryService } from './../../shared/services/repository.service';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-owner-create',
  templateUrl: './owner-create.component.html',
  styleUrls: ['./owner-create.component.css']
})
export class OwnerCreateComponent implements OnInit {
  public errorMessage: string = '';

  public ownerForm: FormGroup;

  constructor(private repository: RepositoryService, private errorHandler: ErrorHandlerService, private router: Router, private datePipe: DatePipe) { }

  ngOnInit() {
    this.ownerForm = new FormGroup({
      name: new FormControl('', [Validators.required, Validators.maxLength(60)]),
      dateOfBirth: new FormControl('', [Validators.required]),
      address: new FormControl('', [Validators.required, Validators.maxLength(100)])
    });
  }

  public validateControl = (controlName: string) => {
    if (this.ownerForm.controls[controlName].invalid && this.ownerForm.controls[controlName].touched)
      return true;

    return false;
  }

  public hasError = (controlName: string, errorName: string) => {
    if (this.ownerForm.controls[controlName].hasError(errorName))
      return true;

    return false;
  }

  public executeDatePicker = (event) => {
    this.ownerForm.patchValue({ 'dateOfBirth': event });
  }

  public createOwner = (ownerFormValue) => {
    if (this.ownerForm.valid) {
      this.executeOwnerCreation(ownerFormValue);
    }
  }

  private executeOwnerCreation = (ownerFormValue) => {
    const owner: OwnerForCreation = {
      name: ownerFormValue.name,
      dateOfBirth: this.datePipe.transform(ownerFormValue.dateOfBirth, 'yyyy-MM-dd'),
      address: ownerFormValue.address
    }

    const apiUrl = 'api/owner';
    this.repository.create(apiUrl, owner)
      .subscribe(res => {
        $('#successModal').modal();
      },
      (error => {
        this.errorHandler.handleError(error);
        this.errorMessage = this.errorHandler.errorMessage;
      })
    )
  }

  public redirectToOwnerList(){
    this.router.navigate(['/owner/list']);
  }

}

Давайте объясним этот код. Как только компонент монтируется, мы инициализируем нашу переменную FormGroup с именем ownerForm со всеми FormControls. Обратите внимание, что ключи в объекте ownerForm совпадают с именами в атрибуте formControlName для всех полей ввода в файле .html, что является обязательным. Более того, они имеют то же имя, что и свойства внутри объекта-владельца (адрес, дата рождения и имя).

При создании экземпляра нового элемента управления формы в качестве первого параметра мы предоставляем значение элемента управления, а в качестве второго параметра - массив Validators, который содержит все правила проверки для наших элементов управления.

В методе validateControl() мы проверяем, является ли текущий элемент управления недействительным (мы не хотим отображать ошибку, если пользователь не сделал этого). Кроме того, функция hasError() проверит, какое правило проверки нарушил текущий элемент управления.

В нашем элементе управления dateOfBirth есть директива appDatepicker, которая присоединяет указатель даты внутри этого элемента управления вводом. Выбор даты не изменит значение для этого элемента управления внутри ownerForm, поэтому нам нужно сделать это вручную внутри функции executeDataPicker().

Создание сущности и DatePipe

createOwner просто вызывает функцию, которая отправляет HTTP-запрос на сервер. Обратите внимание, что в этом примере использование канала даты не ограничивается только файлами HTML. Конечно, чтобы заставить его работать внутри компонента, нам нужно импортировать канал DatePipe и вставить его в конструктор. Нам также необходимо импортировать DatePipe внутри файла app.module.ts и поместить его в массив providers.

Если вы посмотрите на функцию redirectToOwnerList(), вы увидите знакомый код для возврата к предыдущему компоненту. Есть другой способ сделать это, импортировав Location из @angular/common и вставив его в конструктор, а затем просто вызвав функцию back() для этого внедренного свойства (location.back()) . То, что вы решите использовать, полностью зависит от вас.

В конце концов, мы извлекаем значения из ownerForm и отправляем POST-запрос на наш сервер.

Теперь просто измените наш корневой файл CSS (styles.css), чтобы отображать сообщения <em> красным цветом и жирным шрифтом, а также обернуть входные данные красным цветом, если они недопустимы:

em{
    color: #e71515;
    font-weight: bold;
}

.ng-invalid.ng-touched{ 
   border-color: red; 
}

Проверка результатов

Форма с обязательными ошибками:

Форма с дополнительными ошибками:

Действительная форма:

Владелец успешно создан:

Когда вы нажимаете кнопку "ОК", вы будете перенаправлены на страницу со списком владельцев, и новый владелец будет в списке.

В сервисе ErrorHandler мы собираемся изменить функцию handleOtherError:

private handleOtherError(error: HttpErrorResponse){
    this.createErrorMessage(error);
    $('#errorModal').modal();
  }

Теперь, если появляется ошибка, отличная от 500 или 404, мы собираемся показать пользователю модальное сообщение:

Заключение

Прочитав этот пост, вы узнали:

  • О различных типах проверки в Angular
  • Как использовать проверку активной формы в файле HTML
  • Как использовать проверку реактивной формы в файле компонента Angular
  • Способ создания новой сущности на стороне клиента

Спасибо, что прочитали этот пост, надеюсь, он был вам полезен.

В следующей статье мы собираемся написать часть обновления проекта, отправив запрос POST на наш сервер.