При помощи форм наше веб-приложение может взаимодействовать с пользователем. Формы широко используются в веб: это и комментарии на страницах, формы для входа и регистрации, размещение заказов и пр.

В Angular используется два подхода для работы с формами:

  1. Template-driven forms (шаблонный подход). Как следует из названия, ключевую роль тут играет шаблон компонента.
  2. Reactive forms (реактивные формы) — это новый подход работы с формами в Angular.

Реактивные формы предоставляют чуть больше возможностей. В принципе, большинство того, что можно сделать в реактивных формах, можно сделать и с помощью шаблонного подхода, но при этом реактивный получится проще и быстрее. Также масштабируемость и расширение функционала у реактивных форм лучше и проще.

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

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

Для начала нам нужно изменить шаблон нашего файла:

Листинг 7.1 | Файл: 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" [class.text-center]="'text-center'">
        <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">
      <form (ngSubmit)="submitForm(form)" #form="ngForm">
        <div class="form-group">
          <label for="title">Название</label>
          <input type="text" id="title" class="form-control" ngModel name="title">
        </div>
        <div class="form-group">
          <label for="price">Цена</label>
          <input type="number" id="price" class="form-control" ngModel name="price">
        </div>
        <div class="form-group">
          <label for="quantity">Количество (шт.)</label>
          <input type="number" id="quantity" class="form-control" ngModel name="quantity">
        </div>
        <button class="btn btn-success" type="submit">Добавить</button>
      </form>
    </div>
  </div> <!-- /row -->
</div>

Основное, что мы тут добавили это форма. Каждому элементу input формы мы добавили директиву ngModel и атрибут name. Также самой форме добавили обработчик submitForm() события ngSubmit. В качестве параметра этому обработчику передается локальная переменная #form, которая содержит экземпляр директивы ngForm.

Если элементу формы не добавить директиву ngModel, то он значение этого поля не будет передано в форму. Немного позже вы увидите, что будет возвращать локальная переменная #form, мы ее выведем в консоль.

Также небольшие изменения есть в файле стилей, чтобы придать «немного красоты» нашей форме:

Листинг 7.2 | Файл: src/app/all-products/all-products.component.css
.add-new-item {
  background-color: #f8f8f8;
  padding: 20px;
}

И также добавим изменения в основной файл компонента:

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

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

  ...

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

  submitForm(form: NgForm) {
  	console.log(form);
  }
}

Тут мы импортируем модуль NgForm и описываем метод submitForm(). Пока он просто будет выводить в консоль данные нашей формы.

Следующий обязательный шаг — это подключение модуля FormsModule

Листинг 7.4 | Файл: src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } 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,
    FormsModule
  ],
  providers: [DataService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Итак, давайте в форму попробуем добавить новый товар и посмотрим, что у нас выводится в консоли:
ngForm console.log
Как видите, тут много различных свойств. Некоторые из них мы будем использовать для валидации формы. Самое простое, что сейчас можно сделать, это добавить атрибут required какому-либо из полей. Если это поле оставить пустым, то после сабмита свойство status: "INVALID", а также свойство valid: false
Attr required

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

Давайте всем полям нашей формы добавим атрибут required и будем добавлять атрибут disabled кнопке сабмита, если наша форма не валидна. Так как все данные о форме у нас хранятся в локальной переменной #form (а у нас это объект), соответственно все свойства нам доступны через эту переменную. Значит добавим [disabled]="!form.valid" — отрицание ! значит, что атрибут будет добавляться если свойство valid имеет значение false (т.е. не валидна)

Листинг 7.5 | Файл: src/app/all-products/all-products.component.html
...
<form (ngSubmit)="submitForm(form)" #form="ngForm">
  <div class="form-group">
    <label for="title">Название</label>
    <input type="text" id="title" class="form-control" ngModel name="title" required>
  </div>
  <div class="form-group">
    <label for="price">Цена</label>
    <input type="number" id="price" class="form-control" ngModel name="price" required>
  </div>
  <div class="form-group">
    <label for="quantity">Количество (шт.)</label>
    <input type="number" id="quantity" class="form-control" ngModel name="quantity" required>
  </div>
  <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
</form>
...

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

Angular присваивает элементам формы различные классы в зависимости от того валидные ли там данные, заполнял ли пользователь эти поля или нет и т.д. К примеру, нашему полю Название Angular добавляет классы ng-untouched ng-pristine ng-invalid — это значит что поле не тронуто и не валидно. После того как мы введем валидные данные, классы изменятся на ng-dirty ng-valid ng-untouched и как только мы убираем фокус с поля ввода классы поменяются на ng-dirty ng-valid ng-touched.

Input classes

Исходя из этого мы можем добавлять различные классы элементам формы, в зависимости от того успешна валидация или нет. Давайте добавим локальные переменные #title, #price, #quantity и передать им значение ngModel, они будут возвращать объект, похожий на объект формы тот, что мы выводили в консоль.

Листинг 7.6 | Файл: src/app/all-products/all-products.component.html
...
<form (ngSubmit)="submitForm(form)" #form="ngForm">
  <div class="form-group">
    <label for="title">Название</label>
    <input type="text" id="title" class="form-control" ngModel name="title" required #title="ngModel">
    <p *ngIf="title.invalid && title.touched" class="small">Поле обязательно для заполнения</p>
  </div>
  <div class="form-group">
    <label for="price">Цена</label>
    <input type="number" id="price" class="form-control" ngModel name="price" required #price="ngModel">
    <p *ngIf="price.invalid && price.touched" class="small">Поле обязательно для заполнения</p>
  </div>
  <div class="form-group">
    <label for="quantity">Количество (шт.)</label>
    <input type="number" id="quantity" class="form-control" ngModel name="quantity" required #quantity="ngModel">
    <p *ngIf="quantity.invalid && quantity.touched" class="small">Поле обязательно для заполнения</p>
  </div>
  <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
</form>
...

Далее добавим под каждым инпутом сообщение в случае если поле не проходит валидацию. Его мы будем выводить если в например price.invalid && price.touched, т.е. поле invalid и touched (отредактировано и фокус с поля убран).

И добавим изменения в файл стилей:

Листинг 7.7 | Файл: src/app/all-products/all-products.component.css
.add-new-item {
  background-color: #f8f8f8;
  padding: 20px;
}
input.ng-touched.ng-invalid {
  border: 1px solid #c00;
}

Результат:
Валидация формы Angular

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