Angular forms

В Angular прежде чем использовать формы в компонентах, надо импортировать в главном модуле AppModule модуль FormsModule, который позволяет работать с формами:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
 
import { FormsModule }   from '@angular/forms';
 
@NgModule({
    imports:      [ BrowserModule, FormsModule],
    declarations: [ AppComponent],
    bootstrap:    [ AppComponent ]
})
export class AppModule { }

Кроме того, в файле конфигурации приложения package.json среди списка используемых зависимостей должен быть указан пакет "angular/forms":

{
    "name": "helloapp",
    "version": "1.0.0",
    "scripts": {
        "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
        "lite": "lite-server",
        "tsc": "tsc",
        "tsc:w": "tsc -w"
    },
    "dependencies": {
        "@angular/forms": "~4.0.0",
        // остальные пакеты
    },
    "devDependencies": {
       // остальные пакеты
    }
}

При работе с формами ключевым моментом является использование директивы NgModel. Эта директива с помощью переданной модели создает объект FormControl и привязывает эту модель к созданному элементу формы. Объект FormControl отслеживает значение модели, а также отвечает за валидацию этого значения и взаимодействие с пользователем.

Данная директива принимает переданную ей модель в качестве входного свойства. Причем мы можем использовать как однонаправленную, так и двунаправленную привязку.

//one-way	
<input name="title" [ngModel]="title" />

//two-way	
<input name="title" [(ngModel)]="title" />
//Phone.component
import {Component} from '@angular/core';

@Component({
    selector: 'phone',
    templateUrl: "app/phone.component.html"
})
export class Phone {
    public phones:any[] = [];
    public companies:string[] = [
        "Apple",
        "Huawei",
        "Xiaomi",
        "Samsung",
        "LG",
        "Motorola",
        "Alcatel"
    ];

    public addPhone(title:string, price:number, company:string) {
        this.phones.push({title, price, company});
    }
}
//phone.component.html
<label>Название модели</label>
<input name="title" [(ngModel)]="title"/>
<label>Цена</label>
<input 
    type="number" 
     name="price" 
    [(ngModel)]="price"/>
<label>Производитель</label>
<select name="company" [(ngModel)]="company">
    <option *ngFor="let comp of companies" [value]="comp">
        {{comp}}
    </option>
</select>
<button
        (click)="addPhone(title, price, company)"
>Добавить </button>
<h3>Добавленные элементы</h3>
<ul *ngFor="let p of phones">
    <li>{{p.title}} ({{p.company}}) - {{p.price}}</li>
</ul>

Local example (phone.component.ts)

Кроме создания привязки директива ngModel позволяет определить объект NgModel, который будет связан с определенным элементом ввода.

//phone2.component.ts
import { Component} from '@angular/core';
import { NgModel} from '@angular/forms';

@Component({
    selector: 'phone2',
    templateUrl: "app/phone2.component.html"
})
export class Phone2 {

    public phone: any ={
        title: "", 
        price: 0, 
        company: "Samsung"
    };
    public companies: string[] = [
        "Apple",
        "Huawei",
        "Xiaomi",
        "Samsung",
        "LG",
        "Motorola",
        "Alcatel"
    ];

    public addPhone(title:NgModel, 
        price: NgModel, 
        comp: NgModel){
            console.log(title);
            console.log(price);
            console.log(comp);
    }
}
//phone2.component.html
<label>Название модели</label>
<input name="title"
       [(ngModel)]="phone.title" #phoneTitle="ngModel"/>
<label>Цена</label>
<input type="number" name="price"
       [(ngModel)]="phone.price" #phonePrice="ngModel"/>
<label>Производитель</label>
<select name="company"
        [(ngModel)]="phone.company" #phoneCompany="ngModel">
    <option *ngFor="let comp of companies" [value]="comp">
        {{comp}}
    </option>
</select>
<button (click)="addPhone(phoneTitle, phonePrice, phoneCompany)">
    Добавить
</button>
<div>
    <p>{{phoneTitle.name}} : {{phoneTitle.model}}</p>
    <p>{{phonePrice.name}} : {{phonePrice.model}}</p>
    <p>{{phoneCompany.name}} : {{phoneCompany.model}}</p>
</div>

Local example (phone2.component.ts)

Change and ngModelChange

//template
<input name="title"
       [(ngModel)]="phone.title" #phoneTitle="ngModel"
        (change)="onTitleChange()" />
<label>Цена</label>
<input type="number" name="price"
       [(ngModel)]="phone.price" #phonePrice="ngModel"
        (ngModelChange)="onModelChange()"/>
//component
public onTitleChange():void {
        if (this.phone.title) {
            console.log("title", this.phone.title);
        }
    }

    public onModelChange(): void {
        if (this.phone.price) {
            console.log("price", this.phone.price);
        }
    }

Local example (phone2)!!!

Form Validation

Состояние модели

Применение директивы ngModel не только устанавливает привязку данных, но и позволяет отслеживать состояние элемента ввода. Для установки состояния Angular применяет к элементам ввода специальные классы CSS:

  • Если элемент ввода еще не получал фокус, то устанавливается класс ng-untouched. Если же поле ввода уже получало фокус, то к нему применяется класс ng-touched. При этом получение фокуса не обязательно должно сопровождаться изменением значения в этом поле.

  • Если первоначальное значение в поле ввода было изменено, то устанавливается класс ng-dirty. Если же значение не изменялось с момента загрузки страницы, то к элементу ввода применяется класс ng-pristine

  • Если значение в поле ввода корректно, то применяется класс ng-valid. Если же значение некорректно, то применяется класс ng-invalid

<input class="form-control" name="title" [(ngModel)]="title" />

//generated
<input class="form-control ng-untouched ng-pristine ng-valid" name="title" ng-reflect-name="title" />

Validation

В Angular 2 мы можем использовать валидацию HTML5, которая применяется в виде атрибутов:

  • required: требует обязательного ввода значения

  • pattern: задает регулярное выражение, которому должны соответствовать вводимые данные

//template
<input name="title"
       [(ngModel)]="phone.title"
       #phoneTitle="ngModel"
       (change)="onTitleChange()"
        required/>
<span *ngIf="phoneTitle.invalid && phoneTitle.touched"
      class="alert alert-danger">
    Title is required
</span>
<label>Цена</label>
<input type="number"
       name="price"
       [(ngModel)]="phone.price"
       #phonePrice="ngModel"
       (ngModelChange)="onModelChange()"
       required
       pattern="[0-9]{3}"/>
<span *ngIf="phonePrice.invalid && phonePrice.touched"
      class="alert alert-danger">
    Price is invalid
</span>

Как правило, при работе с формами все элементы ввода не определяются сами по себе, а помещаются в стандартный элемент формы - <form></form>. Применение данного элемента позволяет управлять всеми элемента ввода вцелом как одной общей формой.

Непосредственно в Angular для работы с формой определена специальная директива NgForm. Она создает объект FormGroup и привязывает его к форме, что позволяет отслеживать состояние формы, управлять ее валидацией.

//user-from template
<form #myForm="ngForm" novalidate>
    <label>Имя</label>
    <input
            class="form-control"
            name="name"
            [(ngModel)]="name"
            required/>
    <label>Email</label>
    <input
            class="form-control"
            name="email" ngModel
            required
            pattern="[a-zA-Z_]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}"/>
    <label>Телефон</label>
    <input
            class="form-control"
            name="phone"
            ngModel
            required pattern="[0-9]{10}"/>
    <button 
            [disabled]="myForm.invalid"
            class="btn btn-default"
            (click)="submit(myForm)">Добавить
    </button>
</form>
<div>Имя: {{myForm.value.name}}</div>
<div>Email: {{myForm.value.email}}</div>
//user-from component
import { Component} from '@angular/core';
import { NgForm} from '@angular/forms';

@Component({
    selector: 'user-form',
    styleUrls: ["app/user.component.css"],
    templateUrl: "app/user.component.html"
})
export class UserFormComponent {
    public submit(form: NgForm){
        console.log(form);
    }
}

Data-

Driven

В прошлых слайдах был подход Template-Driven, который концентрировался вокруг шаблона компонента: для работы с формой и ее элементами в шаблоне компонента к элементам html применялись директивы NgModel и NgForm, правила валидации задавались в тегах элементов с помощью атрибутов required и pattern. Но есть альтернативный подход - Data-Driven. Рассмотрим, в чем он заключается.

При подходе Data-Driven для формы создается набор объектов FormGroup и FormControl. Сама форма и ее подсекции представляют класс FormGroup, а отдельные элементы ввода - класс FormControl

<form [formGroup]="myForm" novalidate (ngSubmit)="submit()">
    <label>Имя</label>
    <input name="name" formControlName="userName"/>
    <div class="alert alert-danger"
         *ngIf="myForm.controls['userName'].invalid && myForm.controls['userName'].touched">
        Не указано имя
    </div>
    <label>Email</label>
    <input name="email" formControlName="userEmail"/>
    <div class="alert alert-danger"
         *ngIf="myForm.controls['userEmail'].invalid && myForm.controls['userEmail'].touched">
        Некорректный email
    </div>
    <label>Телефон</label>
    <input name="phone" formControlName="userPhone"/>
    <button [disabled]="myForm.invalid">Отправить</button>
</form>
import { Component} from '@angular/core';
import { FormGroup, FormControl, Validators} from '@angular/forms';

@Component({
    selector: 'user-form-2',
    styleUrls: ["app/user.component.css"],
    templateUrl: "app/user2.component.html"
})
export class UserFrom2Component {
    public myForm : FormGroup;
    constructor(){
        this.myForm = new FormGroup({
            "userName": new FormControl("Tom", Validators.required),
            "userEmail": new FormControl("", [
                Validators.required,
                Validators.pattern("[a-zA-Z_]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}")
            ]),
            "userPhone": new FormControl()
        });
    }

    public submit(){
        console.log(this.myForm);
    }
}

Custom validator

По сути валидатор представляет обычный метод - в данном случае метод userNameValidator. В качестве параметра он принимает элемент формы, к которому этот валидатор применяется, а на выходе возвращает объект, где ключ - строка, а значение равно true.

В данном случае проверяем, если значение равно строке "нет", то возвращаем объект {"userName": true}. Значение true указывает, что элемент формы не прошел валидацию. Если же все нормально, то возвращаем null.

export class UserFrom2Component {
    public myForm : FormGroup;
    constructor(){
        this.myForm = new FormGroup({
            "userName": new FormControl("Tom", [Validators.required, this.userNameValidator]),
            "userEmail": new FormControl("", [
                Validators.required,
                Validators.pattern("[a-zA-Z_]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}")
            ]),
            "userPhone": new FormControl()
        });
    }

    public submit(){
        console.log(this.myForm);
    }

    private userNameValidator(control: FormControl): {[s:string]:boolean}{
        if(control.value==="нет"){
            return {"userName": true};
        }
        return null;
    }

FormArray

<form [formGroup]="myForm" novalidate (ngSubmit)="submit()">
    <label>Имя</label>
    <input class="form-control" name="name" formControlName="userName"/>

    <div class="alert alert-danger"
         *ngIf="myForm.controls['userName'].invalid && myForm.controls['userName'].touched">
        Не указано имя
    </div>
    <label>Email</label>
    <input class="form-control" name="email" formControlName="userEmail"/>

    <div class="alert alert-danger"
         *ngIf="myForm.controls['userEmail'].invalid && myForm.controls['userEmail'].touched">
        Некорректный email
    </div>
    <div formArrayName="phones">
        <div class="form-group" 
        *ngFor="let phone of myForm.controls['phones'].controls; let i = index">
            <label>Телефон</label>
            <input class="form-control" formControlName="{{i}}"/>
        </div>
    </div>
    <button class="btn btn-default" (click)="addPhone()">
        Добавить телефон
    </button>
    <button class="btn btn-default" [disabled]="myForm.invalid">
        Отправить
    </button>
</form>

Некоторые элементы на форме могут относиться к одному и тому же признаку. Например, в анкете пользователя могут попросить указать номера телефоно, которыми он владеет. Их может быть несколько, но они будут представлять один и тот же признак - "номера телефонов". То есть логично было бы объединить все поля для ввода номеров телефонов в массив. И в Angular 2 мы легко можем реализовать подобную возможность с помощью класса FormArray.

export class FormArrayComponent {
    public myForm:FormGroup;

    constructor() {
        this.myForm = new FormGroup({

            "userName": new FormControl("Tom", [Validators.required]),
            "userEmail": new FormControl("", [
                Validators.required,
                Validators.pattern("[a-zA-Z_]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}")
            ]),
            "phones": new FormArray([
                new FormControl("+375", Validators.required)
            ])
        });
    }

    public addPhone() {
        (<FormArray>this.myForm.controls["phones"]).push(
            new FormControl("+375", Validators.required)
        );
    }

    public submit() {
        console.log(this.myForm);
    }
}

Form Array

Local example: form-array.componetn.ts

FormBuilder

FormBuilder передается в качестве сервиса в конструктор. С помощью метода group() создается объект FormGroup. Каждый элемент передается в форму в виде обычного массива значений

export class FormBuildComponent {
    private formBuilder: FormBuilder;
    public myForm : FormGroup;

    constructor(formBuilder: FormBuilder){
        this.formBuilder = formBuilder;

        this.myForm = formBuilder.group({
            "userName": ["Tom", [Validators.required]],
            "userEmail": ["", [
                Validators.required,
                Validators.pattern("[a-zA-Z_]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}")
            ]],
            "phones": formBuilder.array([
                ["+375", Validators.required]
            ])
        });
    }
    public addPhone(){
        (<FormArray>this.myForm.controls["phones"]).push(
            new FormControl("+375", Validators.required)
        );
    }
    public submit(){
        console.log(this.myForm);
    }
}
Made with Slides.com