Если вы разрабатываете веб-приложение, вам рано или поздно придётся собирать данные от пользователя. К счастью, реактивные формы в Angular позволяют делать это без лишней сложности — без нагромождения директив и с минимальным количеством шаблонного кода. Более того, их просто валидировать, так что можно обойтись даже без end-to-end тестов.
Говоря проще, form control’ы в Angular дают полный контроль разработчику — ничего не происходит автоматически, и каждое решение по вводу и управлению принимается явно и осознанно. В этом руководстве мы покажем, как объединять form control’ы в form group’ы, чтобы структурировать форму и упростить доступ к её элементам — как к логическим блокам. Чтобы лучше понять, как работают form group’ы в Angular, мы шаг за шагом соберём реактивную форму.
Для работы с примером скачайте стартовый проект с GitHub и откройте его в VS Code. Если ещё не обновляли Angular, поставьте актуальную на момент написания версию — Angular v18.
Что такое form control в Angular
В Angular form control — это базовый строительный блок форм. Он хранит как значение поля, так и правила его валидации. Каждый элемент ввода в форме должен быть связан с соответствующим form control’ом — это позволяет отслеживать данные и валидировать ввод.
Что такое form group в Angular
Form group объединяет несколько form control’ов в одну логическую группу. Так же как form control даёт доступ к состоянию отдельного поля, form group предоставляет доступ к состоянию всех вложенных в неё полей. Каждый form control внутри группы связан с соответствующим form control’ом в коде компонента.
FormControl и FormGroup в Angular
FormControl
— это класс в Angular, который отслеживает значение и статус валидации отдельного поля формы. Наряду с FormGroup
и FormArray
, он входит в тройку ключевых элементов, на которых строятся формы в Angular. Поскольку FormControl
наследуется от AbstractControl
, он предоставляет доступ к значению поля, его статусу валидации, взаимодействиям пользователя и связанным событиям.
FormGroup
в Angular — это контейнер, который объединяет несколько экземпляров FormControl
. Он отслеживает значения и состояния валидации всех вложенных контролов, организуя их в один объект, где каждому полю соответствует ключ по имени. Статус группы вычисляется на основе статусов её дочерних контролов: если хотя бы одно поле в группе невалидно, то и вся группа считается невалидной.
Строгая типизация форм в Angular
До выхода Angular 14 нельзя было задать тип для FormControl
. Это могло привести к путанице во время выполнения — было неочевидно, какое значение ожидается в форме.
Начиная с Angular 14, FormControl
по умолчанию поддерживает строгую типизацию. Это позволяет явно указывать ожидаемый тип данных при создании FormControl
. В ближайшем примере формы мы покажем, как это работает на практике.
Регистрация form group в Angular
Чтобы использовать FormGroup
в компоненте Angular, сначала импортируйте его в файле компонента.
Чтобы увидеть, как это работает, откройте файл employee.component.ts
и вставьте следующий блок кода:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'
@Component({
selector: 'app-employee',
templateUrl: './employee.component.html',
styleUrls: ['./employee.component.css']
})
export class EmployeeComponent implements OnInit {
bioSection = new FormGroup({
firstName: new FormControl<string>(''),
lastName: new FormControl<string>(''),
age: new FormControl<number|null>(null)
});
constructor() { }
ngOnInit() {
}
}
Здесь мы импортировали FormGroup
и инициализировали его, чтобы сгруппировать несколько form control’ов, которые составляют блок «О себе» в форме. Чтобы связать эту группу с шаблоном, нужно привязать модель к представлению, указав имя form group’а, вот так:
<form [formGroup]="bioSection">
<label>
First Name:
<input type="text" [formControl]="bioSection.controls.firstName" >
</label>
<label>
Last Name:
<input type="text" [formControl]="bioSection.controls.lastName">
</label>
<label>
Age:
<input type="text" [formControl]="bioSection.controls.age">
</label>
<button type="submit">Submit Application</button>
</form>
Мы ссылаемся на FormGroup
, созданный в коде, а затем напрямую указываем нужный control внутри директивы FormControl
.
Ваш файл app.component.html
должен выглядеть следующим образом:
<div style="text-align:center">
<h2>Angular Job Board </h2>
<app-employee></app-employee>
</div>
Теперь запустите приложение в режиме разработки с помощью следующей команды:
ng serve
Должно появиться такое:
Валидация form control’ов
Наличие формы — это хорошо, но без валидации данных есть риск получить недостоверную или бессмысленную информацию.
Angular предоставляет набор встроенных валидаторов, так что во многих типичных случаях нет нужды писать свои. В этом примере мы проверим, что имя и фамилия содержат минимум три символа, а возраст больше 18:
// employee.component.ts
bioSection = new FormGroup({
firstName: new FormControl<string>('', [
Validators.minLength(3),
Validators.required
]),
lastName: new FormControl<string>('', [Validators.minLength(3), Validators.required]),
age: new FormControl<number|null>(null, [Validators.min(18), Validators.required])
});
Теперь нужно обновить HTML-шаблон, чтобы отображать ошибки валидации формы. Также важно запретить отправку формы, пока в ней есть ошибки.
В обновлённой версии форма блокирует кнопку отправки, если она невалидна, и показывает пользователю сообщения с объяснением, что именно заполнено неправильно:
<form [formGroup]="bioSection" style="display: flex; flex-direction: column">
<label>
First Name:
<input type="text" [formControl]="bioSection.controls.firstName">
</label>
<label>
Last Name:
<input type="text" [formControl]="bioSection.controls.lastName">
</label>
<label>
Age:
<input type="number" [formControl]="bioSection.controls.age">
</label>
<!-- Атрибут disabled у кнопки блокирует отправку, если форма невалидна -->
<button type="submit" [disabled]="!bioSection.valid">Submit Application</button>
</form>
<div style="width: 100%;">
@if (!bioSection.valid){ <!-- Если форма невалидна... -->
<div style="width: 300px; margin: auto; padding: 20px; background-color: palegoldenrod">
<b>Validation errors</b>
<ol>
<!-- Для каждого контрола проверяем, что его параметры выполнены -->
@if (bioSection.controls.firstName.errors?.['minlength'] ?? bioSection.controls.firstName.errors?.['required']){
<li>The first name is not long enough, or not specified.</li>
}
@if (bioSection.controls.lastName.errors?.['minlength'] ?? bioSection.controls.lastName.errors?.['required']){
<li>The last name is not long enough, or not specified.</li>
}
@if (bioSection.controls.age.errors?.['min'] ?? bioSection.controls.age.errors?.['required']){
<li>You must be over 18 to sign up</li>
}
</ol>
</div>
}
</div>
В результате мы получаем форму, которая принимает только корректные данные и отклоняет некорректный ввод:
Создание собственного form control’а
В Angular есть множество полезных контролов, которые легко добавить в форму с помощью FormControl
. Однако иногда требуется использовать специфический элемент управления, которого нет в стандартной библиотеке. К счастью, создание собственного form control’а — это довольно простой процесс, и такой контрол можно переиспользовать по всему приложению.
В этом примере мы создадим кастомный FormControl
— степпер.
Сначала создадим компонент custom-stepper
. Выполните команду ng generate component CustomStepper
, чтобы Angular CLI сгенерировал базовую структуру компонента.
Затем создадим простой степпер в HTML-шаблоне. Он будет содержать поле ввода, а по бокам от него — кнопки «плюс» и «минус», с помощью которых пользователь сможет увеличивать или уменьшать значение. Кроме того, если пользователь вручную введёт значение в поле, мы должны уведомить форму о том, что значение обновилось:
<div class="stepper">
<button (click)="decrement()">-</button>
<input type="number" [(ngModel)]="value" (input)="updateValue($event)" />
<button (click)="increment()">+</button>
</div>
Давайте кратко разберём, что происходит в шаблоне:
- Есть две кнопки — для увеличения и уменьшения значения. Обе вызывают соответствующие методы в коде компонента.
- Поле ввода привязано к свойству
value
в компоненте. Когда значение меняется, вызывается методupdateValue
с новым значением.
Теперь перейдём к самому компоненту. Сначала нужно указать Angular, что этот компонент реализует ControlValueAccessor
, чтобы он мог корректно использоваться как form control и регистрироваться в форме.
Для этого обновим массив providers
, добавив в него следующую информацию:
@Component({
selector: 'app-custom-stepper',
templateUrl: './custom-stepper.component.html',
styleUrls: ['./custom-stepper.component.css'],
providers: [
// Подключаем NG_VALUE_ACCESSOR
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomStepperComponent),
multi: true,
},
],
})
Далее необходимо реализовать интерфейс ControlValueAccessor
в этом компоненте:
export class CustomStepperComponent implements ControlValueAccessor {
// ...component code...
}
Для реализации ControlValueAccessor
необходимо определить три обязательные функции:
-
writeValue(value)
— записывает новое значение в form control. Вызывается, когда значение формы обновляется программно, чтобы синхронизировать модель и представление кастомного поля. -
registerOnChange(fn)
— регистрирует колбэк, который вызывается при изменении значения в UI. Эта функция сообщает форме о том, что значение обновилось, и синхронизирует его с моделью данных. -
registerOnTouched(fn)
— регистрирует колбэк, который вызывается, когда элемент теряет фокус. Это также необходимо для корректного обновления состояния формы. -
setDisabledState(isDisabled)
(опционально) — вызывается, когда контрол переводится в состояние «отключён». Если переданоtrue
, контрол должен быть заблокирован для взаимодействия, еслиfalse
— снова активирован.
Помимо этого, нужно реализовать собственную логику степпера: увеличение и уменьшение значения при нажатии на кнопки плюс и минус.
После этого компонент будет выглядеть следующим образом:
Вот как будет выглядеть код компонента кастомного степпера, реализующего ControlValueAccessor
:
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-custom-stepper',
templateUrl: './custom-stepper.component.html',
styleUrls: ['./custom-stepper.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomStepperComponent),
multi: true,
},
],
})
export class CustomStepperComponent implements ControlValueAccessor {
value: number | null = null;
onChange: any = () => {};
onTouch: any = () => {};
// -- Реализация ControlValueAccessor --
// Вызывается, когда модель формы обновляется программно
writeValue(value: number): void {
this.value = value;
}
// Регистрирует колбэк, вызываемый при изменении значения пользователем
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Регистрирует колбэк, вызываемый при потере фокуса
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Вызывается, когда контрол переводится в состояние disabled
setDisabledState(isDisabled: boolean): void {
// Можно реализовать блокировку UI при необходимости
}
// -- Логика степпера --
// Увеличение значения на 1
increment(): void {
this.updateValue(this.value !== null ? this.value + 1 : 1);
}
// Уменьшение значения на 1
decrement(): void {
this.updateValue(this.value !== null ? this.value - 1 : -1);
}
// Обновление значения и уведомление формы об изменении
updateValue(newValue: number | null): void {
if (newValue !== this.value) {
this.value = newValue;
this.onChange(newValue);
this.onTouch();
}
}
}
Наконец, в employee.component.html
мы можем добавить ссылку на наш кастомный контрол. Также нужно добавить свойство yearsExperience
в FormGroup
в коде компонента:
<label>
Years experience:
<app-custom-stepper [formControl]="bioSection.controls.yearsExperience"></app-custom-stepper>
</label>
В результате наш новый степпер появляется в интерфейсе формы:
Сейчас самое время немного привести интерфейс в порядок. Чтобы он выглядел аккуратнее, добавьте следующие стили в файл employee.component.css
:
Ниже приведены CSS-правила, которые можно применить к компоненту:
input[type=text] {
width: 30%;
padding: 8px 14px;
margin: 2px;
box-sizing: border-box;
}
button {
font-size: 12px;
margin: 2px;
padding: 8px 14px;
}
Вложенные form group в Angular
API реактивных форм в Angular позволяет вкладывать один FormGroup
внутрь другого. Чтобы продемонстрировать, как работают вложенные группы, скопируйте следующий блок кода в файл employee.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'
@Component({
selector: 'app-employee',
templateUrl: './employee.component.html',
styleUrls: ['./employee.component.css']
})
export class EmployeeComponent implements OnInit {
bioSection = new FormGroup({
firstName: new FormControl<string>('', [
Validators.minLength(3),
Validators.required
]),
lastName: new FormControl<string>('', [Validators.minLength(3), Validators.required]),
age: new FormControl<number>(null, [Validators.min(18), Validators.required]),
yearsExperience: new FormControl<number>(0),
stackDetails: new FormGroup({
stack: new FormControl<string>(''),
experience: new FormControl<string>('')
}),
address: new FormGroup({
country: new FormControl<string>(''),
city: new FormControl<string>('')
})
});
constructor() { }
ngOnInit() {
}
callingFunction() {
console.log(this.bioSection.value);
}
}
Основная form group — это блок «О себе», внутри которого вложены группы с деталями по стеку и адресом. Как видно, вложенные группы задаются не через отдельное присваивание, а с помощью двоеточия — так же, как обычные form control’ы.
В шаблоне это будет выглядеть следующим образом:
// copy inside the employee.component.html file
<form [formGroup]="bioSection" (ngSubmit)="callingFunction()">
<h3>Bio Details
</h3>
<label>
First Name:
<input type="text" formControlName="firstName">
</label> <br>
<label>
Last Name:
<input type="text" formControlName="lastName">
</label> <br>
<label>
Age:
<input type="text" formControlName="age">
</label>
<div [formGroup]="bioSection.controls.stackDetails">
<h3>Stack Details</h3>
<label>
Stack:
<input type="text" formControlName="stack" [formControl]="bioSection.controls.stackDetails.controls.stack">
</label> <br>
<label>
Experience:
<input type="text" formControlName="experience" [formControl]="bioSection.controls.stackDetails.controls.experience">
</label>
</div>
<div [formGroup]="bioSection.controls.address">
<h3>Address</h3>
<label>
Country:
<input type="text" [formControl]="bioSection.controls.address.controls.country">
</label> <br>
<label>
City:
<input type="text" formControlName="city" [formControl]="bioSection.controls.address.controls.city">
</label>
</div>
<button type="submit">Submit Application</button>
</form>
Если вы запустите приложение, в браузере вы увидите примерно такую форму:
Добавление асинхронных данных в форму
Иногда при создании формы вы ещё не знаете всех данных, которые планируете показать пользователю. Например, данные могут подгружаться асинхронно — с API или из другой части приложения.
К счастью, в Angular реализовать это довольно просто. Давайте настроим пример, чтобы продемонстрировать этот сценарий. В файле employee.component.ts
мы укажем длинный список языков программирования:
programmingLanguages = [
'JavaScript',
'Python',
'Java',
'C#',
'C++',
'Ruby',
'Swift',
'Kotlin',
'TypeScript',
'HTML',
'CSS',
'PHP',
'Go',
'Rust',
'Objective-C',
'Scala',
'Shell',
'PowerShell',
'Perl',
'Lua',
'Haskell',
'Dart',
'Groovy',
'R',
'MATLAB',
'CoffeeScript',
'F#',
'Clojure',
'Elixir',
'Julia',
'Haxe',
'Fortran',
'Ada',
'COBOL',
'Lisp',
'Scheme',
'Prolog',
'Bash',
'Assembly',
'Smalltalk',
'Erlang',
'OCaml',
'VHDL',
'Verilog',
'PL/I',
'Ada',
'ABAP',
'ActionScript'
];
Теперь нужно определить два интерфейса: один для описания текущего «состояния» запроса, а второй — для списка языков программирования:
export interface LanguageResponse {
state: LanguageResponseState,
languages: Array<string>
}
export enum LanguageResponseState {
EMPTY,
LOADING,
LOADED,
}
Теперь давайте используем сигнал, чтобы управлять асинхронными данными в шаблоне. При инициализации сигнал будет находиться в состоянии EMPTY
, так как на этом этапе данные ещё не загружены:
filteredLanguages = signal<LanguageResponse>({state: LanguageResponseState.EMPTY, languages: []});
Внутри функции ngOnInit()
мы хотим подписаться на значения, которые эмиттирует FormGroup
. Однако нам не нужно отправлять запрос к API каждый раз, когда пользователь нажимает клавишу.
Чтобы избежать лишних обращений к серверу, мы реализуем «дебаунс» — подпишемся на изменения формы и пропустим их через оператор задержки. Это обеспечит запуск запроса только через полсекунды после последнего изменения формы:
ngOnInit() {
this.bioSection.valueChanges.pipe(debounceTime(500)).subscribe(async x => {
if (x.languageSearch){ // если указано значение для поиска языка
await this.searchForLanguage(x.languageSearch) // выполняем поиск языка
}
else{ // если язык не указан — сбрасываем состояние в "пустое"
this.filteredLanguages.set({
state: LanguageResponseState.EMPTY,
languages: []
})
}
})
}
async searchForLanguage(search: string) {
// устанавливаем сигнал в состояние "загрузка"
this.filteredLanguages.set({state: LanguageResponseState.LOADING, languages: []});
// ждём 2 секунды — имитируем запрос к веб-API
await new Promise(resolve => setTimeout(resolve, 2000));
// устанавливаем результаты — языки, соответствующие поисковому запросу
this.filteredLanguages.set({
state: LanguageResponseState.LOADED,
languages: this.programmingLanguages.filter(x => x.toLowerCase().indexOf(search.toLowerCase()) !== -1)
});
}
}
Теперь нам нужно добавить поле поиска и список для выбора языков в HTML-шаблоне. К счастью, новая синтаксическая конструкция управления потоком, появившаяся в Angular 17, делает это очень просто:
<label>
Language:
<input [formControl]="bioSection.controls.languageSearch" >
</label>
@if (filteredLanguages().state === response.EMPTY){
Please specify a language to search for.
}
@if (filteredLanguages().state === response.LOADING){
Loading languages...
} @else if (filteredLanguages().state === response.LOADED){
<select size="5">
@for (language of filteredLanguages().languages; track language)
{
<option>{{language}}</option>
} @empty {
<p>No languages match...</p>
}
</select>
}
Результат такой:
Как добавить FormControl в FormGroup
Чтобы добавить, обновить или удалить контролы внутри FormGroup
, используйте следующие методы:
addControl()
— добавляет контрол и обновляет его значение и валидность.removeControl()
— удаляет контрол из группы.setControl()
— заменяет существующий контрол новым.contains()
— проверяет, есть ли активный контрол с заданным именем.registerControl()
— регистрирует контрол, но в отличие от других методов, не обновляет его значение и валидность.
Как установить значение в FormGroup
В Angular можно установить значения как для отдельных контролов, так и для всей группы FormGroup
целиком. Чтобы изменить только часть значений в группе, используйте метод patchValue
:
this.myFormGroup.patchValue({
formControlName1: myValue1,
// formControlName2: myValue2
});
Не обязательно указывать все значения при использовании patchValue
— поля, которые вы не задаёте, останутся без изменений.
А чтобы установить значения для всех контролов внутри FormGroup
одновременно, используйте setValue
:
this.myFormGroup.setValue({
formControlName1: myValue1,
formControlName2: myValue2
});
Что такое FormBuilder в Angular
Настраивать form control’ы вручную может быть утомительно, особенно если вы работаете с большой формой. FormBuilder
в Angular упрощает этот процесс и помогает избежать повторяющегося шаблонного кода.
Проще говоря, FormBuilder
— это синтаксический сахар, который облегчает создание экземпляров FormControl
, FormGroup
и FormArray
, снижая объём рутинной работы при построении сложных форм.
Когда вы работаете с большими формами, оптимизация производительности становится особенно важной. Вот несколько техник, которые стоит учитывать.
Используйте стратегию обнаружения изменений OnPush
По умолчанию Angular выполняет проверку всех компонентов при каждом цикле обнаружения изменений. В случае больших форм это может приводить к снижению производительности. Стратегия OnPush
позволяет сократить количество таких проверок — Angular будет проверять компонент только тогда, когда изменяются его входные свойства.
Дебаунс значений формы
Если поля формы используют асинхронные операции, такие как вызов внешнего API, рекомендуется использовать дебаунс. Это позволит отсрочить выполнение действий до стабилизации пользовательского ввода, избежав лишней нагрузки и повысив отзывчивость интерфейса.
Используйте trackBy с FormArray
Если вы применяете FormArray
для работы с динамическими списками, используйте trackBy
в *ngFor
, чтобы Angular не перерисовывал весь список при изменении одного элемента. Функция trackBy
— это чистая функция, возвращающая уникальный идентификатор, с помощью которого Angular определяет, какие элементы были изменены.
Нововведение в Angular 18: единый поток событий изменения состояния контрола
До Angular 18 для отслеживания изменений состояния form control’ов приходилось подписываться на отдельные стримы, такие как statusChanges
и valueChanges
. Это усложняло код и часто приводило к ошибкам.
В Angular 18 появилось улучшение для реактивных форм — единый поток событий изменения состояния контрола. Эта функция упрощает отслеживание изменений, связанных с form control’ами.
Теперь в классе AbstractControl
появилось новое свойство events
— это Observable
, который объединяет в себе отслеживание всех важных изменений: значения, валидации, касания и других состояний.
В поток events
входят следующие типы событий:
FormSubmittedEvent
— вызывается при отправке формы.FormResetEvent
— вызывается при сбросе формы.TouchChangeEvent
— вызывается при изменении состояния касания (touched).ValueChangeEvent
— вызывается при изменении значения.PristineChangeEvent
— вызывается при изменении pristine-состояния.StatusChangeEvent
— вызывается при изменении статуса валидации.
С новым API достаточно одной подписки, чтобы отслеживать все изменения состояния контрола. Например, можно добавить следующий код в employee.component.ts
:
ngOnInit() {
this.bioSection.get('firstName')?.events.subscribe((event) => {
console.log(event);
});
}
Приведенный выше пример позволяет отслеживать изменения значения, статуса валидации и других событий у контрола firstName
. Если ввести значение в поле firstName
, можно увидеть соответствующий вывод в консоли:
Вывод в консоль
Новая возможность — единый events
-поток — делает работу с событиями формы проще и нагляднее: теперь можно реактивно обрабатывать все ключевые изменения состояния в одном месте.
Часто задаваемые вопросы по реактивным формам в Angular
В чём разница между FormControlName и FormControl?
FormControlName
— это директива, которая связывает экземпляр FormControl
с элементом шаблона в HTML. А FormControl
— это программный объект, представляющий отдельный контрол формы: он хранит значение, статус валидации и информацию о взаимодействии пользователя.
Оба можно использовать для представления контрола в HTML-шаблоне:
// Use FormControl
<input type="text" [formControl]="bioSection.controls.firstName">
// use FormControlName
<form [formGroup]="bioSection">
<input type="text" formControlName="firstName">
</form>
Обратите внимание: директиву FormControlName
необходимо использовать только внутри родительского элемента с директивой formGroup
.
В чём разница между FormControl, FormGroup и FormArray?
FormControl
, FormGroup
и FormArray
— это базовые строительные блоки реактивных форм в Angular. Каждый из них отвечает за разные аспекты управления формой:
FormControl
— управляет значением и валидацией одного поля формы.FormGroup
— контейнер, который объединяет несколькоFormControl
-ов в логическую группу. Позволяет управлять набором связанных полей как единой сущностью.FormArray
— контейнер, похожий наFormGroup
, но предназначен для управления массивом контролов. Особенно удобен, когда в форме есть повторяющиеся элементы.
Как применить условную валидацию в FormGroup на основе других значений?
Чтобы настроить условную валидацию полей в зависимости от других значений формы, можно использовать новый поток events
, объединяющий все изменения состояния контрола.
Например, допустим, мы хотим, чтобы поле licenseNumber
становилось обязательным только в том случае, если значение поля age
больше 18:
ngOnInit() {
this.bioSection.get('age')?.events.subscribe((event) => {
if (parseInt(event.source.value as string) > 18) {
this.bioSection.get('licenseNumber')?.addValidators(Validators.required);
} else {
this.bioSection.get('licenseNumber')?.removeValidators(Validators.required);
}
this.bioSection.get('licenseNumber')?.updateValueAndValidity();
});
}
Здесь мы подписываемся на поток events
у контрола age
, который находится внутри FormGroup
под названием bioSection
. Когда значение age
становится больше 18, мы добавляем валидатор required
к контролу licenseNumber
. Обратите внимание, что после изменения валидаторов мы вызываем updateValueAndValidity
, чтобы принудительно обновить статус валидации licenseNumber
и отразить изменения.
Заключение
В этом руководстве мы разобрали всё, что нужно знать о form control’ах в Angular: как использовать FormControl
, как объединять поля с помощью FormGroup
, и почему важно собирать контролы в группы для удобной работы с формой в целом.