+5

Thử Nghiệm Với Angular – Forms Trong Angular

Thử Nghiệm Với Angular – Forms Trong Angular

Hầu hết các ứng dụng web hiện đại đều làm việc với forms để thu thập dữ liệu từ người dùng. Angular cung cấp cho chúng ta hai phương pháp để tạo forms, một là Template-driven forms (mà có thể bạn đã quen thuộc từ Angularjs) và hai là Reactive forms hay Model-driven forms.

Trong bài này chúng ta sẽ cùng tìm hiểu cách tạo forms trong Angular, và để bắt đầu chúng ta sẽ tìm hiểu Template-driven forms.

1. Template-Driven Forms Trong Angular

Template-driven forms là phương pháp mà chúng ta sẽ tạo forms dựa vào template (giống như trong Angularjs). Chúng ta thực hiện việc thêm các directives và hành vi vào template, sau đó Angular sẽ tự động tạo forms để quản lý và sử dụng.

1.1 Template

Giả sử chúng ta có template form như sau:

<form novalidate (submit)="onSubmit()" class="row justify-content-md-center">
  <div class="col-md-8">
    <div class="form-group row">
      <label for="example-text-input" class="col-md-2 col-form-label">Name:</label>
      <div class="col-md-10">
        <input class="form-control" type="text" id="example-text-input">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-email-input" class="col-md-2 col-form-label">Email:</label>
      <div class="col-md-10">
        <input class="form-control" type="email" id="example-email-input">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-fb" class="col-md-2 col-form-label">Facebook:</label>
      <div class="col-md-10">
        <input class="form-control" type="url" id="example-url-fb">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-twt" class="col-md-2 col-form-label">Twitter:</label>
      <div class="col-md-10">
        <input class="form-control" type="url" id="example-url-twt">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-web" class="col-md-2 col-form-label">Website:</label>
      <div class="col-md-10">
        <input class="form-control" type="url" id="example-url-web">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-tel-input" class="col-md-2 col-form-label">Tel:</label>
      <div class="col-md-10">
        <input class="form-control" type="tel" id="example-tel-input">
      </div>
    </div>
    <div class="form-group row">
      <div class="col-md-10 offset-md-2">
        <button class="btn btn-primary" type="submit">Submit</button>
      </div>
    </div>
  </div>
</form>

Trên đây chỉ là một form HTML thông thường, khi browser render chúng ta sẽ có form trông giống như sau:

Contact App Form

1.2 Import APIs cho Template-driven forms

Để có thể sử dụng các APIs mà Angular cung cấp cho việc thao tác với Template-driven forms, chúng ta cần import NgModule là FormsModule từ package @angular/forms như sau:

import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [...],
  imports: [
    ...,
    FormsModule
  ],
  providers: [...],
  bootstrap: [...]
})
export class AppModule { }

1.3 ngForm và ngModel directives

Nhiệm vụ đầu tiên chúng ta cần làm là truy cập vào form instance và gán các control vào form. Chúng ta sẽ lần lượt sử dụng ngFormngModel directives như sau:

<form novalidate #form="ngForm" ...>
</form>

Angular cung cấp một giải pháp để có thể truy cập được directive/component instance ở trong template của component bằng cách sử dụng exportAs trong khai báo directive/component metadata.

Ở trong đoạn code phía trên, chúng ta đã tạo một template variable là form, nó sẽ là một instance của directive ngForm, như thế chúng ta có thể sử dụng các public API mà directive này cung cấp như lấy ra value của nó chẳng hạn như form.value.

Giờ đây chúng ta có thể sử dụng form value cho việc submit form chẳng hạn.

<form novalidate #form="ngForm"
  (submit)="onSubmit(form.value)" ...>
</form>
 
<p>Form value:</p>
<pre>{{ form.value | json }}</pre>

Và để dễ dàng trong quá trình development, chúng ta có thể thêm một phần hiển thị ở template để biết được form value đang có gì như hai dòng code cuối ở trên.

Tại thời điểm này, cho dù chúng ta có form control ở template nhưng Angular không thể biết cái nào cần quản lý nên chúng ta chỉ nhận được một object rỗng.

Bây giờ công việc tiếp theo là chúng ta phải nói cho Angular biết các form control nào cần phải quản lý. Đây chính là lúc chúng ta dùng đến ngModel directive.

Chúng ta sẽ thêm ngModel vào các control như sau:

<input class="form-control" type="text" ngModel ...>

Nhưng nếu bạn không khai báo attribute name cho form control, bạn sẽ gặp phải một lỗi giống như sau:

Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as ‘standalone’ in ngModelOptions.

Kèm với đó là bạn sẽ có các ví dụ để sửa lỗi trên.

OK, chúng ta cần thêm một số config để Angular biết cách tạo ra form control của nó để quản lý. Và chúng ta sẽ thêm attribute name cho các form control ở template trên.

<input class="form-control" type="text" ngModel name="contact-name" ...>

Bây giờ quan sát form value chúng ta sẽ có một object có key contact-name chẳng hạn:

{
  "contact-name": ""
}

Nếu bạn quen với camel case, chúng ta có thể sửa đổi chút để object của chúng ta có key nhìn quen thuộc hơn chẳng hạn.

<input class="form-control" type="text" ngModel name="contactName" ...>

Kết quả nhận được:

{
  "contactName": ""
}

OK cool, giờ chúng ta có thể cài đặt tương tự cho các phần tử khác của form.

Và khi bạn nhập giá trị cho các control thì Angular sẽ tự cập nhật cho các control của form tương ứng, chẳng hạn sau khi nhập xong và submit thì form sẽ có value như sau:

{
  "contactName": "Tiep Phan",
  "email": "abc@deg.com",
  "facebook": "facebook.com",
  "twitter": "twitter.com",
  "website": "tiepphan.com",
  "tel": "1234-5678-90"
}

Bây giờ có một tình huống phát sinh là bạn cần bind data cho các control với một dữ liệu có sẵn, lúc này chúng ta sẽ dùng đến binding cho property, và property chúng ta nhắc đến ở đây chính là ngModel.

Chúng ta có dạng binding quen thuộc như sau:

[ngModel]="obj.prop"

Giả sử object mà chúng ta có ở đây có dạng:

contact = {
  "contactName": "Tiep Phan",
  "email": "abc@deg.com",
  "facebook": "facebook.com",
  "twitter": "twitter.com",
  "website": "tiepphan.com",
  "tel": "1234-5678-90"
}

Template của chúng ta sẽ thay đổi như sau:

<input [ngModel]="contact.contactName" name="contactName" class="form-control" type="text" ...>

Mọi thứ đều bắt nguồn từ những điều cơ bản nhất, [ngModel] chính là one-way binding mà chúng ta vẫn thường dùng.

Lưu ý rằng, khi bạn update form control, bản thân control được form quản lý sẽ thay đổi – form.value, nhưng object contact ở trên sẽ không hề hấn gì, vì chúng ta không hề đụng chạm gì tới nó, chúng ta chỉ binding một chiều, mà không binding ngược trở lại. Điều này dẫn đến chúng ta có thêm một dạng khác của ngModel đó là cú pháp two-way binding [(ngModel)].

<input [(ngModel)]="contact.contactName" name="contactName" class="form-control" type="text" ...>

Như vậy, nếu bạn không cần binding thì chỉ cần thêm ngModel là đủ, nếu bạn muốn one-way binding thì sử dung [ngModel], cuối cùng là muốn dùng two-way binding với [(ngModel)]. Mặc dù vậy, khi bạn thay đổi form control value, thì Angular sẽ cập nhật lại giá trị của các control của form mà nó đang quản lý.

1.4 ngModelGroup directive

Đến lúc này, chúng ta vẫn đang chỉ quản lý form control với một object chứa tất cả các keys cần thiết, vậy làm thế nào chúng ta có thể gom nhóm một số key lại thành một group riêng, câu trả lời là ngModelGroup. Directive này tạo ra một group lồng vào group cha, giống như object nằm trong một object khác.

Giả sử như template kể trên, chúng ta sẽ nhóm các url thành một nhóm có tên là social chẳng hạn:

<fieldset ngModelGroup="social">
  <div class="form-group row">
    <label for="example-url-fb" class="col-md-2 col-form-label">
      Facebook:
    </label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-fb"
        ngModel name="facebook">
    </div>
  </div>
  <div class="form-group row">
    <label for="example-url-twt" class="col-md-2 col-form-label">
      Twitter:
    </label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-twt"
        ngModel name="twitter">
    </div>
  </div>
  <div class="form-group row">
    <label for="example-url-web" class="col-md-2 col-form-label">
      Website:
    </label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-web"
        ngModel name="website">
    </div>
  </div>
</fieldset>

Kết quả thu được chúng ta có form value với cấu trúc:

{
  "contactName": "",
  "email": "",
  "social": {
    "facebook": "",
    "twitter": "",
    "website": ""
  },
  "tel": ""
}

1.5 Submit form

Lưu ý rằng phần nội dung này có thể áp dụng cho cả Reactive forms mà chúng ta sẽ tìm hiểu tiếp theo.

Ở phần trước, chúng ta đã listen event submit của form, nhưng ngoài ra, còn một event khác cũng được fired ra khi thực hiện submit form, đó là ngSubmit. Vậy có điều gì khác biệt giữa submitngSubmit?

Giống như submit, event ngSubmit cũng thực hiện hành động khi form thực hiện submit – người dùng nhấn vào button submit chẳng hạn. Nhưng ngSubmit sẽ thêm một số nhiệm vụ để đảm bảo form của bạn không thực hiện submit form theo cách thông thường – tải lại trang sau khi submit.

Giả sử, chúng ta thực hiện một tác vụ nào đó trong hàm listen form submit mà sinh ra exception, lúc này nếu bạn sử dụng submit, trang web của bạn sẽ reload, còn nếu bạn sử dụng ngSubmit, nó sẽ không reload – phiên bản lúc này tôi đang sử dụng.

onSubmit(formValue) {
  // Do something awesome
  console.log(formValue);
  throw Error('something go wrong');
}

Lời khuyên dành cho bạn là nên dùng ngSubmit cho việc listen form submit.

1.6 Template-driven error validation

– Huh, có vẻ như user nhập sai thông tin rồi, làm sao tôi có thể hiển thị thông báo cho họ biết để sửa đây?

– Bình tĩnh, Angular đã có sẵn tính năng cơ bản cho việc validation của bạn. Bây giờ hãy bắt đầu với việc kiểm tra lỗi và cảnh báo.

Angular cung cấp một số Validators cơ bản mà bạn có thể dùng ngay trong template như: required, minlength, maxlength, pattern và từ Angular v4 trở đi có thêm email. Chúng được viết là các directives, nên bạn có thể sử dụng như các directives khác trong template của bạn.

Chúng ta sẽ bỏ qua validation của HTML5, vậy nên ngay từ đầu form chúng ta đã thêm novalidate attribute vào khai báo form, thay vào đó là sử dụng Angular validation.

Giả sử chúng ta cần cài đặt contactName là required, chúng ta sẽ đặt như sau:

<input ngModel name="contactName" required class="form-control" type="text" ...>

Và để dễ dàng quan sát, chúng ta sẽ thêm phần hiển thị lỗi như sau:

<p>Form contactName errors:</p>
<pre>{{ form.controls.contactName?.errors | json }}</pre>

Chúng ta sử dụng safe navigation operator để truy cập property của một object có thể bị null/undefined mà không gây ra lỗi chương trình.

Khi không nhập gì vào input contactName, chúng ta có thể thấy một thông báo lỗi như sau:

{
  "required": true
}

Khi input này được nhập dữ liệu thì chúng ta sẽ thấy key required của object trên sẽ bị xóa bỏ.

Công việc của chúng ta bây giờ là sử dụng ngIf chẳng hạn để show/hide error cho người dùng được biết.

<div class="col alert alert-danger" role="alert"
  *ngIf="form.controls.contactName?.errors?.required">
  Name is required!
</div>

Và có thể thêm việc không cho người dùng nhấn button submit khi trạng thái của form là invalid như sau:

<button class="btn btn-primary" type="submit"
  [disabled]="form.invalid">
  Submit
</button>

Bây giờ, nếu bạn muốn chỉ hiển thị thông báo error khi người dùng đã focus vào input đó mà không nhập gì, lúc này chúng ta có thể thông qua trạng thái của form bằng cách truy cập các propeties như touched, dirty hay pristine, …

Trong đó:

  • touched: true nếu người dùng đã focus vào input rồi không focus vào nữa.
  • untouched: true nếu người dùng chưa đụng chạm gì hoặc lần đầu tiên focus và chưa bị mất focus (ngược lại với touched)
  • dirty: true nếu người dùng đã tương tác với control – nhập một ký tự vào input text chẳng hạn.
  • pristine: true nếu người dùng chưa tương tác gì với control, mặc dù có thể đã touched, nhưng chưa sửa đổi gì.

Chúng ta sẽ thay đổi một chút form validation:

<div class="col alert alert-danger" role="alert"
  *ngIf="form.controls.contactName?.errors?.required && form.controls.contactName?.touched">
  Name is required!
</div>

OK cool, giờ đây chỉ khi nào người dùng touched vào control và có error thì validation message sẽ hiển thị.

– Huh, giờ chúng ta muốn dùng template variable để truy cập control thay vì truy cập dài dài như trên có được không?

– Câu trả lời là có, chúng ta hoàn toàn có thể dùng template variable như sau:

<input ngModel name="contactName" required #contactName="ngModel" class="form-control" type="text" ...>

Và sử dụng như một template variable thông thường:

<div class="col alert alert-danger" role="alert"
  *ngIf="contactName?.errors?.required && contactName?.touched">
  Name is required!
</div>

Như vậy, việc sử dụng Template-driven form trong Angular khá dễ dàng, trong phần này chúng ta chưa đề cập đến custom validation cho form control. Thay vào đó chúng ta sẽ có một phần riêng để thảo luận về tính năng này.

2. Reactive Forms Trong Angular

Thuật ngữ Reactive Forms hay còn được gọi là Model-Driven Forms, là một phương pháp để tạo form trong Angular, phương pháp này tránh việc sử dụng các directive ví dụ như ngModel, required, etc, thay vào đó tạo các Object Model ở trong các Component, rồi tạo ra form từ chúng. Một điều lưu ý đó là Template-Driven là async còn Reactive là sync.

Trong Reactive forms, chúng ta tạo toàn bộ form control tree ở trong code (khởi tạo ngay, khởi tạo trong constructor, hoặc khởi tạo trong ngOnInit), nên có thể dễ dàng truy cập các phần tử của form ngay tức thì.

Trong Template-driven forms, chúng ta ủy thác việc tạo form control cho directives, để tránh bị lỗi changed after checked, directives cần một cycle nữa để build toàn bộ form control tree. Vậy nên bạn cần đợi một tick nữa để có thể truy cập vào các phẩn tử của form. Chính điều này khiến việc test template-driven form trở nên phức tạp hơn.

Bạn có thể tham khảo thêm tại document sau: https://angular.io/docs/ts/latest/guide/reactive-forms.html#!#async-vs-sync

2.1 Các thành phần cơ bản của form

  • AbstractControl là một abstract class cho 3 lớp con form control: FormControl, FormGroup, và FormArray. Nó cung cấp các hành vi, thuộc tính chung cho các lớp con.

  • FormControl là đơn vị nhỏ nhất, nó lưu giữ giá trị và trạng thái hợp lệ của một form control. Tương ứng với một HTML form control như input, select.

  • FormGroup nó lưu giữ giá trị và trạng thái hợp lệ của một nhóm các đối tượng thuộc AbstractControl – có thể là FormControl, FormGroup, hay FormArray – đây là một dạng composite. Ở level cao nhất của một form trong component của bạn là một FormGroup.

  • FormArray nó lưu giữ giá trị và trạng thái hợp lệ của một mảng các đối tượng thuộc AbstractControl giống như FormGroup. Nó cũng là một dạng composite. Nhưng nó không phải là thành phần ở level cao nhất.

2.2 Template

Cũng giống như trong Template-driven forms, chúng ta sẽ sử dụng một template giống thế.

<form novalidate (ngSubmit)="onSubmit()"
  class="row justify-content-md-center">
  <div class="col-md-8">
    <div class="form-group row">
      <label for="example-text-input" class="col-2 col-form-label">
        Name:
      </label>
      <div class="col-10">
        <input class="form-control" type="text"
          id="example-text-input">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-email-input" class="col-2 col-form-label">
        Email:
      </label>
      <div class="col-10">
        <input class="form-control" type="email"
          id="example-email-input">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-fb" class="col-2 col-form-label">
        Facebook:
      </label>
      <div class="col-10">
        <input class="form-control" type="url" id="example-url-fb">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-twt" class="col-2 col-form-label">
        Twitter:
      </label>
      <div class="col-10">
        <input class="form-control" type="url" id="example-url-twt">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-url-web" class="col-2 col-form-label">
        Website:
      </label>
      <div class="col-10">
        <input class="form-control" type="url" id="example-url-web">
      </div>
    </div>
    <div class="form-group row">
      <label for="example-tel-input" class="col-2 col-form-label">
        Tel:
      </label>
      <div class="col-10">
        <input class="form-control" type="tel" 
          id="example-tel-input">
      </div>
    </div>
    <div class="form-group row">
      <div class="col-10 offset-2">
        <button class="btn btn-primary"
          type="submit">Submit</button>
      </div>
    </div>
  </div>
</form>

Trên đây chỉ là một form HTML thông thường, khi browser render chúng ta sẽ có form trông giống như sau:

Contact App Form

Nhưng trong template trên chúng ta mặc định sử dụng ngSubmit, các bạn có thể trở lại bài trước để hiểu về ngSubmit vs submit event.

2.3 Import APIs cho Reactive forms

Để có thể sử dụng các APIs mà Angular cung cấp cho việc thao tác với Reactive forms, chúng ta cần import NgModule là ReactiveFormsModule từ package @angular/forms như sau:

Lưu ý rằng bạn có thể import cả Reactive form và Template-driven form modules.

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [...],
  imports: [
    ...,
    ReactiveFormsModule
  ],
  providers: [...],
  bootstrap: [...]
})
export class AppModule { }

2.4 Khởi tạo form trong Component

Như đã nói ở phần trước, chúng ta có thể khởi tạo form trong Component ở một trong 3 giai đoạn: khởi tạo ngay lúc khai báo property, trong constructor, trong ngOnInit. Để thống nhất về code, chúng ta sẽ khởi tạo trong ngOnInit.

Giả sử chúng ta tạo một component như sau:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'tpc-contact-reactive-form',
  templateUrl: './contact-reactive-form.component.html',
  styleUrls: ['./contact-reactive-form.component.scss']
})
export class ContactReactiveFormComponent implements OnInit {
  rfContact: FormGroup;
  constructor() { }

  ngOnInit() {
    this.rfContact = new FormGroup({
      contactName: new FormControl(),
      email: new FormControl(),
      social: new FormGroup({
        facebook: new FormControl(),
        twitter: new FormControl(),
        website: new FormControl()
      }),
      tel: new FormControl()
    });
  }

  onSubmit() {
    // Do something awesome
    console.log(this.rfContact);
  }
}

2.5 Binding controls

Ở top-level (form), chúng ta sẽ bind một formGroup như sau:

<form novalidate (ngSubmit)="onSubmit()" [formGroup]="rfContact"
  class="row justify-content-md-center">
</form>

Lần lượt với các FormControl instances, chúng ta sẽ bind với property formControlName như sau:

<input class="form-control" type="text" id="example-text-input"
  formControlName="contactName">

Đối với formGroup lồng trong formGroup như cấu trúc ở trên, chúng ta có thêm một property cần bind là formGroupName:

<fieldset formGroupName="social">
  <div class="form-group row">
    <label for="example-url-fb" class="col-md-2 col-form-label">Facebook:</label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-fb" formControlName="facebook">
    </div>
  </div>
  <div class="form-group row">
    <label for="example-url-twt" class="col-md-2 col-form-label">Twitter:</label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-twt" formControlName="twitter">
    </div>
  </div>
  <div class="form-group row">
    <label for="example-url-web" class="col-md-2 col-form-label">Website:</label>
    <div class="col-md-10">
      <input class="form-control" type="url" id="example-url-web" formControlName="website">
    </div>
  </div>
</fieldset>

Việc binding property vào template cần map cấu trúc cây DOM giống với cấu trúc của model ở component, bạn không thể binding một property không thuộc về một control, chẳng hạn ở model trên, control facebook thuộc về group social, nên bạn không thể bind control này trực tiếp vào group rfContact.

2.6 Reactive Forms Validator

Việc thêm Validator vào cho một control rất đơn giản, việc của bạn là thêm giá trị tham số tiếp theo như sau:

Lưu ý: có 2 loại validators, một là validator validator đồng bộ (sync), ví dụ required, minlenghth, etc. Hai là async validator (validator bất đông bộ) chẳng hạn như validator để xem user tồn tại chưa, lúc này bạn gọi AJAX để thực hiện tìm kiếm và đó là một quá trình xử lý bất đồng bộ. Các class FormControl và FormGroup cho phép thêm validator theo thứ tự sync – async vào danh sách tham số của constructor.

export declare class FormGroup extends AbstractControl {
  constructor(controls: {
    [key: string]: AbstractControl;
  }, validator?: ValidatorFn | null,
     asyncValidator?: AsyncValidatorFn | null);
}

Tương tự cho FormArray.

Và FormControl:

export declare class FormControl extends AbstractControl {
  constructor(formState?: any,
    validator?: ValidatorFn | ValidatorFn[] | null,
    asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
}

Giả sử chúng ta thêm required cho form control contactName ở trên, chúng ta chỉ cần thêm vào constructor như sau:

contactName: new FormControl('', Validators.required)

Với Validators.required là một validator function.

Hoặc có thể thêm nhiều validator với Validators.compose, hoặc truyền vào là một mảng các validator functions:

contactName: new FormControl('',
                 [Validators.required, Validators.minLength(3)])

Ở đoạn code trên, chúng ta gộp requiredminLength(3) cho control contactName, hoặc bạn có thể thêm nhiều các validator function nữa theo ý muốn.

Để hiển thị thông báo cho người dùng biết về lỗi, chúng ta có thể sử dụng phương pháp như đã đề cập trong phần Template-driven form.

<div class="col alert alert-danger" role="alert"
  *ngIf="rfContact.controls.contactName?.errors?.required
         && rfContact.controls.contactName?.touched">
  Name is required!
</div>

Hoặc sử dụng hasError:

<div class="col alert alert-danger" role="alert"
  *ngIf="rfContact.controls.contactName?.hasError('required')
         && rfContact.controls.contactName?.touched">
  Name is required!
</div>

Không những thế, Reactive form còn cho phép chúng ta lấy ra một control theo path đến nó một cách dễ dàng, ví dụ để lấy về contactName:

rfContact.get('contactName')

Hoặc với control facebook:

rfContact.get('social.facebook')
// or
rfContact.get(['social', 'facebook'])

Với phương pháp này chúng ta không cần tạo nhiều template variable nữa.

2.7 Form Builder

Một trong những tính năng tuyệt vời của Angular form là Form Builder, nó giúp bạn tạo form model một cách nhanh chóng và tiện lợi. Việc bạn phải làm là inject class FormBuilder vào class bạn muốn tạo form, và gọi các API như group, array, control để tạo form.

constructor(private fb: FormBuilder) { }
ngOnInit() {
  this.rfContact = this.fb.group({
    contactName: this.fb.control('', [Validators.required, Validators.minLength(3)]),
    email: this.fb.control(''),
    social: this.fb.group({
      facebook: this.fb.control('', [Validators.required, Validators.minLength(3)]),
      twitter: this.fb.control(''),
      website: this.fb.control('')
    }),
    tel: this.fb.control('')
  });
}

Hoặc một cách rút gọn hơn nữa:

this.rfContact = this.fb.group({
  contactName: ['', [Validators.required, Validators.minLength(3)]],
  email: '',
  social: this.fb.group({
    facebook: ['', [Validators.required, Validators.minLength(3)]],
    twitter: '',
    website: ''
  }),
  tel: ''
});

Wow, quá tuyệt vời phải không nào.

2.8 FormArray

Giả sử bây giờ bạn muốn người dùng có thể nhập vào một hoặc nhiều số điện thoại. Vậy có cách nào tạo form với số lượng thay đổi như thế hay không. Rất may cho chúng ta, Reactive form có một loại control là FormArray, nó sẽ giúp chúng ta làm được việc đó, bây giờ hãy thay đổi code một chút.

ngOnInit() {
  this.rfContact = this.fb.group({
    // ...
    tels: this.fb.array([
      this.fb.control('')
    ])
  });
}

get tels(): FormArray {
  return this.rfContact.get('tels') as FormArray;
}

addTel() {
  this.tels.push(this.fb.control(''));
}

removeTel(index: number) {
  this.tels.removeAt(index);
}

Và template của chúng ta sẽ có:

<div formArrayName="tels" *ngIf="tels.controls.length">
  <div class="form-group row" *ngFor="let c of tels.controls; index as i">
    <label for="example-tel-input" class="col-md-2 col-form-label">Tel:</label>
    <div class="col-md-10">
      <input class="form-control" type="tel" id="example-tel-input" [formControlName]="i">
      <div class="text-right">
        <button class="btn btn-info" (click)="addTel()">+</button>
        <button class="btn btn-danger" (click)="removeTel(i)" *ngIf="tels.controls.length > 1">-</button>
      </div>
    </div>
  </div>
</div>

Khi render chúng ta sẽ có form có dạng:

Contact App Form

Ở đoạn code trên chúng ta đã tạo ra một FormArray instance và khi binding vào template chúng ta thông báo nó với directive formArrayName. Khi thực hiện việc lặp, chúng ta tạo ra biến index có tên là "i", với mỗi biến index như thế, Angular sẽ lưu trữ tương ứng với một phần tử trong FormArray là một AbstractControl instance, trong trường hợp này của chúng ta là một FormControl instance, vậy nên chúng ta có đoạn binding property như sau: [formControlName]="i".

FormArray cung cấp một số phương thức cho phép chúng ta thêm, xóa phần tử trong array như insert, push, removeAt. Hay phương thức at để lấy ra phần tử ở vị trí cụ thể. Chúng sẽ có ích khi bạn sử dụng trong ứng dụng của mình – trong trường hợp ở trên chúng ta có dùng để thêm hoặc xóa.

2.9 Forms with a single control

Giả sử bạn có một form search chẳng hạn, bạn chỉ có một form control, lúc này bạn không muốn phải bao cả form và form group, có cách nào làm được thế không.

Đây là lúc formControl directive phát huy tác dụng. Chúng ta hãy xem xét search form sau đây.

<input type="search" class="form-control" [formControl]="searchControl">

Còn đây là component code:

export class SearchComponent implements OnInit {
  searchControl = new FormControl();
  ngOnInit() {
    this.searchControl.valueChanges.subscribe(value => {
      // do search with value here
      console.log(value);
    });
  }
}

Giờ đây, bạn có thể tạo ra một form seach cực kỳ nhanh và đơn giản phải không nào.

2.10 Cập nhật giá trị cho form, control

Có 2 phương thức để cập nhật giá trị cho form control được mô tả bởi class AbstractControlsetValuepatchValue. Chúng là các abstract method, vậy nên các class dẫn xuất sẽ phải implement riêng cho chúng.

Đối với class FormControl, không có gì khác biệt giữa 2 phương thức – thực chất patchValue gọi lại setValue.

Đối với các class FormGroupFormArray, patchValue sẽ cập nhật các giá trị được khai báo tương ứng trong object value truyền vào. Nhưng setValue sẽ báo lỗi nếu một control nào bị thiếu.

Vậy nên nếu bạn muốn cập nhật một phần của form thì hãy dùng patchValue, nếu bạn muốn set lại tất cả và đảm bảo không cái nào bị thiếu thì dùng setValue để tận dụng việc báo lỗi của nó.

Ngoài ra, còn có phương thức reset để bạn có thể reset lại trạng thái lúc khởi tạo của form hoặc control.

3. Custom Forms Validation Trong Angular

Custom Forms Validation Trong Angular

4. Video bài học

Template-driven forms trong Angular

Reactive forms trong Angular

5. Tham khảo:

Source:

Thử Nghiệm Với Angular Phần 17 – Template-Driven Forms Trong Angular Thử Nghiệm Với Angular Phần 18 – Reactive Forms Trong Angular

Supported Validators: https://angular.io/docs/ts/latest/api/#!?query=validator&type=directive

Forms documentation: https://angular.io/docs/ts/latest/guide/forms.html

Reactive Forms documentation: https://angular.io/docs/ts/latest/guide/reactive-forms.html

Git repo: https://github.com/tieppt/try-angular/tree/lesson-17 https://github.com/tieppt/try-angular/tree/lesson-18


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí