SSV
+1

Xây dựng form trong Angular 2 - Phần 1 : Dùng Template

Mở đầu

Xin chào mọi người,

Thử tưởng tượng bạn biết chút chút về Javscript/CSS, không am tường lắm về những khái niệm như là SPA (Single Page App), Shadow DOM, Module, Component, Typescript,... Công việc trước đó thì chủ yếu sử dụng JQuery hay VanillaJS (Nếu bạn không biết VanillaJS là gì : http://vanilla-js.com/) để thao tác với HTML, tạo hiệu ứng, sử dụng lib, đổi chỗ này một chút, đổi chỗ kia một tẹo, thì khi tiếp cận với AngularJS, mà Angular 2 luôn nhé, thì tâm trạng hẳn sẽ thấy rất là ngợp + chán, khi hơi tí đọc tutorial lại phải bấm vào link tham khảo để hiểu xem cái đang nói đến là gì, và từ cái tham khảo đó lại dẫn đến n cái định nghĩa khác nữa. Phải nói là rất tốn thời gian.

Mình cũng gặp hoàn cảnh tương tự, khi đọc tut Angular mà cứ kéo kéo từ đầu đến cuối, xong rồi ngáo ngắn ngáp dài rồi bỏ đó đi xem Facebook =)). Tuy nhiên vì công việc bắt buộc, cứ cắm mặt vào làm, copy paste code mẫu thì cũng ra thôi, app vẫn chạy mà mình vẫn chả hiểu hết sao nó chạy được =)) Sau đó có thời gian thì mới tìm hiểu dần dần, và rồi sẽ đạt được những phút giây "Ah-ha!" khi hiểu ra mình đang làm cái của nợ gì.

Và để lưu trữ lại thành quả của việc tìm hiểu đó, vì nếu cứ để lại trong đầu thì tốn nơ-ron + bào mòn theo thời gian, mình sẽ viết dần những kiến thức cần nhớ ra thành những bài viết như này, để tiện theo dõi, hệ thống lại kiến thức và làm màu đem đi doạ trẻ con 😃)

Bắt đầu với bài viết đầu tiên này, mình sẽ viết về việc tạo ra một form trong Angular 2 trở lên. Từ giờ mình sẽ gọi tắt là Angular cho gọn, vì google cũng thế mà 😃) (điều đầu tiên chúng ta có thể cần phải nhớ là Angular 1 sẽ có tên là AngularJS với trang chủ là https://angularjs.org/ , còn Angular 2 trở lên (h đến bản 4 rồi) sẽ chỉ có tên là Angular với trang chủ là https://angular.io/)

Trong Angular, chúng ta có thể tạo ra form bằng 2 cách, với tên gọi lần lượt là Model DrivenTemplate Driven. Trong bài viết này, mình sẽ giới thiệu về Template Driven.

Template Driven có nghĩa là việc định nghĩa, thao tác với các thành phần trên form sẽ được định nghĩa trên file Template (file HTML) của Angular Component, với Component là đơn vị cơ bản dùng để xây dựng lên một trang web sử dụng Angular (một trang web được tạo thành từ nhiều component, với mỗi component sẽ bao gồm HTML/CSS, JS riêng biệt, giống như nhà được xây từ nhiều viên gạch).

Tạo dự án và form tĩnh

Để dễ hình dung, chúng ta sẽ bắt tay vào làm một form để lưu thông tin người dùng bao gồm tên và địa chỉ (địa chỉ sẽ gồm tên đường và mã vùng), với giao diện trông như thế này :

Chúng ta sẽ sử dụng Angular CLI (hiện đang là 1.4.x) để tạo ra một project Angular với câu lệnh sau:

ng new form-demo
cd form-demo && yarn && ng server

Mở localhost:4200 sẽ thấy web đã chạy, code thôi.

Trước hết, chúng ta sẽ thêm thư viện Bootstrap vào index.html để trang trí form cho đẹp, đồng thời sửa lại app.component.html thêm vài wrapper mặc định của Bootstrap:

#src/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>FormDemo</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css"> <!--add this -->
</head>
<body>
  <app-root></app-root>
</body>
</html>


#src/app/app.component.html
<div class="container">
  <div class="row">
    <h1>Form Demo</h1>
    <hr>
    <div class="col-xs-12">Form will go there</div>
  </div>
</div>

Tiếp đó chúng ta sẽ tạo ra một form component (ko sử dụng app-component đã có vì đó là component root cho cả app, sẽ chỉ nên chứa các component khác) nằm trong thư mục form bằng lệnh :

ng generate component user-form

(thường chỉ cần gõ cách viết tắt là ng g c user-form)

Sau đó chỉnh sửa user-form để tạo ra các thành phần của form và đưa vào app-component để hiển thị :


#src/app.component.html
<div class="container">
  <div class="row">
    <h1>Form Demo</h1>
    <hr>
    <app-user-form></app-user-form>
  </div>
</div>

#src/user-form/user-form.component.html	

<div class="col-xs-12">
  <form action="#">
    <div class="form-group">
      <label for="username">Name</label>
      <input type="text" class="form-control" id="username">
    </div>
    <div class="form-group">
      <label for="street">Street</label>
      <input type="text" class="form-control" id="street">
    </div>
    <div class="form-group">
      <label for="postcode">Post code</label>
      <input type="text" class="form-control" id="postcode">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</div>

Done, trang của chúng ta giờ sẽ trông như thế này.

Giờ để có thể làm được các kiểm tra, hiệu ứng phức tạp với Form, chúng ta cần theo dõi trạng thái và sự thay đổi của form dựa trên thao tác của người dùng. Nếu sử dụng jQuery hoặc Javascript thông thường, chúng ta sẽ phải khai báo 1 loạt event onclick, onchange để check giá trị của từng input và lưu vào biến để sử dụng.

Áp dụng ngModel vào input

Nay với Angular, chúng ta có thể sử dụng một phương thức kinh điển từ AngularJS được gọi là 2-way databinding, thông qua một directive có tên là ngModel. Tuy nhiên ngModel directive nằm trong một module có tên là FormsModuel, là cái mà không được thêm mặc định trong Angular bản mới nhất, nên chúng ta cần import bằng tay trong app.module như sau :

# src/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { UserFormComponent } from './user-form/user-form.component';

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


Sau đó chúng ta có thể sử dụng ngModel cho từng input như sau trong file user-form.component.html :

      <input type="text" class="form-control" id="name" [(ngModel)]="username" name="username">
      <input type="text" class="form-control" id="street" [(ngModel)]="street" name="street">
      <input type="text" class="form-control" id="postcode" [(ngModel)]="postcode" name="postcode">

Để ý là chúng ta sẽ thêm 2 thuộc tính có tên là [(ngModel)]name. Đây là cách sử dụng của directive ngModel , với dấu [ biểu thị cho việc Input, và dấu ( biểu thị cho việc Ouput, tức là directive ngModel khi được dùng cho element input thì có thể nhận dữ liệu - vì có khai báo [], và có thể xuất dữ liệu - vì có khai báo (), có nghĩa là bằng việc set giá trị ở thuộc tính [(ngModel)], ở đây là username, chúng ta có thể thông qua biến username ở trong file user-form.component.ts để set/get giá trị cho input username của form. Còn lý do tại sao cần name vì đó là yêu cầu khi sử dụng ngModel trong form, vì bản chất là ngModel sẽ tạo ra một FormControl với tên là giá trị nằm trong thuộc tính name của input (FormControl là gì sẽ được giải thích ở phần 2 - tạo form bằng Model Driven ) (ref: https://angular.io/guide/forms#ngForm)

Giải thích tương tự với các thành phần khác, lúc này chúng ta có thể thay đổi một chút để theo dõi xem có thật là dữ liệu đã được save vào các biến hay không.

<!-- user-form.component.html -->
<!-- Thêm đoạn sau vào sau form tag -->
  <h3>Check Variable Data</h3>
  <hr>
  <p>Name is : {{ username }} </p>
  <p>Street is : {{ street }}</p>
  <p>Postcode is : {{ postcode }}</p>

Và đây là kết quả, giá trị chúng ta gõ vào trong form đã được hiển thị ở dưới.

Đấy là phần xuất dữ liệu (kí hiệu () ở ngModel), còn phần nhập thì sao nhỉ? Để ý rằng mặc dù chúng ta không hề khai báo các biến username, street, postcode trong file user-form.component.ts, nhưng thực tế là ngModel đã dùng các biến đó để lưu trữ dữ liệu từ các input, và chúng ta có thể khai báo và thay đổi chúng để set các giá trị đầu vào nếu muốn

#thay đổi trong file user-form.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
  styleUrls: ['./user-form.component.css']
})
export class UserFormComponent implements OnInit {
  username: string;
  street: string;
  postcode: string;
  constructor() { }

  ngOnInit() {
    this.username = "Default Username";
    this.street = "Universe";
    this.postcode = "XXX";
  }

}

Save lại và khi mở trang web, chúng ta sẽ thấy các giá trị được nhập sẵn như sau.

Yeah vậy là chúng ta đã hiểu về cách sử dụng ngModel, tiếp theo chúng ta sẽ đến với xử lý validation và kiểm tra dữ liệu khi submit.

Tuy nhiên, mình sẽ refactor lại code một chút để follow-up theo best practice khi tạo form của Angular, đó là các thành phần input nên được gộp vào một model để quản lý và mở rộng cho dễ (mình cũng thấy đúng thật, khoảng 5-6 properties mà ném hết trong component.ts thì cũng không ổn lắm, code dài vcl).

Tạo file user.model.ts trong thư mục user-form:

export class User {
  constructor(public username: string, public street: string, public postcode: string) {}
}

Chỉnh sửa user-form.component.ts:

import { Component, OnInit } from '@angular/core';
import { User } from './user.model';  #import thêm User
@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
  styleUrls: ['./user-form.component.css']
})
export class UserFormComponent implements OnInit {
  user: User; #dùng 1 biến user thay cho 3 biến
  constructor() { }

  ngOnInit() {
    this.user = new User('Default Username', 'Universe', 'XXXX'); #khởi tạo user
  }
}

Chỉnh sửa user-form.component.html:

<div class="col-xs-12">
  <form action="#">
    <div class="form-group">
      <label for="username">Name</label>
      <input type="text" class="form-control" id="username" [(ngModel)]="user.username" name="username"> <!-- chỉ đổi value của ngModel -->
    </div>
    <div class="form-group">
      <label for="street">Street</label>
      <input type="text" class="form-control" id="street" [(ngModel)]="user.street" name="street"> <!-- chỉ đổi value của ngModel -->
    </div>
    <div class="form-group">
      <label for="postcode">Post code</label>
      <input type="text" class="form-control" id="postcode" [(ngModel)]="user.postcode" name="postcode"> <!-- chỉ đổi value của ngModel -->
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
  <h3>Check Variable Data</h3>
  <hr>
  <p>Inputed Data</p>
  <p> {{ user | json }} </p>
</div>

Và kết quả sẽ là như này

Kiểm tra form submit data

Tiếp theo, chúng ta sẽ để kiểm tra xem dữ liệu mà form sẽ sử dụng để tạo ra request là như thế nào.

Thay đổi tag trong file user-form.component.html như sau :

<div class="col-xs-12">
  <form #userForm="ngForm">
    <div class="form-group">
      <label for="username">Name</label>
      <input type="text" class="form-control" id="username" [(ngModel)]="user.username" name="name_input">
    </div>
    <div class="form-group">
      <label for="street">Street</label>
      <input type="text" class="form-control" id="street" [(ngModel)]="user.street" name="street_input">
    </div>
    <div class="form-group">
      <label for="postcode">Post code</label>
      <input type="text" class="form-control" id="postcode" [(ngModel)]="user.postcode" name="post_code">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
  <hr>
  <h4>Inputed Data</h4>
  <p> {{ user | json }} </p>
  <hr>
  <h4>Form Data</h4>
  <p> {{ userForm.value | json }} </p>
</div>

Và kết quả chúng ta thu được là.

Ở đây mình đã có những thay đổi như sau:

  • Thêm thuộc tính #userForm="ngForm" vào thẻ form
  • Thay đổi giá trị thuộc tính name của các input khác đi với thuộc tính của model User
  • Thêm hiển thị {{ userForm.value | json }}

Ý nghĩa của thuộc tính #userForm là để reference (tham chiếu) đến thẻ form, hay còn gọi là đặt tên cho thẻ. Bằng việc đặt tên này, mình có thể gọi đến form ở bất kỳ đâu trong template dưới cái tên userForm, và mình đã lợi dụng điều đó để in ra giá trị mà form sẽ submit bằng cách sử dụng {{ userForm.value | json }}. Còn giá trị của thuộc tính #userForm thì lúc nào cũng cố định là ngForm, vì Angular đã tạo sẵn ngForm gắn vào thẻ form cho chúng ta, còn việc mình vừa là lấy cái ngForm đó ra, gắn vào một biến có tên tự đặt (userForm trong trường hợp này) và sử dụng.

Kế đến, để ý thấy là Form Data giờ có giá trị của từng key tương ứng với giá trị của từng input, nhưng tên của key giống với giá trị mà đã đặt trong thuộc tính name của từng input, không phải trong ngModel. Đây chính là ý nghĩa của thuộc tính name, dùng để xác định tên của param mà bạn sẽ truyền lên Request là gì. Thông thường thì giá trị của name sẽ trùng với tên của thuộc tính trong model nên chúng ta ít để ý, tuy nhiên có thể thay đổi như trên để phù hợp với hoàn cảnh, ví dụ như là Typescript khuyến cáo dùng camelCase, tuy nhiên API của Rails lại yêu cầu param dạng underscore_case chẳng hạn, thì chúng ta chỉ việc set giá trị trong name là đủ cho API, còn trong ngModel thì cứ camelCase thoải mái.

Form Validation

Đến phần cuối cùng, chúng ta sẽ thêm vài validation cho form theo yêu cầu như sau :

  • Name phải trên 5 kí tự, bắt buộc nhập
  • Street bắt buộc nhập
  • Hiển thị lỗi khi submit form hoặc khi nhập chưa thoả mãn yêu cầu.

Để thực hiện, chúng ta chỉ cần thay đổi template user-form.component.html (đương nhiên vì đang là template driven mà) như sau :

<div class="col-xs-12">
  <form #userForm="ngForm" (ngSubmit)="onSubmit()">
    <div class="form-group">
      <label for="username">Name</label>
      <input type="text" class="form-control" id="username" [(ngModel)]="user.username" name="name_input" required minlength="5" #inputUserName="ngModel">
      <div class="alert alert-danger" *ngIf="!inputUserName.valid && (!inputUserName.pristine || userForm.submitted)">
        <small>Name is required and must have more than 5 characters</small>
      </div>      
    </div>
    <div class="form-group">
      <label for="street">Street</label>
      <input type="text" class="form-control" id="street" [(ngModel)]="user.street" name="street_input" required #inputStreet="ngModel">
      <div class="alert alert-danger" *ngIf="!inputStreet.valid && (!inputStreet.pristine || userForm.submitted)">
        <small>Street is required</small>
      </div>
    </div>
    <div class="form-group">
      <label for="postcode">Post code</label>
      <input type="text" class="form-control" id="postcode" [(ngModel)]="user.postcode" name="post_code">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
  <hr>
  <h4>Inputed Data</h4>
  <p> {{ user | json }} </p>
  <hr>
  <h4>Form Data</h4>
  <p> {{ userForm.value | json }} </p>
</div>

Ở đây mình đã thêm thuộc tính #inputUserName="ngModel"#inputStreet="ngModel" lần lượt cho 2 input. Nguyên tắc thì giống như #userForm="ngForm" ở trên, lần này chúng ta sẽ tạo ra 2 tham chiếu cho 2 input vào 2 biến là inputUserNameinputStreet để dùng trong template, và 2 input này dùng với ngModel nên value sẽ là ngModel.

Tiếp theo chúng ta tạo 2 div để hiển thị lỗi sử dụng directive ngIf (lưu ý khi sử dụng ngIf thì phải có dấu * ở đầu, với ý nghĩa đó là một directive có tác dụng thay đổi cấu trúc HTML, tương tự như ngFor khi sử dụng cũng là *ngFor). Trong này chúng ta sẽ check điều kiện hiển thị của div lỗi là khi input có giá trị không hợp lệ và input đó đã được tác động hoặc form đã submit (!inputStreet.valid && (!inputStreet.pristine || userForm.submitted)). Do đó lỗi sẽ không hiển thị lúc đầu load trang mà chỉ hiển thị sau khi người dùng đã nhập dữ liệu sai hoặc lập tức submit form.

Kết quả thu được sẽ là

Tổng kết

Vậy là mình đã trình bày xong việc sử dụng Template Driven để tạo form trong Angular như thế nào, sẽ một số gạch đầu dòng mọi người cần lưu ý :

  • ngModel2-way data binding.
  • Sử dụng thuộc tính name để set tên cho param sẽ truyền lên request.
  • Sử dụng template reference (dấu #) để tham chiếu đến các thành phần trong form.
  • form và control sẽ có các thuộc tính để xác định trạng thái và kiểm tra xem có hợp lệ hay không (valid, dirty, pristine,...).

Trong bài tiếp theo, mình sẽ trình bày nốt về việc tạo Form sử dụng Model Driven và so sánh 2 cách này, rồi chúng ta sẽ đi đến các form phức tạp hơn.

Tham khảo

https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2 https://scotch.io/tutorials/using-angular-2s-template-driven-forms https://scotch.io/tutorials/how-to-deal-with-different-form-controls-in-angular-2 http://billpatrianakos.me/blog/2013/09/29/rails-tricky-error-no-implicit-conversion-from-symbol-to-integer/ TypeError (no implicit conversion of Symbol into Integer):


All Rights Reserved