Bài viết được dịch từ: barbarianmeetscoding.com

Đây là bài thứ 5 trong loạt bài viết: "Bắt đầu với Angular 2 từng bước một". Bạn có thể xem các bài viết khác ở đây:

Ở bài trước bạn đã học về Angular 2 routing và trong bài này bạn sẽ học về cách tạo các form và validate form trong Angular 2.

Code mẫu

Bạn có thể tải toàn bộ code mẫu ở GitHub repo.

Ứng dụng của chúng ta cho tới thời điểm này

Hiện tại, chúng ta đã phát triển một ứng dụng Angular 2 nhỏ. Với 2 component PeopleListComponent hiển thị một danh sách các nhân vật và PersonDetailComponent hiển thị thông tin chi tiết về nhân vật mà bạn chọn.

Chúng ta sử dụng Angular 2 routing để điều hướng giữa 2 view, danh sách các nhân vật là view mặc định và render ngay sau khi ứng dụng khởi tạo.

Chúng ta muốn lưu dữ liệu của mình!

Cho tới thời điểm này, chúng ta chỉ đọc thông tin và đã cảm thấy điều này thật tẻ nhạt! Chúng ta muốn có thể viết và lưu dữ liệu của mình.

Để làm được điều đó chúng ta cần chuyển đổi PersonDetailComponent thành một form và thêm một vài validation để đảm bảo thông tin chúng ta lưu là chính xác.

Khi hoàn thành, form chi tiết của chúng ta sẽ giống như thế này:
 

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

Trước hết! Chúng ta cần thêm module Forms tới ứng dụng của mình!

Chúng ta sẽ cần import FormsModule trong app.module.ts:

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

Và chỉ định nó như một dependency thông qua thuộc tính imports:

@NgModule({
  imports: [ BrowserModule, routing, FormsModule ], // here!
  declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Toàn bộ app.module.ts sẽ trông như thế này:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { routing } from './app.routes';

import { AppComponent }  from './app.component';
import { PeopleListComponent } from './people-list.component';
import { PersonDetailsComponent } from './person-details.component';

@NgModule({
  imports: [ BrowserModule, routing, FormsModule ],
  declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Sử dụng một file template thay vì inline template

Điều đầu tiên chúng ta làm, là tách template của PersonDetailsComponent thành một file riêng.

Làm thế nào chúng ta có thể sử dụng một file template thay vì inline template?

Decorator @Component có thuộc tính tempalteUrl ngoài thuộc tính template chúng ta đã sử dụng. Sử dụng thuộc tính này để nói với Angular 2 file HTML template chúng ta sẽ sử dụng cho component. Hãy tách template của PersonDetailsComponent vào file person-details.component.ts:

 

<section *ngIf="person">
  <h2>You selected: {{person.name}}</h2>
  <h3>Description</h3>
  <p>
    {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
  </p>
</section>
<button (click)="gotoPeoplesList()">Back to peoples list</button>

Và cập nhật PersonDetailsComponent:

// some imports

@Component({
  selector: 'person-details',
  templateUrl: 'app/person-details.component.html',    // <=== update this
})
export class PersonDetailsComponent implements OnInit {
    // some code...
}

Một Form cơ bản trong Angular 2

Bây giờ, như đã đề cập, hãy thêm một HTML form tới view của chúng ta. 

Chúng ta cập nhật template như thế này:

<!-- new syntax for ng-if -->
<section *ngIf="person">
  <section>
    <h2>You selected: {{person.name}}</h2>
    <h3>Description</h3>
    <p>
      {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>
  <section>
    <form>
      <div>
        <label for="name">Name: </label>
        <input type="text" name="name">
      </div>
      <div>
        <label for="weight">Weight: </label>
        <input type="number" name="weight">
      </div>
      <div>
        <label for="height">Height: </label>
        <input type="number" name="height">
      </div>
    </form>
  </section>

  <button (click)="gotoPeoplesList()">Back to peoples list</button>
<section>

Ok, hiện tại form chưa hiển thị thông tin gì. Hãy sử dụng một vài thứ chúng ta đã học như event và property binding để hiển thị thông tin. Chúng ta có thể làm như sau:

<div>
    <label for="name">Name: </label>
    <input type="text" name="name" [value]="person.name" (change)="person.name = name.value" #name>
</div>

Thuộc tính [value] để liên kết thuộc tính person.name của component tới phần tử input, và chúng ta sử dụng (change) event binding để cập nhật thuộc tính name của person

Có một vài thứ khá lạ trong đoạn code trên, đó là #name trong phần tử input. Angular 2 gọi là một biến template cục bộ (template local variable) và thường được sử dụng trong template bên ngoài code của component.

Trong trường hợp này, #name tham chiếu tới chính phần tử input. Đó là lý do tại biểu thức person.name = name.value giúp cập nhật tên của person khi chúng ta thay đổi giá trị của input.

NgModel và Angular 2 two-way data binding.

ngModel data binding rất giống ng-model của Angular 1, nó giúp chúng ta thành lập two-way data binding giữa component và template. Two-way data binding đồng bộ hóa các thay đổi cả ở component và template.

Cú pháp sẽ như thế này:

<div>
    <label for="name">Name: </label>
    <input type="text" name="name" [(ngModel)]="person.name">
</div>

Trước khi tiếp tục hãy xem xét đoạn code này một chút [(ngModel)]="person.name" từng phần một.

Chúng ta đã học event bindings liên kết một chiều từ template tới component và biểu diễn bằng dấu ngoặc tròn (). Chúng ta cũng đã học property bindings liên kết một chiều từ component tới template biểu diễn bằng dấu ngoặc vuông []. Vì thế, liên kết 2 chiều tương đương với event bindings cộng property bindings và bạn có cú pháp [(ngModel)].

Bây giờ, chúng ta sẽ tạo liên kết 2 chiều cho mỗi thuộc tính của nhân vật (trừ id):

<!-- new syntax for ng-if -->
<section *ngIf="person">
  <section>
    <h2>You selected: {{person.name}}</h2>
    <h3>Description</h3>
    <p>
      {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>
  <section>
    <form>
      <div>
        <label for="name">Name: </label>
        <input type="text" name="name" [(ngModel)]="person.name">
      </div>
      <div>
        <label for="weight">Weight: </label>
        <input type="number" name="weight" [(ngModel)]="person.weight">
      </div>
      <div>
        <label for="height">Height: </label>
        <input type="number" name="height" [(ngModel)]="person.height">
      </div>
    </form>
  </section>

  <button (click)="gotoPeoplesList()">Back to peoples list</button>
</section>

Nếu bạn gõ lệnh npm start bạn có thể thấy bất kỳ khi nào bạn thay đổi giá trị trong thẻ input, các thay đổi sẽ được hiển thị trong phần description.

Tổng kết về Angular 2 data bindings

Angular 2 hỗ trợ các kiểu liên kết dữ liệu (data bindings):

  • interpolation: liên kết dữ liệu một chiều (one-way data binding) từ component tới template. Giúp bạn hiển thị thông tin từ component tới template. VD: {{person.name}}.
  • property bindings: liên kết dữ liệu một chiều (one-way data binding) từ component tới template. Giúp bạn liên kết dữ liệu từ component tới template. VD [src]="person.imageUrl".
  • event bindings: liên kết dữ liệu một chiều (one-way data binding) từ template tới component. Giúp bạn liên kết các sự kiện ở template tới component. VD: (click)="selectPerson(person)".
  • [(ngModel)]: liên kết dữ liệu 2 chiều (two-way data binding) từ component tới template và ngược lại. VD: [(ngModel)]="peson.name".

Thêm validation tới Form

Bây giờ hãy thêm một vài validation để đảm bảo rằng dữ liệu chúng nhập là hợp lệ trước khi lưu chúng.

Chúng ta sẽ thêm vào trường name thuộc tính required, sau đó sẽ hiển thị thông báo lỗi bất kỳ khi nào trường name trống và chúng ta sẽ cho phép hoặc vô hiệu hóa nút submit form dựa trên tính hợp lệ của các thẻ input trong form.

Chúng ta sẽ theo dõi sự thay đổi và tính hợp lệ của thẻ input trong Angular 2 thông qua directive ngModel. Bằng các sử dụng directive này với một thẻ input chúng ta có thể có được các thông tin về việc người sử dụng có hoặc không làm  một thứ gì đó, đã thay đổi hoặc chưa thay đổi giá trị và thậm chí nếu nó không hợp lệ.

Hãy thêm required tới thẻ input name:

<label for="name">Name: </label>
<input type="text" name="name" required [(ngModel)]="person.name">

Angular 2 sử dụng thuộc tính name để xác định một thẻ input cụ thể và theo dõi sự thay đổi và tính hợp lệ của nó.

Cách dễ dàng nhất để xem Angular 2 theo dõi các thay đổi của thẻ input là xem cách nó thêm hoặc xóa các class vào thẻ input dựa trên trạng thái của thẻ input.

Hãy thêm đoạn code sau để hiển thị thuộc tính className của thẻ input và mở trình duyệt, bạn sẽ thấy các class khác nhau được thêm vào thẻ input thay đổi theo tương tác của bạn:

<label for="name">Name: </label>
<input type="text" name="name" required [(ngModel)]="person.name" #name>
<p>
 input "name" class is: {{ name.className }}
</p>

Nếu bạn chưa làm gì, các class sẽ là: ng-untouched, ng-pristine và ng-valid

Click vào bên trong sau đó là bên ngoài thẻ input nó sẽ được đánh dấu là visited và có class là ng-touched

Nhập một vài thứ nó sẽ được đánh dấu là dirty và có class ng-dirty

Xóa toàn bộ nội dung nó sẽ được đánh dấu là invalid và có class là ng-invalid.

Chúng ta có thể tận dụng tính năng này để thêm một vài css tới các thẻ input khi chúng hợp lệ hoặc không hợp lệ. Bạn có thể cập nhập styles.css như sau:

.ng-valid[required] {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid {
  border-left: 5px solid #a94442; /* red */
}

File css này đã được liên kết trong index.html. Nó là styles áp dụng cho toàn bộ ứng dụng của bạn.

Trở lại PersonDetailsComponent template, bây giờ chúng ta muốn hiển thị một thông báo lỗi bất kỳ khi nào người dùng không nhập tên. Chúng ta có thể làm điều đó bằng cách tạo một biến template cục bộ (local template variable) và thiết lập giá trị của nó là ngModel như thế này:

<label for="name">Name: </label>
<input type="text" name="name" required [(ngModel)]="person.name" #name="ngModel">

Bây giờ, name giữ các giá trị của directive ngModel, chúng ta có thể truy cập các thuộc tính của nó và kiểm tra trạng thái của thẻ input là hợp lệ hay không hợp lệ. Chúng ta có thể sử dụng thông tin đó để hiển thị hoặc ẩn thông báo lỗi:

<label for="name">Name: </label>
<input type="text" name="name" required [(ngModel)]="person.name" #name="ngModel">
<div [hidden]="name.valid || name.pristine" class="error">
    Name is required my good sir/lady!
</div>

Thêm style sau vào file styles.css:

.error {
  padding: 12px;
  background-color: rgba(255, 0, 0, 0.2);
  color: red;
}

Chú ý cách sử dụng property binding, chúng ta có thể liên kết mọi biểu thức tới thuộc tính DOM [hidden]. Trong trường hợp này chúng ta chỉ ẩn thông điệp khi ngModel nói cho chúng ta là thẻ input là valid (hợp lệ) hoặc pristine (trạng thái khi người sử dụng chưa chỉnh sửa input).

Bước tiếp theo chúng ta sẽ thực sự lưu các thay đổi. Hãy thêm một nút submit vào cuối form:

<section>
  <form>
    <!--- form inputs --->
    <button type="submit">Save</button>
  </form>
</section>

Và chúng ta cần vô hiệu hóa khi form không hợp lệ.

Để làm điều đó chúng ta sẽ tạo một biến template cục bộ (local template variable) khác #personForm để truy cập tới form thực sự thông qua directive ngForm. Sau đó, chúng ta sẽ sử dụng biến này để vô hiệu hóa nút submit khi form không hợp lệ:

<section>
   <form #personForm="ngForm">
     <div>
         <!-- inputs -->
     </div>
     <button type="submit" [disabled]="!personForm.form.valid">Save</button>
   </form>
</section>

Cuối cùng, chúng ta thiết lập sự kiện submit trong form để có thể lưu thông tin chi tiết của nhân vật bất kỳ khi nào chúng ta submit form:

<section>
    <form (ngSubmit)="savePersonDetails()" #personForm="ngForm">
      <div>
          <!-- inputs -->
      </div>
      <button type="submit" [disabled]="!personForm.form.valid">Save</button>
    </form>
</section>

Toàn bộ template trông sẽ như thế này:

<!-- new syntax for ng-if -->
<section *ngIf="person">
  <section>
    <h2>You selected: {{person.name}}</h2>
    <h3>Description</h3>
    <p>
      {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>
  <section>
    <form (ngSubmit)="savePersonDetails()" #personForm="ngForm">
        <div>
            <label for="name">Name: </label>
            <input type="text" name="name" required [(ngModel)]="person.name" #name="ngModel">
            <div [hidden]="name.valid || name.pristine" class="error">
                Name is required my good sir/lady!
            </div>
      </div>
      <div>
        <label for="weight">Weight: </label>
        <input type="number" name="weight" [(ngModel)]="person.weight">
      </div>
      <div>
        <label for="height">Height: </label>
        <input type="number" name="height" [(ngModel)]="person.height">
      </div>
      <button type="submit" [disabled]="!personForm.form.valid">Save</button>
    </form>
  </section>

  <button (click)="gotoPeoplesList()">Back to peoples list</button>
<section>

Chúng ta chỉ cần cập nhật PersonDetailsComponent để có thể sử lý sự kiện submit:

// imports 

@Component({
  selector: 'person-details',
  templateUrl: 'app/people/person-details.component.html'
})
export class PersonDetailsComponent implements OnInit {
    // codes...

    savePersonDetails(){
        alert(`saved!!! ${JSON.stringify(this.person)}`);
    }
}

Bây giờ, bạn có kiểm tra tất cả những thứ đã làm trong trình duyệt: click vào Luke, thay đổi tên của anh ấy và lưu lại, bạn sẽ thấy các thay đổi của bạn trong một hộp thoại.

Tiếp theo hãy cập nhật component của chúng ta để có thể lưu thông tin đã thay đổi với sự trợ giúp của PeopleService.

Lưu thông tin

Chúng ta sẽ thêm phương thức save trong PeopleService để lưu các thay đổi về nhân vật được chọn.

Bắt đầu bằng cách cập nhật PersonDetailsComponent:

// etc
export class PersonDetailsComponent implements OnInit {
    savePersonDetails(){
      this.peopleService.save(this.person);
    }

Sau đó là cập nhật PeopleService với phương thức save:

import { Injectable } from '@angular/core';
import { Person } from './person';

const PEOPLE : Person[] = [
      {id: 1, name: 'Luke Skywalker', height: 177, weight: 70, profession: ''},
      {id: 2, name: 'Darth Vader', height: 200, weight: 100, profession: ''},
      {id: 3, name: 'Han Solo', height: 185, weight: 85, profession: ''},
    ];

@Injectable()
export class PeopleService{

  getAll() : Person[] {
    return PEOPLE.map(p => this.clone(p));
  }
  get(id: number) : Person {
    return this.clone(PEOPLE.find(p => p.id === id));
  }
  save(person: Person){
    let originalPerson = PEOPLE.find(p => p.id === person.id);
    if (originalPerson) Object.assign(originalPerson, person);
    // saved muahahaha
  }

  private clone(object: any){
    // hack
    return JSON.parse(JSON.stringify(object));
  }
}

Bạn có thể chú ý đến phương thức clone. Mục đích của nó là để tránh chia sẻ cùng một object giữa các component khác nhau.

Đâu là sự khác biệt giữa ngModel và ngForm?

Nếu bạn giống tôi, bạn thường nhầm lẫn một chút giữa ngModel và ngForm. Vì thế hãy tóm tắt về chúng:

  • ngModel giúp bạn theo dõi trạng thái và tính hợp lệ của các thẻ input.
  • ngModel thêm các class tới các thẻ input dựa vào trạng thái của chúng.
  • Bất kỳ khi nào bạn thêm directive ngModel tới một thẻ input Angular 2, bạn cần đăng ký nó sử dụng tên bạn cung cấp (nhớ name="name") với directive ngForm Angular 2 tự động đính kèm nó với phần tử form.
  • Sử dụng #name="ngModel" trong một phần tử input tạo ra một biến template cục bộ (local template variable) và gán directive ngModel tới nó. Bạn có thể sử dụng biến này để truy cập tới các thuộc tính của directive ngModel như valid, pristine, touched, ...
  • Angular 2 đính kèm một directive NgForm tới mọi phần tử form
  • Directive ngForm chứa một tập hợp các điều khiển tạo ra bằng cách sử dụng directive ngModel.
  • Directive ngForm cung cấp thuộc tính form.valid giúp bạn biết tất cả điều khiển trong một form là hợp lệ hay không.

Thêm một thẻ select trong Angular 2

Hãy thử thêm một thẻ select với Angular 2 để lựa chọn profession của nhân vật được chọn.

Chúng ta sẽ bắt đầu với việc thêm profession tới interface Person:

export interface Person {
  id: number;
  name: string;
  height: number;
  weight: number;
  // it is optional because I know it
  // doesn't exist in the API that we will
  // consume in the next exercise :)
  profession?: string;
}

Sau đó cập nhật PersonDetailsComponent để bao gồm tất cả các professions có sẵn:

export class PersonDetailsComponent implements OnInit {
    professions: string[] = ['jedi', 'bounty hunter', 'princess', 'sith lord'];
    // other code
}

Và cuối cùng cập nhật PersonDetailsComponent template bao gồm phần tử select:

  <!-- description header ... -->
  <section>
    <form (ngSubmit)="savePersonDetails()" #personForm="ngForm">
      <div>
      <! -- old inputs ... -->
      <!-- ...and the new select element -->
      <div>
        <label for="profession">Profession:</label>
        <select name="profession" [(ngModel)]="person.profession">
          <option *ngFor="let profession of professions" [value]="profession">{{profession}}</option>
        </select>
      </div>
      <button type="submit" [disabled]="!personForm.form.valid">Save</button>
    </form>
  </section>

Bạn muốn đọc nhiều hơn về Forms?

Kết luận

Bạn đã học về forms và validation. 

Bài tiếp theo sẽ là: Sử dụng dữ liệu thực với Angular 2 Http.