Введение


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

В Angular существует 2 подхода к созданию форм:

Template-driven forms — подход, в котором ключевую роль играет шаблон компонента, и все описание производится в нем — этот подход является развитием работы с формами в AngularJS;

Reactive forms — новый подход для работы с формами в реактивном стиле. Описание формы происходит в компоненте в виде дерева объектов, после чего это дерево связывается с шаблоном. Все манипуляции (проверка валидности, подписка на изменение значения и прочее) производятся в компоненте, что делает работу более гибкой, удобной и предсказуемой.

В данной статье мы разберем, как начать работать с reactive forms на примере простой формы с валидацией и сообщениями об ошибках. Код примера.

Создание реактивной формы


Подключим ReactiveFormsModule в модуль, в котором будем использовать форму:

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

import { AppComponent } from './app.component';

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


В реактивных формах используются 3 типа блоков:

  • FormControl — одиночный контрол формы;
  • FormGroup — группа контролов формы;
  • FormArray — массив контролов формы.

Все они наследуются от Abstract Control.

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

Добавим в компонент формы FormBuilder и FormGroup:

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

@Component({
 selector: 'my-app',
 templateUrl: './app.component.html',
 styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit  {
 myFirstReactiveForm: FormGroup;

 constructor(private fb: FormBuilder){}
 ngOnInit(){}
}

Теперь опишем форму и инициализируем ее в ngOnInit:

export class AppComponent implements OnInit  {
 myFirstReactiveForm: FormGroup;

 constructor(private fb: FormBuilder){}

 ngOnInit(){  
  this.initForm();
 }

 /** Инициализация формы*/
 initForm(){
  this.myFirstReactiveForm = this.fb.group({
   name: ['Иван'],
   email: [null]
  });
 }
}

Данная форма состоит из двух контролов:

  • name со значением «Иван» при инициализации;
  • email без стартового значения.

Свяжем форму с шаблоном компонента через директивы formGroup и formControlName:

<form [formGroup]="myFirstReactiveForm">
  <label for="name">имя</label>
  <input type="text" id="name" formControlName="name" />
  <br/><br/>

  <label for="email">email</label>
  <input type="text" id="email" formControlName="email" />
  <br/><br/>

  <button type="submit">отправить</button>
</form>

Тут нужно обратить внимание на то, что formControlName принимает имя строкой и пишется без [ ].

Данные формы мы можем получить в компоненте в виде объекта через свойство value и вывести их в шаблон через jsonPipe (на данном этапе это необходимо для проверки работоспособности):

<div>
 {{myFirstReactiveForm.value | json}}
</div>

Валидация и подсветка не валидных контролов


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

Мы используем следующие:

  • Validators.required — делает контрол обязательным для заполнения;
  • Validators.email — валидация эл. адреса;
  • Validators.pattern — валидация по регулярному выражению.

Импортируем валидаторы из angular/forms в компонент:

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

Добавим их в описание контролов формы:

this.myFirstReactiveForm = this.fb.group({
 name: ['', [
   Validators.required,
   Validators.pattern(/[А-я]/)
  ]
 ],
 email: ['', [
   Validators.required, Validators.email
  ]
 ]
});

На все контролы формы Angular динамически добавляет парные CSS классы в зависимости от определенных условий:

  • ng-invalid/ng-valid — меняется в зависимости от валидности контрола;
  • ng-pristine/ng-dirty — контрол считается dirty, если в нем хотя бы раз менялось значение;
  • ng-untouched/ng-touched — контрол считается touched при первой потере фокуса.

В CSS добавим следующие стили:

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

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

Вывод сообщения об ошибке


Получить доступ к контролу в компоненте можно следующим образом:

this.myFirstReactiveForm.controls[controlName]

Проверим свойства invalid и touched.

Полный список свойств и методов контрола смотреть здесь.

Добавим в компонент метод для проверки валидности контрола, который принимает на вход имя контрола и возвращает true/false:

isControlInvalid(controlName: string): boolean {
const control = this.myFirstReactiveForm.controls[controlName];

 const result = control.invalid && control.touched;

 return result;
}

В шаблоне добавим под контролом div с сообщением об ошибке, который будет отображаться по *ngIf, если контрол не валидный:

<label for="name">имя</label>
<input type="text" id="name" formControlName="name" />
<div class="error" *ngIf="isControlInvalid('name')">
 Имя должно состоять только из русских букв
</div>

Для вывода разных ошибок (в зависимости от условий) можно воспользоваться библиотекой ngx-errors.

Отправка формы


Добавим в компонент метод onSubmit:

onSubmit() {
const controls = this.myFirstReactiveForm.controls;

 /** Проверяем форму на валидность */ 
 if (this.myFirstReactiveForm.invalid) {
  /** Если форма не валидна, то помечаем все контролы как touched*/
  Object.keys(controls)
   .forEach(controlName => controls[controlName].markAsTouched());
   
   /** Прерываем выполнение метода*/
   return;
  }

 /** TODO: Обработка данных формы */
 console.log(this.myFirstReactiveForm.value);
}

Если форма не валидна, через foreach помечаем все контролы как touched для подсветки ошибок и прерываем выполнение метода. В противном случае обрабатываем данные формы.

Добавим обработчик события submit в шаблон:

<form [formGroup]="myFirstReactiveForm" (submit)="onSubmit()">

Форма готова!

Заключение


В следующей части разберем реактивную работу с формами, а именно:
подписку на событие изменения контрола;
динамический сброс и блокировку зависимых контролов;
динамическое добавление и удаление контролов и групп контролов в форму.

Ссылки


Код примера смотреть здесь.
Более подробную информацию можно получить из официальной документации.
Все интересующиеся Angular могут присоединяться к группе русскоговорящего Angular сообщества в Telegram.

Комментарии (13)


  1. shuron
    09.01.2018 00:44
    +1

    В чем таки достоинства Reactive forms в сравнении с Template-driven?
    Template-driven вариант мне кажется более интуитивным и простым. Может быть это связано с тем что я не видел каких-то особых ситуаций ( Я во фронтэнде только набегами, раз в пятилетку, на ангуляре первый серьезный проект)? Мог-бы кто-то пролить свет на это?


    1. klimentRu Автор
      09.01.2018 07:48

      В официальной документации написано, что это просто 2 разных подхода со своими плюсами и минусами. Можно прочитать ne.

      При работе с большими и сложными формами реактивный подход удобнее по следующим причинам:

      • Не нужно создавать для каждого контрола свойство. Все находится в дереве формы;
      • Вся логика содержится в одном компоненте. Нет «размытия» по шаблону;
      • Легче производить манипуляции с контролами за счет встроенных методов;
      • Подписка на изменение контрола происходит не через обработчик превязанный к шаблону, а через подписку(subscribe);
      • Легче тестировать.


      1. Vadem
        09.01.2018 22:26

        Не нужно создавать для каждого контрола свойство. Все находится в дереве формы;

        Разве в случае Template-driven подхода обязательно создавать для каждого контрола свойство?
        Там же тоже можно получить доступ к свойству value у FormGroup.
        Например, если у вас в шаблоне есть форма:
        <form #myForm="ngForm">
          <input name="firstName" ngModel />
        </form>
        

        То в компоненте можно писать так:
        @ViewChild('myForm') myForm: NgForm;
        
        someMethod() {
          console.log(this.myForm.form.value.firstName);
        }
        


        1. klimentRu Автор
          09.01.2018 23:01

          Template-driven вариант мне кажется более интуитивным и простым.

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

          Из официальной документации:
          Neither is «better». They're two different architectural paradigms, with their own strengths and weaknesses. Choose the approach that works best for you. You may decide to use both in the same application.

          Так что не стоит холиварить. В некоторых ситуациях использование template-driven forms может быть удобнее чем reactive forms.


          1. Vadem
            10.01.2018 14:50

            Спасибо за ответ.
            Тоже считаю, что получение формы через ViewChild неудобный и неочевидный способ.
            Я не холивара ради, а понимания для.


  1. dreamseeker92
    09.01.2018 07:14

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


    1. klimentRu Автор
      09.01.2018 07:17

      Про вложенные группы и другие более сложные кейсы напишу во второй части. Тут хотелось на простом примере показать как начать работать с реактивными формами.


  1. kentov
    09.01.2018 09:07

    на vue.js понятней и меньше писать ))


    1. klimentRu Автор
      09.01.2018 09:09

      Сомневаюсь, что в работе со сложными формами vue обладает нужными инструментами. Такими как: динамические валидации, динамическое добавление контролов, и прочее…
      Если можно, то пример в студию)


  1. MOTORIST
    09.01.2018 17:01

    Не обязательно гонять все поля формы на ошибки, можно просто заблокировать кнопку отправки:
    <button type=«submit» [disabled]="!reactiveForm.valid">


    1. klimentRu Автор
      09.01.2018 18:01
      +1

      Согласен, но в моей практике был кейс, когда надо было делать именно подсветку не валидных контролов.
      Подумал, что это более интересный вариант.


    1. Vadem
      09.01.2018 22:32

      Часто вижу, что пишут именно так:

      [disabled]="!reactiveForm.valid"

      Хотя кажется, что проще писать вот так:
      [disabled]="reactiveForm.invalid"

      Есть какие-то плюсы у первого подхода?


      1. klimentRu Автор
        09.01.2018 23:07

        Разницы в работе нет. Только удобство чтения.