Это вторая часть серии «Введение в формы в 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.
Шаблон HTML
Прежде чем мы углубимся в фактический шаблон компонента, нам нужно иметь абстрактное представление о том, что мы создаем. Итак, вот структура формы, которую я имею в виду. Форма регистрации будет иметь несколько полей ввода, элемент 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 .
приложение / 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 { }
|
Затем создайте модель пользователя для формы регистрации. Мы можем использовать класс или интерфейс для создания модели. Для этого урока я собираюсь экспортировать класс со следующими свойствами.
приложение / User.ts
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
.
приложение / регистрация форма / регистрация-form.component.ts
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
.
приложение / регистрация форма / регистрация-form.component.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
|
<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
.
Отслеживание состояния с помощью FormControl
При создании форм с помощью стратегии реактивных форм вы не встретите директивы 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(‘[email protected]’, Validators.required);
|
Angular имеет ограниченный набор встроенных валидаторов. Популярные методы Validators.required
включают Validators.required
, Validators.minLength
, Validators.maxlength
и Validators.pattern
. Однако, чтобы использовать их, сначала нужно импортировать API-интерфейс Validator.
Для нашей формы регистрации у нас есть несколько полей контроля ввода (для электронной почты и пароля), поле селектора и поле флажка. Вместо того, чтобы создавать отдельные объекты FormControl
, не имеет ли больше смысла группировать все эти FormControl
в одну сущность? Это полезно, потому что теперь мы можем отслеживать значение и действительность всех объектов sub-FormControl в одном месте. Для этого и существует FormGroup
. Поэтому мы зарегистрируем родительскую FormGroup с несколькими дочерними FormControls.
Группировать несколько FormControls с FormGroup
Чтобы добавить FormGroup, сначала импортируйте ее. Затем объявите signupForm как свойство класса и инициализируйте его следующим образом:
приложение / регистрация форма / регистрация-form.component.ts
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 следующим образом:
приложение / регистрация форма / регистрация-form.component.html
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"
как мы только что сделали здесь.
Чтобы увидеть, все ли работает как положено, добавьте следующее после тега формы:
приложение / регистрация форма / регистрация-form.component.html
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 следующим образом.
Здесь следует отметить две вещи:
- JSON не совсем соответствует структуре пользовательской модели, которую мы создали ранее.
- 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
Создайте новую группу FormGroup для пароля.
приложение / регистрация форма / регистрация-form.component.ts
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, внесите следующие изменения:
приложение / регистрация форма / регистрация-form.component.html
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 в модели формы.
приложение / регистрация форма / регистрация-form.component.ts
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)
})
|
Затем отключите кнопку, пока форма недействительна.
приложение / регистрация форма / регистрация-form.component.html
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)])
}),
|
Вот и все для проверки. Однако логика модели формы выглядит загроможденной и повторяющейся. Давайте сначала очистим это.
Рефакторинг кода с использованием FormBuilder
Angular предоставляет вам синтаксический сахар для создания новых экземпляров FormGroup и FormControl под названием FormBuilder. API FormBuilder не делает ничего особенного, кроме того, что мы рассмотрели здесь.
Это упрощает наш код и облегчает процесс создания формы. Чтобы создать FormBuilder, вы должны импортировать его в signup-form.component.ts и вставить FormBuilder в конструктор.
приложение / регистрация форма / регистрация-form.component.ts
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
для создания формы. За исключением синтаксиса, все остальное остается прежним.
приложение / регистрация форма / регистрация-form.component.ts
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>
|
Здесь есть пара вопросов.
- Откуда взялись
pristine
иpristine
? -
signupForm.controls.email.invalid
слишком длинный и глубокий. - Ошибка явно не говорит, почему она недействительна.
Чтобы ответить на первый вопрос, каждый FormControl имеет определенные свойства, такие как invalid
, valid
, pristine
, dirty
, untouched
и untouched
. Мы можем использовать их, чтобы определить, должно ли отображаться сообщение об ошибке или предупреждение. Изображение ниже подробно описывает каждое из этих свойств.
Таким образом, элемент div с *ngIf
будет отображаться только в том случае, если электронная почта недействительна. Тем не менее, пользователь получит сообщение об ошибке, когда поля ввода не заполнены, даже до того, как они получат возможность редактировать форму.
Чтобы избежать этого сценария, мы добавили второе условие. Ошибка будет отображаться только после посещения элемента управления .
Чтобы избавиться от длинной цепочки имен методов ( signupForm.controls.email.invalid
), я собираюсь добавить несколько сокращенных методов получения. Это делает их более доступными и короткими.
приложение / регистрация форма / регистрация-form.component.ts
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 ниже:
приложение / регистрация форма / регистрация-form.component.html
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
для проверки всех возможных ошибок валидации, а затем отображать их обратно пользователю в виде пользовательских сообщений. Теперь выполните ту же процедуру для других элементов формы. Вот как я кодировал проверку паролей и контроль ввода терминов.
приложение / регистрация форма / регистрация-form.component.html
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>
|
Отправьте форму с помощью ngSubmit
Мы почти закончили с формой. В нем отсутствует функция отправки, которую мы собираемся реализовать сейчас.
1
2
3
|
<form class=»form-horizontal»
[formGroup]=»signupForm»
(ngSubmit)=»onFormSubmit()» >
|
При отправке формы значения модели формы должны передаваться в пользовательское свойство компонента.
приложение / регистрация форма / регистрация-form.component.ts
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 для чтения, изучения и использования.