JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /FormGroup, FormControl, FormArray: создание структуры фор...

FormGroup, FormControl, FormArray: создание структуры формы

Модуль 4: Node.js, Next.js и Angular
16 уровень , 6 лекция
Открыта

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 Одно поле
formControlName="field"
new FormControl('')
FormGroup Группа разных полей/групп
formGroupName="group"
new FormGroup({...})
FormArray Массив однотипных полей/групп
formArrayName="array"
new 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 не узнает, что вы хотите отправить форму.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ