Статьи

Введение в формы в Angular 4: Реактивные формы

Конечный продукт
Что вы будете создавать

Это вторая часть серии «Введение в формы в Angular 4. В первой части мы создали форму, используя шаблонный подход. Мы использовали директивы, такие как ngModel , ngModelGroup и ngForm чтобы перегружать элементы формы. В этом уроке мы будем использовать другой подход к построению форм — реактивный.

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

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

Обзор высокого уровня Реактивных форм с использованием модельно-ориентированного подхода

Практически говоря, если мы создаем форму для обновления профиля пользователя, модель данных — это пользовательский объект, полученный с сервера. По соглашению, это часто хранится внутри пользовательского свойства компонента ( this.user ). Объект управления формой или модель формы будут связаны с фактическими элементами управления формой шаблона.

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

Давайте начнем.

Вам не нужно следовать первой части этой серии, чтобы вторая часть имела смысл. Однако, если вы новичок в формах в Angular, я настоятельно рекомендую пройти через шаблонную стратегию. Код для этого проекта доступен в моем репозитории GitHub . Убедитесь, что вы находитесь в правой ветке, а затем загрузите почтовый индекс или, альтернативно, клонируйте репо, чтобы увидеть форму в действии.

Если вы предпочитаете начинать с нуля, убедитесь, что у вас установлен Angular CLI. Используйте команду ng для создания нового проекта.

1
$ ng new SignupFormProject

Затем создайте новый компонент для SignupForm или создайте его вручную.

1
ng generate component SignupForm

Замените содержимое app.component.html следующим:

1
<app-signup-form> </app-signup-form>

Вот структура каталогов для каталога src / . Я удалил некоторые ненужные файлы, чтобы все было просто.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
.
├── app
│  ├── app.component.css
│  ├── app.component.html
│  ├── app.component.ts
│  ├── app.module.ts
│  ├── signup-form
│  │  ├── signup-form.component.css
│  │  ├── signup-form.component.html
│  │  └── signup-form.component.ts
│  └── User.ts
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
├── tsconfig.app.json
└── typings.d.ts

Как видите, каталог для компонента SignupForm был создан автоматически. Вот куда пойдет большая часть нашего кода. Я также создал новый User.ts для хранения нашей модели User.

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

Шаблон HTML

Вот HTML-шаблон, который мы будем использовать для нашей страницы регистрации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<div class=»row custom-row»>
  <div class= «col-sm-5 custom-container jumbotron»>
       
    <form class=»form-horizontal»>
        <fieldset>
          <legend>SignUp</legend>
         
            <!— Email Block —>
            <div class=»form-group»>
              <label for=»inputEmail»>Email</label>
              <input type=»text»
                id=»inputEmail»
                placeholder=»Email»>
            </div>
            <!— Password Block —>
            <div class=»form-group»>
              <label for=»inputPassword»>Password</label>
              <input type=»password»
                id=»inputPassword»
                placeholder=»Password»>
            </div>
     
            <div class=»form-group»>
              <label for=»confirmPassword» >Confirm Password</label>
              <input type=»password»
                id=»confirmPassword»
                placeholder=»Password»>
            </div>
             
            <!— Select gender Block —>
            <div class=»form-group»>
              <label for=»select»>Gender</label>
                <select id=»select»>
                  <option>Male</option>
                  <option>Female</option>
                  <option>Other</option>
                </select>
            </div>
             
            <!— Terms and conditions Block —>
             <div class=»form-group checkbox»>
              <label>
                <input type=»checkbox»> Confirm that you’ve read the Terms and
                Conditions
              </label>
            </div>
            
           <!— Buttons Block —>
            <div class=»form-group»>
                <button type=»reset» class=»btn btn-default»>Cancel</button>
                <button type=»submit» class=»btn btn-primary»>Submit</button>
            </div>
        </fieldset>
    </form>
  </div>
</div>

Классы CSS, используемые в шаблоне HTML, являются частью библиотеки Bootstrap, используемой для создания красивых вещей. Поскольку это не учебник по дизайну, я не буду много говорить об аспектах CSS формы, если в этом нет необходимости.

Чтобы создать реактивную форму, вам нужно импортировать ReactiveFormsModule из @angular/forms и добавить его в массив импорта в app.module.ts .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// Import ReactiveFormsModule
import { ReactiveFormsModule } from ‘@angular/forms’;
 
@NgModule({
  .
  .
  //Add the module to the imports Array
  imports: [
    BrowserModule,
    ReactiveFormsModule
 .
 .
})
export class AppModule { }

Затем создайте модель пользователя для формы регистрации. Мы можем использовать класс или интерфейс для создания модели. Для этого урока я собираюсь экспортировать класс со следующими свойствами.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
export class User {
 
    id: number;
    email: string;
    //Both the passwords are in a single object
    password: {
      pwd: string;
      confirmPwd: string;
    };
     
    gender: string;
    terms: boolean;
 
    constructor(values: Object = {}) {
      //Constructor initialization
      Object.assign(this, values);
  }
 
}

Теперь создайте экземпляр модели User в компоненте SignupForm .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, OnInit } from ‘@angular/core’;
// Import the User model
import { User } from ‘./../User’;
 
@Component({
  selector: ‘app-signup-form’,
  templateUrl: ‘./signup-form.component.html’,
  styleUrls: [‘./signup-form.component.css’]
})
export class SignupFormComponent implements OnInit {
 
  //Gender list for the select control element
  private genderList: string[];
  //Property for the user
  private user:User;
 
  ngOnInit() {
 
    this.genderList = [‘Male’, ‘Female’, ‘Others’];
   
    
}

Для файла signup-form.component.html я собираюсь использовать тот же шаблон HTML, который обсуждался выше, но с небольшими изменениями. Форма регистрации имеет поле выбора со списком параметров. Хотя это работает, мы сделаем это угловым путем, просматривая список с ngFor директивы ngFor .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class=»row custom-row»>
  <div class= «col-sm-5 custom-container jumbotron»>
       
    <form class=»form-horizontal»>
        <fieldset>
          <legend>SignUp</legend>
.
.
            <!— Gender Block —>
            <div class=»form-group»>
              <label for=»select»>Gender</label>
                   <select id=»select»>
                      
                     <option *ngFor = «let g of genderList»
                       [value] = «g»> {{g}}
                     </option>
                   </select>
               </div>
.
.
    </fieldset>
    </form>
  </div>
</div>

Примечание. Вы можете получить сообщение об ошибке « Нет поставщика для ControlContainer» . Ошибка появляется, когда у компонента есть тег <form> без директивы formGroup. Ошибка исчезнет, ​​как только мы добавим директиву FormGroup позже в этом руководстве.

У нас есть компонент, модель и шаблон формы. Что теперь? Пришло время испачкать руки и познакомиться с API-интерфейсами, необходимыми для создания реактивных форм. Это включает FormControl и FormGroup .

При создании форм с помощью стратегии реактивных форм вы не встретите директивы ngModel и ngForm. Вместо этого мы используем базовый FormControl и FormGroup API.

FormControl — это директива, используемая для создания экземпляра FormControl, который можно использовать для отслеживания состояния определенного элемента формы и его состояния проверки. Вот как работает FormControl:

1
2
3
4
5
6
7
/* Import FormControl first */
import { FormControl } from ‘@angular/forms’;
 
/* Example of creating a new FormControl instance */
export class SignupFormComponent {
  email = new FormControl();
}

email теперь является экземпляром FormControl, и вы можете привязать его к элементу управления вводом в вашем шаблоне следующим образом:

1
2
3
4
5
<h2>Signup</h2>
 
<label class=»control-label»>Email:
  <input class=»form-control» [formControl]=»email»>
</label>

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

Конструктор FormControl принимает три аргумента — начальное значение, массив валидаторов синхронизации и массив асинхронных валидаторов — и, как вы уже догадались, все они являются необязательными. Мы рассмотрим первые два аргумента здесь.

1
2
3
4
5
6
7
import { Validators } from ‘@angular/forms’;
.
.
.
/* FormControl with initial value and a validator */
 
  email = new FormControl(‘bob@example.com’, Validators.required);

Angular имеет ограниченный набор встроенных валидаторов. Популярные методы Validators.required включают Validators.required , Validators.minLength , Validators.maxlength и Validators.pattern . Однако, чтобы использовать их, сначала нужно импортировать API-интерфейс Validator.

Для нашей формы регистрации у нас есть несколько полей контроля ввода (для электронной почты и пароля), поле селектора и поле флажка. Вместо того, чтобы создавать отдельные объекты FormControl , не имеет ли больше смысла группировать все эти FormControl в одну сущность? Это полезно, потому что теперь мы можем отслеживать значение и действительность всех объектов sub-FormControl в одном месте. Для этого и существует FormGroup . Поэтому мы зарегистрируем родительскую FormGroup с несколькими дочерними FormControls.

Чтобы добавить FormGroup, сначала импортируйте ее. Затем объявите signupForm как свойство класса и инициализируйте его следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Import the API for building a form
import { FormControl, FormGroup, Validators } from ‘@angular/forms’;
 
 
export class SignupFormComponent implements OnInit {
     
    genderList: String[];
    signupForm: FormGroup;
    .
    .
 
   ngOnInit() {
 
    this.genderList = [‘Male’, ‘Female’, ‘Others’];
 
    this.signupForm = new FormGroup ({
        email: new FormControl(»,Validators.required),
        pwd: new FormControl(),
        confirmPwd: new FormControl(),
        gender: new FormControl(),
        terms: new FormControl()
    })
   
   }
}

Свяжите модель FormGroup с DOM следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<form class=»form-horizontal» [formGroup]=»signupForm» >
       <fieldset>
         <legend>SignUp</legend>
        
           <!— Email Block —>
           <div class=»form-group»>
             <label for=»inputEmail»>Email</label>
             <input type=»text» formControlName = «email»
               id=»inputEmail»
               placeholder=»Email»>
            
           .
           .
        
       </fieldset>
   </form>

[formGroup] = "signupForm" сообщает Angular, что вы хотите связать эту форму с FormGroup объявленной в классе компонента. Когда Angular видит formControlName="email" , он проверяет наличие экземпляра FormControl со значением ключа email внутри родительской FormGroup.

Аналогично, обновите другие элементы формы, добавив formControlName="value" как мы только что сделали здесь.

Чтобы увидеть, все ли работает как положено, добавьте следующее после тега формы:

1
2
3
<!— Log the FormGroup values to see if the binding is working —>
   <p>Form value {{ signupForm.value |
    <p> Form status {{ signupForm.status |

SignupForm свойство SignupForm через JsonPipe чтобы отобразить модель как JSON в браузере. Это полезно для отладки и регистрации. Вы должны увидеть вывод в формате JSON следующим образом.

Состояние и действительность формы в управляемых моделями формах

Здесь следует отметить две вещи:

  1. JSON не совсем соответствует структуре пользовательской модели, которую мы создали ранее.
  2. SignupForm.status показывает, что статус формы недействителен. Это ясно показывает, что Validators.required в поле управления электронной почтой работает должным образом.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// Form model
 {
    «email»: «»,
    «pwd»: «»,
    «confirmPwd»: «»,
    «gender»: «»,
    «terms»: false
}
 
//User model
{
    «email»: «»,
    «password»: {
      «pwd»: «»,
      «confirmPwd»: «»,
    },
    «gender»: «»,
    «terms»: false
}

Чтобы получить иерархическую структуру модели данных, мы должны использовать вложенную FormGroup. Кроме того, всегда полезно иметь связанные элементы формы в одной группе FormGroup.

Создайте новую группу FormGroup для пароля.

1
2
3
4
5
6
7
8
9
this.signupForm = new FormGroup ({
       email: new FormControl(»,Validators.required),
       password: new FormGroup({
           pwd: new FormControl(),
           confirmPwd: new FormControl()
       }),
       gender: new FormControl(),
       terms: new FormControl()
   })

Теперь, чтобы связать новую модель формы с DOM, внесите следующие изменения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<!— Password Block —>
   <div formGroupName = «password»>
       <div class=»form-group»>
         <label for=»inputPassword»>Password</label>
         <input type=»password» formControlName = «pwd»
           id=»inputPassword»
           placeholder=»Password»>
       </div>
 
       <div class=»form-group»>
         <label for=»confirmPassword» >Confirm Password</label>
         <input type=»password» formControlName = «confirmPwd»
           id=»confirmPassword»
           placeholder=»Password»>
       </div>
   </div>

formGroupName = "password" выполняет привязку для вложенной FormGroup. Теперь структура модели формы соответствует нашим требованиям.

1
2
3
4
5
6
7
8
Form value: {
    «email»: «», «
    password»: { «pwd»: null, «confirmPwd»: null },
    «gender»: null,
    «terms»: null
    }
 
Form status «INVALID»

Далее нам нужно проверить элементы управления формой.

У нас есть простая проверка для контроля ввода электронной почты. Однако этого недостаточно. Вот полный список наших требований к валидации.

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

Первое легко. Добавьте Validator.required для всех FormControls в модели формы.

01
02
03
04
05
06
07
08
09
10
this.signupForm = new FormGroup ({
    email: new FormControl(»,Validators.required),
    password: new FormGroup({
        pwd: new FormControl(», Validators.required),
        confirmPwd: new FormControl(», Validators.required)
    }),
    gender: new FormControl(», Validators.required),
    //requiredTrue so that the terms field isvalid only if checked
    terms: new FormControl(», Validators.requiredTrue)
})

Затем отключите кнопку, пока форма недействительна.

1
2
3
4
5
<!— Buttons Block —>
   <div class=»form-group»>
       <button type=»reset» class=»btn btn-default»>Cancel</button>
       <button type=»submit» [disabled] = «!signupForm.valid» class=»btn btn-primary»>Submit</button>
   </div>

Чтобы добавить ограничение на электронную почту, вы можете использовать Validators.email по умолчанию или создать собственный Validators.pattern() который задает регулярные выражения, подобные приведенному ниже:

1
2
3
email: new FormControl(»,
   [Validators.required,
   Validators.pattern(‘[a-z0-9._%+-]+@[a-z0-9.-]+\.[az]{2,3}$’)])

Используйте валидатор minLength для полей пароля.

1
2
3
4
password: new FormGroup({
           pwd: new FormControl(», [Validators.required, Validators.minLength(8)]),
           confirmPwd: new FormControl(», [Validators.required, Validators.minLength(8)])
       }),

Вот и все для проверки. Однако логика модели формы выглядит загроможденной и повторяющейся. Давайте сначала очистим это.

Angular предоставляет вам синтаксический сахар для создания новых экземпляров FormGroup и FormControl под названием FormBuilder. API FormBuilder не делает ничего особенного, кроме того, что мы рассмотрели здесь.

Это упрощает наш код и облегчает процесс создания формы. Чтобы создать FormBuilder, вы должны импортировать его в signup-form.component.ts и вставить FormBuilder в конструктор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import { FormBuilder, FormGroup, Validators } from ‘@angular/forms’;
.
.
export class SignupFormComponent implements OnInit {
    signupForm: FormGroup;
 
    //Inject the formbuilder into the constructor
    constructor(private fb:FormBuilder) {}
     
    ngOnInit() {
     
    …
         
    }
 
}

Вместо создания новой FormGroup() мы используем this.fb.group для создания формы. За исключением синтаксиса, все остальное остается прежним.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
         
    ngOnInit() {
        …
         
        this.signupForm = this.fb.group({
            email: [»,[Validators.required,
                        Validators.pattern(‘[a-z0-9._%+-]+@[a-z0-9.-]+\.[az]{2,3}$’)]],
            password: this.fb.group({
                pwd: [», [Validators.required,
                           Validators.minLength(8)]],
                confirmPwd: [», [Validators.required,
                                  Validators.minLength(8)]]
            }),
            gender: [», Validators.required],
            terms: [», Validators.requiredTrue]
        })
}

Для отображения ошибок я собираюсь использовать условную директиву ngIf для элемента div. Давайте начнем с поля управления вводом для электронной почты:

1
2
3
4
<!— Email error block —>
<div *ngIf=»signupForm.controls.email.invalid && signupForm.controls.email.touched»
    Email is invalid
</div>

Здесь есть пара вопросов.

  1. Откуда взялись pristine и pristine ?
  2. signupForm.controls.email.invalid слишком длинный и глубокий.
  3. Ошибка явно не говорит, почему она недействительна.

Чтобы ответить на первый вопрос, каждый FormControl имеет определенные свойства, такие как invalid , valid , pristine , dirty , untouched и untouched . Мы можем использовать их, чтобы определить, должно ли отображаться сообщение об ошибке или предупреждение. Изображение ниже подробно описывает каждое из этих свойств.

Свойства FormControl в угловом реактивном подходе

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

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

Чтобы избавиться от длинной цепочки имен методов ( signupForm.controls.email.invalid ), я собираюсь добавить несколько сокращенных методов получения. Это делает их более доступными и короткими.

01
02
03
04
05
06
07
08
09
10
11
12
export class SignupFormComponent implements OnInit {
 
    get email() { return this.signupForm.get(’email’);
     
    get password() { return this.signupForm.get(‘password’);
 
    get gender() { return this.signupForm.get(‘gender’);
 
    get terms() { return this.signupForm.get(‘terms’);
     
}

Чтобы сделать ошибку более явной, я добавил вложенные условия ngIf ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
<!— Email error block —>
   <div *ngIf=»email.invalid && email.touched»
       class=»col-sm-3 text-danger»>
 
       <div *ngIf = «email.errors?.required»>
           Email field can’t be blank
       </div>
 
       <div *ngIf = «email.errors?.pattern»>
           The email id doesn’t seem right
       </div>
 
    </div>

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<!— Password error block —>
       <div *ngIf=»(password.invalid && password.touched)»
        class=»col-sm-3 text-danger»>
     
        Password needs to be more than 8 characters
    </div>
       
.
.
.
 <!— Terms error block —>
      <div *ngIf=»(terms.invalid && terms.touched)»
        class=»col-sm-3 text-danger»>
         
        Please accept the Terms and conditions first.
      </div>
    </div>

Мы почти закончили с формой. В нем отсутствует функция отправки, которую мы собираемся реализовать сейчас.

1
2
3
<form class=»form-horizontal»
   [formGroup]=»signupForm»
   (ngSubmit)=»onFormSubmit()» >

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

1
2
3
4
5
6
7
public onFormSubmit() {
       if(this.signupForm.valid) {
           this.user = this.signupForm.value;
           console.log(this.user);
           /* Any API call logic via services goes here */
       }
   }

Если вы с самого начала читали этот учебный курс, у нас был практический опыт работы с двумя популярными технологиями создания форм в Angular. Методы, основанные на шаблонах и моделях, — это два способа достижения одного и того же. Лично я предпочитаю использовать реактивные формы по следующим причинам:

  • Вся логика проверки формы будет расположена в одном месте — внутри класса вашего компонента. Это намного более продуктивно, чем шаблонный подход, где директивы ngModel разбросаны по всему шаблону.
  • В отличие от управляемых шаблонами форм, управляемые моделями формы легче тестировать. Вам не нужно прибегать к сквозным библиотекам для тестирования вашей формы.
  • Логика проверки будет идти внутри класса компонента, а не в шаблоне.
  • Для формы с большим количеством элементов формы этот подход имеет то, что называется FormBuilder, чтобы упростить создание объектов FormControl.

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

В то же время, существует множество платформ и библиотек, которые могут вас занять, и множество вещей на Envato Market для чтения, изучения и использования.