В отличии от шаблонного подхода, вся логика в реактивных формах хранится в JS-файле. Шаблонный подход остался как наследие от AngularJS, а начиная с Angular2 есть возможность использовать также и реактивный подход, который, по моему мнению, более удобный. Поэтому в нашей практической части, мы будем использовать именно его.

Подготовка формы

Вы можете скачать уже готовый проект:

Если же Вы предпочитаете вносить изменения самостоятельно, тогда приступим.

Сначала добавим стили, если вы не добавляли их в прошлой главе.

Листинг 8.1 | Файл: src/app/all-products/all-products.component.css

Теперь, добавим форму в файл шаблона.

Листинг 8.2 | Файл: src/app/all-products/all-products.component.html
<div class="container">
  <div class="row">
    <h3 class="col-md-12 mr-3 mt-2">{{ title }}</h3>
    <div class="col-md-12">
      <table class="table table-striped text-center mt-3">
        <thead [ngClass]="headClass()">
          <tr>
            <th>ID</th>
            <th>Название</th>
            <th>Цена</th>
            <th>Кол-во</th>
          </tr>
        </thead>
        <ng-container *ngFor="let product of products">
          <tr *ngIf="product.quantity > 0">
            <td>{{ product.id }}</td>
            <td>{{ product.name }}</td>
            <td [ngStyle]="getStyles(product.price)">${{ product.price }}</td>
            <td [ngSwitch]="product.quantity">
              <span class="badge" [ngClass]="quantityBagde(product.quantity)">{{ product.quantity }}</span><br>
              <span *ngSwitchCase="1">Остался последний</span>
              <span *ngSwitchCase="2">Осталось всего два</span>
              <span *ngSwitchCase="3">Осталось еще три</span>
              <span *ngSwitchCase="5">Товара достаточно</span>
              <span *ngSwitchDefault>Значение по умолчанию</span>
            </td>
          </tr>
        </ng-container>
      </table> <!-- /.table -->
    </div>
    
    <div class="col-md-12 mt-2 add-new-item">
      <h4 class="text-center">Добавление нового товара</h4><hr>
      <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="title">Название</label>
          <input type="text" id="title" class="form-control" formControlName="title">
        </div>
        <div class="form-group">
          <label for="price">Цена</label>
          <input type="number" id="price" class="form-control" formControlName="price">
        </div>
        <div class="form-group">
          <label for="quantity">Количество (шт.)</label>
          <input type="number" id="quantity" class="form-control" formControlName="quantity">
        </div>
        <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
      </form>
    </div> <!-- /add-new-item -->
  </div> <!-- /row -->
</div>

Тут необходимо тегу form задать атрибут [formGroup] со значением переменной с помощью связки. В нашем случае это будет переменная form, которая будет определена в файле компонента ниже. Имя переменной, в принципе, может быть любым. Также добавляем обработчик события (отправки формы) (ngSubmit)="onSubmit()". Ниже в листинге 8.4 мы добавим данный метод в файл компонента.

Директива formControlName предназначена для того, чтобы связать поля в шаблоне с теми полями формы, которые будут проинициализированны в файле компонента.

Для работы с реактивными формами, нам обязательно нужно добавить модуль ReactiveFormsModule из библиотеки @angular/forms в файле app.module.ts

Листинг 8.3 | Файл: src/app/app-module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { AllProductsComponent } from './all-products/all-products.component';
import { DataService } from './shared/data.service'

@NgModule({
  declarations: [
    AppComponent,
    AllProductsComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule
  ],
  providers: [DataService],
  bootstrap: [AppComponent]
})
export class AppModule { }

А также, давайте добавим изменения в файл компонента. Обратите внимание, что некоторые методы были удалены за ненадобностью.

Листинг 8.4 | Файл: src/app/all-products/all-products.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

import { DataService } from '../shared/data.service';

@Component({
  selector: 'app-all-products',
  templateUrl: './all-products.component.html',
  styleUrls: ['./all-products.component.css']
})
export class AllProductsComponent implements OnInit {

  title = 'Каталог товаров';

  products = [];

  constructor(private dataService: DataService) {}

  form: FormGroup;

  ngOnInit() {
  	this.products = this.dataService.products;

  	this.form = new FormGroup({
  		title: new FormControl(''),
  		price: new FormControl('100'),
  		quantity: new FormControl('1')
  	});
  }

  onSubmit() {
  	console.log(this.form);
  }

  headClass() {
  	return 'thead-dark';
  }

  quantityBagde(quantity) {
  	return {
  		'badge-success': quantity >= 3,
  		'badge-danger': quantity < 3
  	}
  }

  getStyles(price) {
  	return {
  		fontSize: '1.1rem',
  		'letterSpacing.px': 1,
  		backgroundColor: price > 300 ? 'green' : 'red'
  	}
  }
}

Итак, давайте рассмотрим какие изменения мы внесли в данном файле. Во первых, нам нужно импортировать FormControl и FormGroup. Определяем переменную form с типом FormGroup, эта же переменная была добавлена в файле шаблона у самой формы [formGroup]="form". При отправке формы мы получим в эту переменную объект, в котором будет содержаться информация о форме, данные полей и пр.

Для инициализации нашей формы нам нужно в методе ngOnInit создать новую форму, которая будет экземпляром класса new FormGroup. В конструкторе данного класса нам нужно определить все поля (контролы) нашей формы. По сути, мы должны передать объект, в котором как ключ будет передаваться имя поля (оно должно соответствовать значениям указанным в шаблоне, как атрибут formControlName — Листинг 8.2). Каждый контрол будет являться экземпляром класса FormControl.

В конструктор класса FormControl мы должны передать несколько параметров:
new FormControl('default', validator);
default — значение данного поля по умолчанию
validator(s) — это валидатор или массив валидаторов для данного поля.

Также давайте посмотрим какие данные мы получим при отправке формы:

FormGroup object

Также как и в шаблонном подходе, мы получаем объект с данными формы типа FormGroup. К примеру, мы в листинге 8.2 использовали свойство valid объекта form для того, чтобы отключить кнопку если форма не валидна.

Валидация форм (Angular Reactive Forms Validation)

Для валидации реактивной формы нам необходимо передать второй параметр в экземпляр класса FormControl. Как говорилось выше, тут можно передать один валидатор или массив валидаторов.

Самый простой способ это использование класса Validators, который нам нужно будет импортировать из библиотеки @angular/forms.

Листинг 8.5 | Файл: src/app/all-products/all-products.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { DataService } from '../shared/data.service';
...

Давайте рассмотрим какие методы имеет данный класс.

Метод Тип данных Описание
min (number) Валидатор, который требует, чтобы значение было «не меньше чем» или «равно» указанному значению
max (number) Требует, чтобы значение было «не больше чем» или «равно»
required Валидатор на заполнение поля. Если поле не заполнено, валидация не будет пройдена
email Проверка на правильность формата email’а
minLength (number) Проверка на минимальное количество символов
maxLength (number) Проверка на максимальное количество символов.
pattern (string | RegExp) Валидатор, который требует, чтобы значение поля соответствовало заданному регулярному выражению
nullValidator Валидатор, который не выполняет никаких операций.
compose Компонует несколько валидаторов в одну функцию
composeAsync Объединяет несколько асинхронных валидаторов в одну функцию

Давайте используем несколько методов этого класса для примера. Возьмем полю с названием добавим валидатор required, а полям с ценой и количеством — min и max соответственно.

Листинг 8.6 | Файл: src/app/all-products/all-products.component.ts
...
  ngOnInit() {
  	this.products = this.dataService.products;

  	this.form = new FormGroup({
  		title: new FormControl('', Validators.required),
  		price: new FormControl('100', Validators.min(1)),
  		quantity: new FormControl('1', Validators.max(100))
  	});
  }
...

Теперь давайте посмотрим, что будет если ввести невалидные данные в форму:
Reactive Forms Validation
Во-первых, наша кнопка «Добавить» будет disabled, так как не параметр form.valid будет иметь значение false. Второе, невалидные поля будут обведены красной рамкой и им будут присвоены классы form-control ng-dirty ng-invalid ng-touched, у валидных полей будут классы form-control ng-dirty ng-valid ng-touched

Описание классов проверки форм в Angular:

Класс Описание
ng-touched
ng-untouched
Эти классы назначаются полю в случае если поле было ‘затронуто‘ или ‘не затронуто‘. Затронутым поле будет считаться после того, как туда будет сначала помещен курсор, а затем убран фокус с этого поля.
ng-pristine
ng-dirty
ng-pristine назначается в случае, если элемент формы не был отредактирован пользователем. Как только вы будете редактировать элемент, класс сменится на ng-dirty и назад уже не изменится, даже если вы очистите поле.
ng-valid
ng-invalid
Эти классы назначаются в зависимости это того пройдена ли валидация или нет.

Теперь давайте полю с ценой добавим второй валидатор, т.е. это уже будет массив валидаторов:

Листинг 8.7 | Файл: src/app/all-products/all-products.component.ts
...
  ngOnInit() {
  	this.products = this.dataService.products;

  	this.form = new FormGroup({
  		title: new FormControl('', Validators.required),
  		price: new FormControl('100', [Validators.min(1), Validators.max(1500)]),
  		quantity: new FormControl('1', Validators.max(100))
  	});
  }
...

Также давайте выведем сообщение, если значение поля невалидно. Для этого нужно внести изменение в файл шаблона:

Листинг 8.8 | Файл: src/app/all-products/all-products.component.html
...
    <div class="col-md-12 mt-2 add-new-item">
      <h4 class="text-center">Добавление нового товара</h4><hr>
      <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="title">Название</label>
          <input type="text" id="title" class="form-control" formControlName="title">
        </div>
        <div class="form-group">
          <label for="price">Цена</label>
          <input type="number" id="price" class="form-control" formControlName="price">
          <p *ngIf="form.get('price').invalid && form.get('price').touched" style="color: red">Значение поля не валидно.</p>
        </div>
        <div class="form-group">
          <label for="quantity">Количество (шт.)</label>
          <input type="number" id="quantity" class="form-control" formControlName="quantity">
        </div>
        <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
      </form> 
    </div> <!-- /add-new-item -->
...

С помощью директивы *ngIf мы будем выводить сообщение в том случае, если значение поля price будет invalid и touched.

Группировка полей

Чтобы сгруппировать поля, нам нужно добавить объект этих полей в новый экземпляр класса FormGroup. Это удобно когда вы работаете с большими формами (регистрации, например), там где будет много полей, в таком случае их можно разбить на группы для более удобной работы.

К примеру, если мы хотим сгруппировать поля цены и количества в группу с названием details, нам нужно сделать следующее:

Листинг 8.9 | Файл: src/app/all-products/all-products.component.ts
...
  ngOnInit() {
  	this.products = this.dataService.products;

  	this.form = new FormGroup({
  		title: new FormControl('', Validators.required),
  		details: new FormGroup({
  			price: new FormControl('100', [Validators.min(1), Validators.max(1500)]),
  			quantity: new FormControl('1', Validators.max(100))
  		})
  	});
  }
...

А файле шаблона обернуть эти два поля в дополнительный div и для того, чтобы Angular понял, что это наша группа задать ему директиву formGroupName со значением details.

Листинг 8.10 | Файл: src/app/all-products/all-products.component.html
...
      <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="title">Название</label>
          <input type="text" id="title" class="form-control" formControlName="title">
        </div>
        <div formGroupName="details">
          <div class="form-group">
            <label for="price">Цена</label>
            <input type="number" id="price" class="form-control" formControlName="price">
            <p *ngIf="form.get('details.price').invalid && form.get('details.price').touched" style="color: red">Значение поля не валидно.</p>
          </div>
          <div class="form-group">
            <label for="quantity">Количество (шт.)</label>
            <input type="number" id="quantity" class="form-control" formControlName="quantity">
          </div>
        </div>
        <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
      </form> 
...

Также обратите внимание, что были сделаны изменения в строке, где мы выводим сообщение о валидации. Так как теперь нам нужно получить не просто поле price, а price как свойство объекта details. Дальше вы поймете почему.
Теперь давайте заполним форму и увидим, что получится при ее отправке:
formGroup

Как вы видите, значения полей нашей группы содержатся в отдельном объекте details.

Custom Validation

Angular нам предоставляет возможность помимо использования стандартных валидаторов класса Validators, также описать свою валидацию.

Для этого нам необходимо описать метод в файле компонента. Давайте, к примеру, сделаем валидатор, который будет проверять длину введенного тайтла:

Листинг 8.11 | Файл: src/app/all-products/all-products.component.ts
...
  ngOnInit() {
  	this.products = this.dataService.products;

  	this.form = new FormGroup({
  		title: new FormControl('', [Validators.required, this.checkLength]),
  		details: new FormGroup({
  			price: new FormControl('100', [Validators.min(1), Validators.max(1500)]),
  			quantity: new FormControl('1', Validators.max(100))
  		})
  	});
  }

  checkLength(control: FormControl) {
  	if (control.value.length <= 2) {
  		return {
  			'lengthError': true
  		}
  	}
  	return null;
  }
...

Мы описали метод checkLength, в котором как параметр принимаем control типа FormControl. В таком случае сюда попадет наше поле, дальше мы сможем получить его свойство value и получить длину строки, которая передана с помощью свойства lenght. Если это значение будет меньше или равно 2, значит нам нужно вернуть свойство 'lenghtError': true (имя этого свойства вы можете задать любое), или вернуть null, если длина строки больше 2.

Теперь нам нужно добавить этот метод-валидатор к нашему экземпляру класса FormControl (см. строку 25 в листинге 8.11).

Теперь, чтобы проверить что мы получаем в объект form давайте временно уберем у кнопки Добавить атрибут disabled, не вводя название отправим форму:
Custom Error
Тут мы видим, что у свойства title есть свойство-объект errors, у которого в свою очередь есть два свойства наших валидаторов — lengthError (наш кастомный валидатор) и required (стандартный валидатор Angular). Значения true означают, что оба валидатора вернули ошибку, т.к. мы отправили пустое поле Название.

Зная эту информацию мы можем добавить сообщение об ошибке, если будет введено недостаточное количество символов или поле будет совсем пустым (тогда выведем 2 ошибки).

В файле шаблона добавим следующие изменения:

Листинг 8.12 | Файл: src/app/all-products/all-products.component.html
...
      <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="title">Название</label>
          <input type="text" id="title" class="form-control" formControlName="title">
          <p *ngIf="form.get('title').invalid && form.get('title').touched">
            <span *ngIf="form.get('title').errors['required']">Поле не должно быть пустым! </span>
            <span *ngIf="form.get('title').errors['lengthError']">Название должно иметь больше 2х символов</span>
          </p>
        </div>
        <div formGroupName="details">
          <div class="form-group">
            <label for="price">Цена</label>
            <input type="number" id="price" class="form-control" formControlName="price">
            <p *ngIf="form.get('details.price').invalid && form.get('details.price').touched" style="color: red">Значение поля не валидно.</p>
          </div>
          <div class="form-group">
            <label for="quantity">Количество (шт.)</label>
            <input type="number" id="quantity" class="form-control" formControlName="quantity">
          </div>
        </div>
        <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
      </form>
...

Мы добавляем сообщение об ошибке в случае, если поле Название было «затронуто» пользователем, но при этом не проходит валидацию. Вторая проверка (в тегах span) будет для вывода отдельных сообщений об ошибке в случае если поле не заполнено совсем или название имеет меньше 2x символов.
Custom Validation

Ваши вопросы и комментарии:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *