Angular 2 căn bản - Phần 5: Forms and Validation
Chào mừng các bạn đến với phần thứ 5 trong series Angular 2 căn bản của mình. Tính đến bài viết này thì chúng ta đã có một ứng dụng Angular 2 nho nhỏ với 2 component, trong đó component PeopleListComponent hiển thị một danh sách tên người và PersonDetailsCompent hiển thị thông tin chi tiết của ...
Chào mừng các bạn đến với phần thứ 5 trong series Angular 2 căn bản của mình. Tính đến bài viết này thì chúng ta đã có một ứng dụng Angular 2 nho nhỏ với 2 component, trong đó component PeopleListComponent hiển thị một danh sách tên người và PersonDetailsCompent hiển thị thông tin chi tiết của người đó khi ta click vào. Chúng ta cũng đã sử dụng Angular routing để điều hướng view giữa các component đó. Tuy nhiên, danh sách bây giờ thật là nhàm chán khi mới chỉ có 3 cái tên. Bạn không muốn ứng dụng của mình nhàm chán như vậy khi cứ xem đi xem lại 3 cái tên đó, bạn muốn mình có thể tạo thêm những cái tên khác trong danh sách. Điều đó không hề khó, mình sẽ hướng dẫn các bạn ngay dưới đây. Chúng ta sẽ chuyển PersonDetailsComponent thành một form và thêm một vài validations vào để đảm bảo tính đúng đắn của dữ liệu. Đầu tiên ta cần import FormsModule từ @angular/forms và import nó trong app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { PeopleListComponent } from './people-list/people-list.component'; import { PeopleService } from './people.service'; import { PersonDetailsComponent } from './person-details/person-details.component'; import { appRouterModule } from "./app.routes"; @NgModule({ declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent ], imports: [ BrowserModule, FormsModule, // <=== HERE! HttpModule, appRouterModule ], providers: [PeopleService], bootstrap: [AppComponent] }) export class AppModule { }
Ok, tiếp tục thôi!!!
Sử dụng file template thay cho inline template
Hiện nay chúng ta vẫn đang viết chung template cho PersonDetailsComponent trong person-details.component.ts, giờ ta sẽ viết sang một file html riêng. Ta sẽ sửa thuộc tính template trong decorator @Component thành templateUrl và tạo một file person-details.component.html , trong folder person-details rồi copy đoạn html đã có vào như sau: File person-details.component.html:
<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>
File person-details.component.ts:
@Component({ selector: 'app-person-details', templateUrl: './person-details.component.html', styles: [] })
Basic Form trong Angular 2
Hãy add một fomr HTML cơ bản vào view nào.
<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, như vậy là chúng ta đã có một form nhưng chưa hiển thị gì. Sử dụng những kiến thức đã học ta có thể đưa forrm này vào component person bằng cách sử dụng event và properties binding. Ví dụ:
<div> <label for="name">Name: </label> <input type="text" name="name" [value]="person.name" (change)="person.name = name.value" #name> </div>
Nhìn vào đoạn code trên các bạn có thể thấy rằng ta sử dụng properties binding [value] để gán person.name vào Input và event binding (change) để update tên 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à two-way binding trong Angular 2
ngModel trong Angular 2, cũng tương tự như trong AngularJS, giúp chúng ta thiết lập một two-way databinding cho một properties giữa template và component. Nó giúp chúng ta đồng bộ giá trị giữa template và component.
<div> <label for="name">Name: </label> <input type="text" name="name" [(ngModel)]="person.name"> </div>
Ôn lại kiến thức cũ một chút nào, event bindings liên kết một chiều từ template tới component và được thể hiện bằng dấu ngoặc tròn (). Ngược lại, 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 []. Rất đơn giản để ta có thể đoán rằng:
two-way binding = event binding + properties binding
và bạn có cú pháp [(ngModel)] để thực hiện điều trên Áp dụng vào form của chúng ta thôi (trừ thuộc tính id):
<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>
Và bây giờ hãy ở mở browser lên để tận hưởng thành quả thôi
ng serve --open
Tổng kết về Angular binding
Với [(ngModel)] binding chúng ta đã tìm hiểu xong về toàn bộ phương pháp data binding trong angular 2. Hãy cùng tổng kết lại một chút trước khi chuyển sang validation cho form nhé:
- 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 ersonDetailsComponent 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. 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> <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> </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> </div> <button type="submit" [disabled]="!personForm.form.valid">Save</button> </form> </section>
Toàn bộ template trông sẽ như thế này:
<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: 'Kobayashi Taihei', height: 177, weight: 65}, {id: 2, name: 'Vu Xuan Dung', height: 165, weight: 62}, {id: 3, name: 'Tran Ngoc Thang', height: 173, weight: 68}, ]; @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:
<section> <form (ngSubmit)="savePersonDetails()" #personForm="ngForm"> <div> <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>