1. Введение
Если вы когда-нибудь пытались собрать анкету на 10+ полей, то знаете: просто массив инпутов очень быстро превращается в "кашу", где сложно понять, что к чему относится. Например, у пользователя есть имя, email, адрес (который сам состоит из улицы, города и индекса), а ещё — список телефонов, который может быть пустым, а может содержать десяток номеров.
FormControl — это отдельное поле (например, одно текстовое поле).
FormGroup — это группа полей, объединённых логически (например, адрес: улица, город, индекс).
FormArray — это массив однотипных полей или групп (например, список телефонов).
Аналогия:
- FormControl — это один кирпичик.
- FormGroup — это стена из нескольких кирпичей.
- FormArray — это целый кирпичный забор, где количество секций можно менять на лету.
FormControl: создаём и используем отдельное поле
Начнём с самого простого — отдельного поля.
import { FormControl } from '@angular/forms';
// Создаём поле для имени пользователя
const nameControl = new FormControl(''); // '' — начальное значение
// Получить значение:
console.log(nameControl.value); // выведет ''
// Изменить значение:
nameControl.setValue('Иван');
console.log(nameControl.value); // выведет 'Иван'
В шаблоне Angular связываем с инпутом через директиву [formControl]:
<input type="text" [formControl]="nameControl" placeholder="Ваше имя">
Факт: FormControl — это не только значение, но и состояние (валидность, touched, dirty и т.п.), о чём поговорим позже.
2. FormGroup: объединяем поля в логическую структуру
Теория
FormGroup нужен, чтобы связать несколько FormControl (или даже других FormGroup) в единую структуру. Это удобно для валидации, получения значений, сброса и контроля состояния сразу над группой полей.
Пример: форма пользователя
import { FormGroup, FormControl } from '@angular/forms';
const userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
})
});
Структура:
- userForm (FormGroup)
- name (FormControl)
- email (FormControl)
- address (FormGroup)
- street (FormControl)
- city (FormControl)
- zip (FormControl)
Получение значений
console.log(userForm.value);
// Выведет объект:
// {
// name: '',
// email: '',
// address: {
// street: '',
// city: '',
// zip: ''
// }
// }
Связываем с шаблоном
В компоненте:
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html'
})
export class UserFormComponent {
userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
})
});
onSubmit() {
console.log(this.userForm.value);
}
}
В шаблоне (user-form.component.html):
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>
Имя:
<input formControlName="name">
</label>
<label>
Email:
<input formControlName="email">
</label>
<div formGroupName="address">
<label>
Улица:
<input formControlName="street">
</label>
<label>
Город:
<input formControlName="city">
</label>
<label>
Индекс:
<input formControlName="zip">
</label>
</div>
<button type="submit">Отправить</button>
</form>
Важный момент:
- Для вложенных групп используется formGroupName.
- Для полей — formControlName.
3. FormArray: динамические списки полей
Теория
FormArray — это массив FormControl или FormGroup. Представьте, что у пользователя может быть несколько телефонов, и он может добавлять/удалять их по желанию.
Пример: список телефонов
В компоненте:
import { Component } from '@angular/core';
import { FormGroup, FormControl, FormArray } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html'
})
export class UserFormComponent {
userForm = new FormGroup({
name: new FormControl(''),
phones: new FormArray([
new FormControl('')
])
});
get phones() {
return this.userForm.get('phones') as FormArray;
}
addPhone() {
this.phones.push(new FormControl(''));
}
removePhone(index: number) {
this.phones.removeAt(index);
}
onSubmit() {
console.log(this.userForm.value);
}
}
В шаблоне:
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>
Имя:
<input formControlName="name">
</label>
<div formArrayName="phones">
<div *ngFor="let phone of phones.controls; let i = index">
<input [formControlName]="i" placeholder="Телефон {{i + 1}}">
<button type="button" (click)="removePhone(i)" *ngIf="phones.length > 1">Удалить</button>
</div>
<button type="button" (click)="addPhone()">Добавить телефон</button>
</div>
<button type="submit">Отправить</button>
</form>
Как это работает?
- FormArray содержит набор FormControl (или FormGroup).
- Для доступа к элементам используем индекс: [formControlName]="i".
- Можно динамически добавлять и удалять поля.
Аналогия: FormArray — это как список гостей на вечеринке: кто-то пришёл, кто-то ушёл, а вы всегда знаете, сколько их и кто они.
4. Вложенные структуры: FormGroup внутри FormArray и наоборот
Можно строить сложные формы, комбинируя группы и массивы.
Пример: список адресов (каждый адрес — группа полей)
В компоненте:
import { FormGroup, FormControl, FormArray } from '@angular/forms';
@Component({
selector: 'app-address-list',
templateUrl: './address-list.component.html'
})
export class AddressListComponent {
form = new FormGroup({
addresses: new FormArray([
new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
})
])
});
get addresses() {
return this.form.get('addresses') as FormArray;
}
addAddress() {
this.addresses.push(new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
}));
}
removeAddress(index: number) {
this.addresses.removeAt(index);
}
}
В шаблоне:
<form [formGroup]="form">
<div formArrayName="addresses">
<div *ngFor="let addr of addresses.controls; let i = index" [formGroupName]="i">
<input formControlName="street" placeholder="Улица">
<input formControlName="city" placeholder="Город">
<input formControlName="zip" placeholder="Индекс">
<button type="button" (click)="removeAddress(i)" *ngIf="addresses.length > 1">Удалить адрес</button>
</div>
<button type="button" (click)="addAddress()">Добавить адрес</button>
</div>
</form>
5. Практический пример: форма пользователя с адресами и телефонами
Давайте соберём всё вместе и создадим форму пользователя, где:
- Имя и email — отдельные поля,
- Адреса — массив групп (FormArray из FormGroup),
- Телефоны — массив строк (FormArray из FormControl).
import { Component } from '@angular/core';
import { FormGroup, FormControl, FormArray } from '@angular/forms';
@Component({
selector: 'app-full-user-form',
templateUrl: './full-user-form.component.html'
})
export class FullUserFormComponent {
userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
addresses: new FormArray([
new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
})
]),
phones: new FormArray([
new FormControl('')
])
});
get addresses() {
return this.userForm.get('addresses') as FormArray;
}
get phones() {
return this.userForm.get('phones') as FormArray;
}
addAddress() {
this.addresses.push(new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
}));
}
removeAddress(i: number) {
this.addresses.removeAt(i);
}
addPhone() {
this.phones.push(new FormControl(''));
}
removePhone(i: number) {
this.phones.removeAt(i);
}
onSubmit() {
console.log(this.userForm.value);
}
}
В шаблоне (full-user-form.component.html):
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>
Имя:
<input formControlName="name">
</label>
<label>
Email:
<input formControlName="email">
</label>
<div formArrayName="addresses">
<div *ngFor="let addr of addresses.controls; let i = index" [formGroupName]="i">
<input formControlName="street" placeholder="Улица">
<input formControlName="city" placeholder="Город">
<input formControlName="zip" placeholder="Индекс">
<button type="button" (click)="removeAddress(i)" *ngIf="addresses.length > 1">Удалить адрес</button>
</div>
<button type="button" (click)="addAddress()">Добавить адрес</button>
</div>
<div formArrayName="phones">
<div *ngFor="let phone of phones.controls; let i = index">
<input [formControlName]="i" placeholder="Телефон {{i + 1}}">
<button type="button" (click)="removePhone(i)" *ngIf="phones.length > 1">Удалить телефон</button>
</div>
<button type="button" (click)="addPhone()">Добавить телефон</button>
</div>
<button type="submit">Отправить</button>
</form>
6. Особенности работы с FormGroup, FormControl и FormArray
- FormControl — всегда для одного поля.
- FormGroup — для объединения разных полей или вложенных групп.
- FormArray — для динамического списка однотипных полей или групп.
Таблица сравнения
| Класс | Для чего нужен | Как обращаться в шаблоне | Пример в TS-коде |
|---|---|---|---|
| FormControl | Одно поле | |
|
| FormGroup | Группа разных полей/групп | |
|
| FormArray | Массив однотипных полей/групп | |
|
7. Типичные ошибки при создании структуры формы
Ошибка №1: Не та директива в шаблоне.
Очень часто путают formControlName, formGroupName и formArrayName. Если вы используете FormArray, то для каждого элемента в ngFor должны указывать [formGroupName]="i" (если это FormGroup) или [formControlName]="i" (если это FormControl). Если перепутать — Angular обидится и выбросит ошибку.
Ошибка №2: Доступ к controls без приведения типа.
FormGroup и FormArray возвращают абстрактный тип AbstractControl, поэтому для работы с методами push/remove обязательно делать приведение:
this.userForm.get('phones') as FormArray
Ошибка №3: Неинициализированные массивы или группы.
Если вы забыли добавить хотя бы один элемент в FormArray при создании, шаблон не сможет отобразить поля, и форма будет "пустой". Не ленитесь добавлять хотя бы один элемент по умолчанию.
Ошибка №4: Не обновили шаблон после добавления/удаления.
Если работаете с FormArray и динамически добавляете/удаляете элементы, обязательно используйте методы push/removeAt, а не мутируйте массив напрямую.
Ошибка №5: Не используете formGroup/formArrayName в шаблоне.
Если забыть обернуть поля в <div [formGroupName]="i"> или <div formArrayName="phones">, Angular не сможет правильно связать шаблон с формой, и получите ошибку "Cannot find control with path".
Ошибка №6: Не используете (ngSubmit) для отправки формы.
Если вы используете <form>, то для реактивных форм всегда используйте (ngSubmit)="onSubmit()", иначе Angular не узнает, что вы хотите отправить форму.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ