
Разработчики 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 } } ... }
Полный код примера из статьи
Спасибо за внимание, надеюсь эта информация окажется для кого-то полезной.
ссылка на оригинал статьи https://habr.com/ru/post/720084/
Добавить комментарий