Разработчики angular, как правило знают, что для работы с формами существует два подхода: reactive forms и template driven forms. Также, хорошо известно, что для работы с формами разработан такой функционал как валидация, однако исчерпывающе описано его применения для подхода reactive forms. Давайте рассмотрим как можно получить те же преимущества для template driven подхода.
Допустим, у нас есть поле ввода.
<input [(ngModel)]="item.name" />
Мы хотим, чтобы оно было не менее N символов в длину, а в случае ошибки - добавить класс error. Конечно, можно сделать это напрямую, но давайте воспользуемся механизмом валидации angular. Создадим функцию валидатор.
export function minLengthValidator(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if(control.value?.length >= 3) {
return null;
}
return {
minLength: `Min length is ${minLength}`
};
};
}
Далее, чтобы добавить валидатор в шаблоне компонента - создадим директиву
@Directive({
selector: '[appMinLength]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: MinLengthDirective,
multi: true,
},
],
})
export class MinLengthDirective implements Validator {
@Input() appMinLength: number;
validate(control: AbstractControl): ValidationErrors | null {
if (this.appMinLength === null || this.appMinLength === undefined || this.appMinLength < 1) {
return null;
}
return minLengthValidator(this.appMinLength)(control);
}
}
Теперь мы можем использовать валидатор через директиву в шаблоне
<input [(ngModel)]="item.name" [appMinLength]="10" />
Благодаря тому что у директивы NgModel указано свойство exportAs, мы можем получить к ней доступ в шаблоне, вот как это выглядит в исходниках angular
@Directive({
selector: '[ngModel]:not([formControlName]):not([formControl])',
providers: [formControlBinding],
exportAs: 'ngModel'
})
export class NgModel extends NgControl implements OnChanges, OnDestroy
Тут же мы видим, что NgModel наследует от NgControl.
Итак, получим доступ к нашему контролу, и добавим класс с ошибкой, если контрол трогали, и его значение является неверным.
<input #model="ngModel"
[(ngModel)]="item.name"
[appMinLength]="10"
[class.error]="model.control.invalid && model.control.touched" />
Уже недурно, но давайте рассмотрим случай, когда часть редактируемых полей находится в дочерних компонентах, а мы хотим управлять всем этим делом из родительского. Например у нас есть модель данных, которая содержит массив дочерних элементов, каждый из которых мы хотим провалидировать и, допустим, не дать сохранить данные если что-то заполнено неверно.
export interface MyModel {
name: string;
items: MyItem[];
}
export interface MyItem {
name: string;
age: number;
}
Далее рассмотрим компонент формы
@Component({
selector: 'app-my-form',
template: `<form #form>
<div class="row">
<div style="width: 70px;">name:</div>
<input [class.error]="model.control.invalid && model.control.touched"
#model="ngModel"
type="text"
required
[appMinLength]="10"
[(ngModel)]="data.name"
name="name" />
</div>
<div>Items:</div>
<app-my-item [item]="item" *ngFor="let item of data.items"></app-my-item>
<div>
<button type="button" (click)="save()">Save</button>
<button type="button" (click)="add()">Add</button>
</div>
</form>`,
styleUrls: ['./my-form.component.css'],
imports: [FormsModule, MyItemComponent, CommonModule, MinLengthDirective],
standalone: true,
})
export class MyFormComponent {
@ViewChild(NgForm) form: NgForm;
data: MyModel = {
name: '',
items: [
{
age: null,
name: '',
},
],
};
save() {
console.log(this.form.controls);
}
add() {
this.data.items.push({
name: '',
age: null,
});
}
}
И компонент для элемента списка
@Component({
selector: 'app-my-item',
template: `<div class="row">
<div style="width: 70px;">name:</div>
<input [class.error]="nameModel.control.invalid && nameModel.control.touched"
#nameModel="ngModel"
type="text"
required
[appMinLength]="10"
[(ngModel)]="item.name" />
</div>
<div class="row">
<div style="width: 70px;">age:</div>
<input [class.error]="ageModel.control.invalid && ageModel.control.touched"
type="number"
#ageModel="ngModel"
type="text"
required
[(ngModel)]="item.age" />
</div>`,
styleUrls: ['./my-item.component.css'],
imports: [FormsModule, MinLengthDirective, CommonModule],
standalone: true,
})
export class MyItemComponent {
@Input() item: MyItem;
}
На данный момент наши контролы уже подсветятся ошибкой, но при нажатии на save() мы увидим только один контрол. Однако хочется получить доступ к состоянию формы с учетом дочерних компонент. Для этого, чтобы получить доступ к родительской форме нам нужно внедрить ControlContainer в дочерние компоненты. Немного изменим декларацию компонента MyItem
@Component({
selector: 'app-my-item',
template: `<div [ngModelGroup]="item.id.toString()">
<div class="row">
<div style="width: 70px;">name:</div>
<input [class.error]="nameModel.control.invalid && nameModel.control.touched"
name="name"
#nameModel="ngModel"
type="text"
required
[appMinLength]="10"
[(ngModel)]="item.name" />
</div>
<div class="row">
<div style="width: 70px;">age:</div>
<input [class.error]="ageModel.control.invalid && ageModel.control.touched"
name="age"
type="number"
#ageModel="ngModel"
type="text"
required
[(ngModel)]="item.age" />
</div>
</div>`,
styleUrls: ['./my-item.component.css'],
imports: [FormsModule, MinLengthDirective, CommonModule],
standalone: true,
viewProviders: [
{
provide: ControlContainer,
useExisting: NgForm,
},
],
})
export class MyItemComponent {
@Input() item: MyItem;
}
Теперь у нас есть полный контроль над элементами формы, и ее состоянием, можно дописать функционал компонента формы, с валидацией и реакцией на ее состояние
@Component({
selector: 'app-my-form',
...
})
export class MyFormComponent {
...
save() {
console.log(this.form.controls);
this.form.form.markAllAsTouched();
this.form.form.updateValueAndValidity();
if (this.form.valid) {
// Do the saving stuff
}
}
...
}
Полный код примера из статьи
Спасибо за внимание, надеюсь, эта информация окажется для кого-то полезной.