Skip to content

Commit 32905da

Browse files
committed
wip
1 parent 822c047 commit 32905da

2 files changed

Lines changed: 339 additions & 0 deletions

File tree

apps/forms/lecture/advanced.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
soon

apps/forms/lecture/basics.md

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
## Template driven forms
2+
Это подход при котором логика формы, типы контролов, валидаторы и тд описываются в шаблоне, практически без использования TypeScript. Возьмем пример
3+
4+
```typescript
5+
import { Component } from '@angular/core';
6+
import { FormsModule, NgForm } from '@angular/forms';
7+
8+
@Component({
9+
selector: 'login',
10+
styles: `
11+
input {
12+
border: 1px solid #ddd;
13+
padding: 5px 15px;
14+
border-radius: 5px;
15+
display: block;
16+
margin-bottom: 5px;
17+
}
18+
19+
.error {
20+
font-size: 12px;
21+
color: red;
22+
}
23+
`,
24+
standalone: true,
25+
imports: [FormsModule],
26+
template: `
27+
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
28+
<input
29+
name="email"
30+
[(ngModel)]="model.email"
31+
required
32+
email
33+
#emailField="ngModel"
34+
/>
35+
@if (emailField.invalid && emailField.touched) {
36+
<p class="error">Введите корректный email</p>
37+
}
38+
39+
<input
40+
name="password"
41+
type="password"
42+
[(ngModel)]="model.password"
43+
required
44+
minlength="8"
45+
/>
46+
47+
<button type="submit" [disabled]="form.invalid">Войти</button>
48+
</form>
49+
`,
50+
})
51+
export class LoginComponent {
52+
model = { email: '', password: '' };
53+
54+
onSubmit(form: NgForm) {
55+
if (form.valid) {
56+
console.log(form.value); // { email: '...', password: '...' }
57+
}
58+
}
59+
}
60+
61+
```
62+
63+
Здесь нужно обратить внимание на следующее:
64+
1. Доступ к форме у нас происходит через шаблонную переменную `#form`
65+
2. Доступ к форм-контролам у на спроисходит так же, через шаблонные переменные, например `emailField`. Таким образом можно получать статус валидности контрола, ошибки и тд
66+
3. Атрибут name на форм-контроле обязатален - Angular использует его как ключ в объекте формы.
67+
4. Правила валидации каждого форм-контрола мы указываем как атрибут dom-элемента. Например на контроле пароля это required и minLength, а на контроле почты - это email. Ангуляр под капотом автоматически навешивает стандартные функции валидации
68+
Такой подход хорош для простых форм. Простых как по структуре так и по логике. Но когда все становится сложнее, то такими формами крайне недобно управлять, расширять их, обвешивать всякой доп. логикой. Например при таком подходе будет крайне неудобно программно установить значение в контрол. Это в принципе возможно сделать, через viewChild, но придется писать много кода, по сути не особо то нужного. Плюсом к этому, чтобы получить контрол через viewChild вам сначала нужно дождаться полного рендера формы, соотв ДО рендера, ничего сделать не получится, а часто это нужно.
69+
70+
Чтобы убедиться в хрупкости такого подхода, вот вам задача:
71+
1. После инициализации формы, нужно выждать 3 секунды и установить в контрол почты значение `lazy-value@mymail.ru`
72+
## Reactive forms
73+
Реактивный подход к построению форм немного другой: сначала описываешь в TS структуру формы, правила валидации а потом к этой форме и форм-контролам привязываешь дом-элементы.
74+
75+
Angular предоставляет три класса для построения модели формы. Все они наследуются от `AbstractControl`:
76+
77+
78+
```
79+
AbstractControl
80+
├── FormControl — одно поле
81+
├── FormGroup — группа полей (объект)
82+
└── FormArray — массив полей
83+
```
84+
85+
Думайте об этом как о структуре данных: `FormGroup` — это объект, `FormArray` — массив, `FormControl` — примитив. Любую форму можно описать их комбинацией.
86+
87+
### FormGroup & FormControl
88+
1. Описываем форму через классы FormGroup и FormControl
89+
2. Привязываем форм-контролы к дом-элементам через директивы formGroup и formControlName
90+
91+
```typescript
92+
import { Component } from '@angular/core';
93+
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
94+
95+
@Component({
96+
selector: 'app-reactive-login',
97+
standalone: true,
98+
imports: [ReactiveFormsModule],
99+
template: `
100+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
101+
<input formControlName="email" placeholder="Email" />
102+
<input formControlName="password" type="password" placeholder="Пароль" />
103+
<button type="submit" [disabled]="form.invalid">Войти</button>
104+
</form>
105+
`
106+
})
107+
export class ReactiveLoginComponent {
108+
form = new FormGroup({
109+
email: new FormControl('', [Validators.required, Validators.email]),
110+
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
111+
});
112+
113+
// Геттеры — чтобы не писать this.form.controls.email везде
114+
get email() { return this.form.controls.email; }
115+
get password() { return this.form.controls.password; }
116+
117+
onSubmit() {
118+
if (this.form.valid) {
119+
console.log(this.form.value);
120+
// { email: 'user@example.com', password: 'secret123' }
121+
}
122+
}
123+
}
124+
```
125+
126+
Если сравнить с прошлым подходом, то видно что из HTML ушло много лишней информации, стало поменьше магии. Теперь у нас остались просто привязки контролов к дом-элементам. Плюс ко всему мы можем сконфигурировать форму и менять ее ДО рендеринга HTML, что было невозможно в подходе Template driven.
127+
128+
Валидаторы больше не описываются в HTML, они описываются функциями в TS.
129+
130+
До v14 значения форм было нетипизированным и это доставляло много проблем, начиная с Angular v14 эту проблему исправили и теперь крайне рекомендуется типизировать значения форм-контролов
131+
132+
```ts
133+
const form = new FormGroup({
134+
age: new FormControl<number>(0),
135+
name: new FormControl<string>(''),
136+
});
137+
```
138+
139+
140+
141+
### FormArray
142+
Часто бывают случаи, когда набор вашей формы меняется пользователем в рантайме. Например когда пользователь указывает несколько телефонов для связи. Если при таком кейсе использовать FormGroup то пришлось бы как то вручную управлять добавлением контролов в группу, удалением их оттуда и тд. Можно, но неэффективно и неудобно. Для таких задач существует FormArray
143+
144+
```ts
145+
@Component({
146+
template: `
147+
<form [formGroup]="form">
148+
<div formArrayName="phones">
149+
@for (phone of phones.controls; track $index) {
150+
<div>
151+
<input [formControlName]="$index" placeholder="Телефон {{ $index + 1 }}" />
152+
<button (click)="removePhone($index)">✕</button>
153+
</div>
154+
}
155+
</div>
156+
<button (click)="addPhone()">+ Добавить телефон</button>
157+
</form>
158+
`
159+
})
160+
export class ContactFormComponent {
161+
form = new FormGroup({
162+
name: ['', Validators.required],
163+
phones: new FormArray([new FormControl('')]),
164+
});
165+
166+
get phones() {
167+
return this.form.controls.phones;
168+
}
169+
170+
addPhone() {
171+
this.phones.push(new FormControl(''));
172+
}
173+
174+
removePhone(index: number) {
175+
this.phones.removeAt(index);
176+
}
177+
}
178+
```
179+
180+
Обратите внимание: в шаблоне `formArrayName="phones"` указывает Angular где искать массив, а `[formControlName]="i"` — привязка по числовому индексу, а не по имени.
181+
У FormArray для управления коллекцией уже есть встроенные методы:
182+
- push
183+
- removeAt
184+
- insert
185+
Следует использовать именно их, для добавления или удаления контролов
186+
187+
## Обработка значения контролов и формы
188+
Так как и FormControl и FormGroup унаследованы от класса AbstractControl то АПИ по работе со значениями одинаков:
189+
- setValue() - установка нового значения
190+
- patchValue() - установка значения конкретного поля формы
191+
- reset() - сброс значения формы
192+
Как правило вы будете устанавливать значения конкретьного контрола через форму, а не напрямую через конкретный FormControl. Например вернемся к нашей прошлой форме логина
193+
```ts
194+
form = new FormGroup({
195+
email: new FormControl('', [Validators.required, Validators.email]),
196+
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
197+
});
198+
199+
form.setValue({ email: 'a@b.com', password: '123' });
200+
form.patchValue({ email: 'a@b.com' });
201+
```
202+
203+
- `setValue` требует передать значения для **всех** полей группы, иначе выбросит ошибку.
204+
- `patchValue` обновляет только те поля, которые вы передали. На практике `patchValue` используется чаще — например, когда вы заполняете форму данными с сервера, где могут быть не все поля.
205+
Одно из самых главных преимуществ реактивного построения форм - это подписка на изменения контролов. Реактивные формы — это Observable. Каждый `FormControl` и `FormGroup` предоставляет два потока: `valueChanges` и `statusChanges`. Вы можете подписаться на них и реагировать на любые изменения.
206+
```typescript
207+
// Реагируем на изменение конкретного поля
208+
this.form.controls.email.valueChanges.subscribe(value => {
209+
console.log('email changed:', value);
210+
});
211+
212+
// Реагируем на любое изменение в форме
213+
this.form.valueChanges.subscribe(formValue => {
214+
console.log('form changed:', formValue);
215+
});
216+
217+
// Реагируем на изменение статуса (valid/invalid/pending)
218+
this.form.statusChanges.subscribe(status => {
219+
// 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'
220+
});
221+
```
222+
223+
Раз у нас valueChanges это поток (Observable) то мы можем использовать весь арсенал RxJS-операторов для обработки: фильтровать (filter), комбинировать значения вместе (combineLatest) и тд, в зав-ти от задачи
224+
225+
## Валидация
226+
Angular предоставляет набор готовых валидаторов в классе `Validators`:
227+
228+
```typescript
229+
import { Validators } from '@angular/forms';
230+
231+
Validators.required // поле не должно быть пустым
232+
Validators.email // валидный email
233+
Validators.minLength(n) // минимальная длина строки
234+
Validators.maxLength(n) // максимальная длина строки
235+
Validators.min(n) // минимальное числовое значение
236+
Validators.max(n) // максимальное числовое значение
237+
Validators.pattern(regexp) // соответствие регулярному выражению
238+
Validators.nullValidator // всегда валидно (заглушка)
239+
```
240+
241+
Несколько валидаторов передаются массивом — Angular применяет их все и объединяет ошибки:
242+
243+
```typescript
244+
new FormControl('', [Validators.required, Validators.email, Validators.maxLength(100)])
245+
```
246+
247+
Встроенных валидаторов часто недостаточно, но в Ангуляре можно создать свой валидатор очень просто. Это обычная функция с чётко определённой сигнатурой.
248+
249+
Валидатор принимает `AbstractControl` и возвращает либо объект с ошибками, либо `null` если всё хорошо. Ключи объекта ошибок — это то, что вы будете проверять через `ctrl.errors?.['ERROR_KEY']`.
250+
251+
```typescript
252+
import { AbstractControl, ValidationErrors } from '@angular/forms';
253+
254+
// Простой валидатор-функция
255+
function noSpaces(control: AbstractControl): ValidationErrors | null {
256+
if (control.value?.includes(' ')) {
257+
return { noSpaces: true }; // ключ ошибки - noSpaces
258+
}
259+
return null; // всё хорошо, значение валидно
260+
}
261+
262+
// Валидатор-фабрика — принимает параметры и возвращает функцию-валидатор
263+
function forbiddenValue(forbidden: string) {
264+
return (control: AbstractControl): ValidationErrors | null => {
265+
if (control.value === forbidden) {
266+
return { forbiddenValue: { value: control.value } };
267+
}
268+
return null;
269+
};
270+
}
271+
272+
// Использование
273+
const ctrl = new FormControl('', [
274+
Validators.required,
275+
noSpaces,
276+
forbiddenValue('admin'),
277+
]);
278+
```
279+
280+
Заметьте разницу: `noSpaces` передаётся как ссылка на функцию, а `forbiddenValue('admin')` — вызывается и возвращает функцию. Это стандартный паттерн: когда валидатору нужны параметры, он реализуется как фабрика.
281+
## Статусы формы и контролов
282+
Каждый `FormControl` — это не просто хранилище значения. Это полноценный объект состояния, который отслеживает историю взаимодействия пользователя с полем.
283+
284+
```typescript
285+
const ctrl = new FormControl('');
286+
287+
// Статус валидации
288+
ctrl.valid // true если все валидаторы прошли
289+
ctrl.invalid // !valid
290+
ctrl.pending // true если асинхронный валидатор ещё работает
291+
ctrl.disabled // поле отключено
292+
293+
// История взаимодействия
294+
ctrl.pristine // пользователь ещё ни разу не менял значение
295+
ctrl.dirty // пользователь хоть раз изменил значение
296+
ctrl.touched // пользователь покинул поле (сработал blur)
297+
ctrl.untouched
298+
299+
// Данные
300+
ctrl.value // текущее значение
301+
ctrl.errors // { required: true } | null
302+
```
303+
304+
Зачем нам нужны `touched` и `dirty`? Представьте: пользователь только что открыл форму. Все поля пустые — значит, `required` сразу показывает ошибки. Но показывать ошибки до того, как пользователь вообще попытался что-то заполнить — плохой UX. Поэтому стандартный паттерн: показывать ошибки только после `touched` (пользователь посетил поле и ушёл).
305+
306+
Как используется:
307+
```html
308+
@if (email.invalid && email.touched) {
309+
<div>Введите корректный email</div>
310+
}
311+
312+
<!-- Плохо: ошибка сразу при загрузке страницы -->
313+
@if (email.invalid) {
314+
<div>Введите корректный email</div>
315+
}
316+
```
317+
318+
Но это еще не всё! Angular автоматически добавляет и убирает CSS-классы на элементе формы, отражая его состояние. Вам не нужно делать это вручную:
319+
320+
```
321+
ng-valid / ng-invalid
322+
ng-pristine / ng-dirty
323+
ng-touched / ng-untouched
324+
```
325+
326+
Это позволяет стилизовать поля декларативно:
327+
328+
```css
329+
/* Красная рамка только на полях, которые пользователь тронул и заполнил неверно */
330+
input.ng-invalid.ng-touched {
331+
border-color: red;
332+
}
333+
334+
/* Зелёная рамка для корректно заполненных полей */
335+
input.ng-valid.ng-dirty {
336+
border-color: green;
337+
}
338+
```

0 commit comments

Comments
 (0)