Angular. Работа с template-driven формами

от автора

Разработчики 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *