Angular: Form in Details (Part 1)

Giới thiệu

Form có lẽ là một trong những thành phần quan trọng bậc nhất trong bất kỳ một ứng dụng web nào. Cho dù chúng ta có thể nhận được những dữ liệu theo những cách khác nhau từ phía người dùng như click, hover,... tuy nhiên, form vẫn là một phuơng pháp mạnh mẽ và trực quan nhất để chúng ta có thể thu thập thông tin từ phía người dùng

Nhìn từ phía giao diện, form có vẻ rất đơn giản và rõ ràng: chúng ta có 1 thẻ <input>, người dùng điền các thông tin được yêu cầu, bấm submit, nhưng vì sao xử lý form lại thường xuyên khá khó khăn và đòi hỏi chúng ta bỏ khá nhiều công sức để xử lý chúng một cách hiệu quả?

Nếu xem xét kỹ hơn vấn đề, form có thể rất phức tạp, sau đây là một số lý do: - các trường input của form thay đổi dữ liệu trên cả server và client - khi thay đổi các giá trị được nhập vào form, những giá trị thay đổi đó cần được thể hiện ở các nơi khác trên trang - người dùng được toàn quyền nhập các giá trị vào form, do vậy, chúng ta cần một số phuơng pháp để validates các giá trị đó - giao diện phải hiển thị rõ ràng các lỗi và giải thích tường tận cho người dùng - một số trường của form phụ thuộc vào giá trị của các trường khác, do đó, ta cần xử lý logic các giá trị này - tất nhiên, test là một buớc không thể thiếu, tuy nhiên, chúng ta cần test các form mà không phụ thuộc vào các DOM selector

Một điều đáng mừng cho các bạn sử dụng Angular, đó là ở phiên bản hiện tại (Angular 4), chúng ta được cung cấp một cách khá đầy đủ các công cụ để có thể giải quyết các vấn đề đã nêu. Vì vậy, trong bài viết này, chúng ta sẽ từng bước xây dựng form từ form cơ bản nhất, từ đó xây dựng các forms khác có sự phức tạp hơn về logic ở các bài tiếp theo.

FormControls và FormGroups

Đây là 2 đối tượng cơ bản nhất để chúng ta làm việc với form trong Angular

FormControl

FormControl đại diện cho 1 trường input, đây là đơn vị nhỏ nhất của 1 form trong Angular FormControl chứa các giá trị của trường hiện tại, trạng thái (state) của trường, ví dụ như valid, dirty, hoặc có lỗi hay không. Để xây dựng 1 các form, chúng ta tạo nhiều các FormControls này, thêm các metadata và các thành phần logic cho chúng Tuơng tự với rất nhiều các thành phần khác trong Angular, chúng ta có một class (FormControl) mà sẽ được gắn vào DOM bằng 1 attributes (formControl), ví dụ: <input type="text" [formControl]="name"/> dòng trên sẽ tạo mới một đối tượng FormControl trong phạm vi của form chúng ta đang tạo, cụ thể hơn, mình sẽ đề cập ở phần dưới

FormGroup

Hầu hết các form đều có nhiều hơn một trường, do vậy, Angular cung cấp cho chúng ta một cách để quản lý các FormControls là FormGroup. Hãy thử xét một trường hợp cho thấy sự cần thiết của FormGroup: Khi form của chúng ta có nhiều trường, ta cần kiểm tra tính hợp lệ của các trường đó. Nếu không có FormGroup, chúng ta buộc phải lặp qua các phần tử mảng FormControls và kiểm tra tính hợp lệ của từng FormControl, khá phức tạp khi mỗi FormControl có thể lại có một cách kiểm tra tính hợp lệ khác nhau.

Chính vì thế, để giải quyết vấn đề này, FormGroup được tạo ra để cung cấp một interface giúp chúng ta có thể thao tác dễ dàng với một tập hợp các FormControls

Cách tạo một FormGroup, rất đơn giản:

let personalInfo = new FormGroup({
	firstName: new FormControl('Phuong');
    lastName: new FormControl('Tran');
    zip: new FormControl('100000');
})

FormGroup và FormControl có chung một lớp cha (AbstractControl), điều đó có nghĩa, chúng ta cũng có thể kiểm tra statusvalue của personnalInfo như cách đã làm với FormControl:

personalInfo.value // { firstName: "Phuong", lastName: "Tran", zip: "100000"}
personalInfo.errors // StringMap<string, any> of errors
personalInfo.dirty //false
personalInfo.valid // true

Chú ý rằng, khi chúng ta lấy value của FormGroup, chúng ta nhận được một đối tượng với 1 cặp key-value ứng với từng FormControl, điều này rất hữu ích do chúng ta không cần lặp qua các FormControls để lấy từng giá trị của chúng

Xây dựng form

Chúng ta sẽ cùng nhau xây dựng một forms để có thể có cái nhìn rõ nét hơn về form, mình sẽ giải thích chi tiết ở từng bước trong khi chúng ta làm Form đầu tiên của chúng ta sẽ rất đơn giản, chỉ gồm một label, một trường input và một nút submit mà thôi.

1. Import các thư viện cần thiết

Để có thể sử dụng thư viện forms trong Angular, chúng ta cần import chúng vào trong NgModule

MÌnh xin nói thêm, trong Angular, có 2 kiểu forms mà chúng ta có thể dùng: FormsModuleReactiveFormsModule, trong bài này, mình sẽ ưu tiên giới thiệu FormsModule trước do nó dễ tiếp cận hơn và dễ hiểu hơn 1 chút 😄

(Nói thêm một chút về FormsModule, module này cung cấp cho chúng ta một số template driven directives, hiểu nôm na là chúng ta xây dựng form dựa vào các phần tử trên DOM)

Để import thư viện, chúng ta vào thư file app.module.ts (mình mặc định các bạn dùng công cụ angular/cli nhé)

  1. import các module cần thiết:

    import {
        FormsModule,
        ReactiveFormsModule
    } from '@angular/forms';
    
  2. imports vào trong @NgModule

    @NgModule({
        ...
        imports: [
            ...
            FormsModule,
            ReactiveFormsModule
            ...
        ]
        ...
    })
    

Vậy là xong, giờ chúng ta có thể bắt đầu rồi xD

2. @Component Decorator (app.component.ts)

Hiện tại, bạn vẫn sử dụng các giá trị mặc định cho file này (do chúng ta chưa cần quan tâm đến logic của form)

3. Template (app.component.html)

Dưới đây là template của form, như mình đã nói, rất đơn giản, chỉ input. label và submit thôi, ơ mà khoan, sao nhiều thứ thế?

<div class="container">
  <h2 class="text-center">What's your name?</h2>
  <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">

    <div class="form-group">
      <label for="nameInput">Name</label>
      <input type="text"
             class="form-control"
             id="nameInput" 
             placeholder="Enter your name here" 
             name="username" 
             ngModel>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</div>

Ok, mình không điêu đâu, cơ mà công nhận là nó nhiều thứ thật, giải thích từng phần nhé:

  1. formNgForm

Do ở bước 1, chúng ta đã import FormsModule, điều này sẽ khiến NgForm có sẵn để sử dụng trên view. Khi chúng ta làm cho 1 directive (ở đây là NgForm) có sẵn trên view, chúng sẽ gắn với bất kỳ element nào trên DOM mà có selector khớp với selector của directive đó

NgForm cũng làm việc như vậy, nó chứa form trong selector của mình, điều đó có nghĩa rằng, khi chúng ta include FormsModule, NgForm sẽ được gắn vào bất kỳ các thẻ <form> nào trên view

Có 2 tính năng quan trọng mà NgForm cung cấp cho chúng ta: - FormGroup có tên ngForm - (ngSubmit) output

Cùng xem xét kỹ hơn template chúng ta đang có:

 <form #f="ngForm" 
             (ngSubmit)="onSubmit(f.value)">

Ở đây, cú pháp #f="ngForm" có thể hiểu: chúng ta tạo 1 alias cho ngForm trên view hiện tại, gán vào biến #f, ngForm có được là từ NgForm directive ngForm ở đây là một đối tượng kiểu FormGroup, do vậy, chúng ta có thể dùng biến f để lấy giá trị của FormGroup này và đưa vào như 1 tham số cho (ngSubmit)

Chắc hẳn nhiều bạn sẽ thắc mắc: ở trên mình có nói là việc import NgForm sẽ tự động gắn với bất kỳ thẻ <form> nào trên view, vậy tại sao ở dưới lại phải thêm ngForm làm gì? Okay, nếu chúng ta sử dụng ngForm như một key của 1 thuộc tính, chúng ta đang khai báo với Angular rằng chúng ta sẽ sử dụng NgForm trong thuộc tính đó, còn trong trường hợp hiện tại, chúng ta sử dụng ngForm như một giá trị thuộc tính khi chúng ta gán nó vào 1 biến (#f). Vì vậy, có thể nỏi rằng, giá trị của biểu thức ngForm sẽ được gán vào giá trị biến f ngForm như đã nói, có sẵn trên element này rồi, chúng ta có thể hiểu thứ đang xảy ra ở đây là: export giá trị của FormGroup ra và ta có thể tham chiếu tới nó ở các vùng khác trên view

Tiếp theo, (ngSubmit)="onSubmit(f.value)" có lẽ không cần giải thích quá nhiều: khi chúng ta submit form, gọi hàm onSubmit trong component, với tham số là f.value

  1. inputngModel
  <div class="form-group">
      <label for="nameInput">Name</label>
      <input type="text"
             class="form-control"
             id="nameInput" 
             placeholder="Enter your name here" 
             name="username" 
             ngModel>
    </div>

Cần chú ý ở input: - label forid của input cần khớp với nhau (theo W3C)

NgModel directive có selector là ngModel, điều đó có nghĩa rằng, chúng ta có thể gắn NgModel với input một cách dễ dàng qua cú pháp: ngModel="something", tuy nhiên, ở đây chúng ta lại không đặt một giá trị nào cho attribute ngModel cả, trường hợp này, sẽ phát sinh một số thứ như sau: - one-way data binding - tạo FormControl trên form này, với tên là username (do thuộc tính name có giá trị username - NgModel sẽ tạo 1 FormControl và tự động được thêm vào FormGroup cha (ở đây chính là <form></form>), sau đó gắn DOM element vào FormControl này

***NModel và nModel - NgModel: tên class, tham chiếu đến kiểu của 1 đối tượng nào đó - ngModel: là selector của directives, chỉ có thể sử dụng trên view


4. Implement tính năng Submit (app.component.ts)

onSubmit(form: any): void {
    console.log('you submitted value:', form);
}

Không có gì khỏ hiểu cả 😄, mình sẽ để đây và không nói gì 😄

Sau khi viết xong, các bạn có thể kiểm tra thành quả 😄 :

Kết luận

Phew! một form đơn giản mà mất nhiều công sức nhỉ?

Mình đồng ý với ý kiến đó, tuy nhiên, nó cũng giống như các huấn luyện viên khi chỉ dẫn cho các cầu thủ vậy: mô tả kỹ thuật rất chi tiết, mất rất nhiều thời gian, nhưng khi cầu thủ thực hiện kỹ thuật lại chỉ mất có vài giây 😄

Vì thế, bài viết này mình công nhận khá dài, nhưng khi các bạn đã nắm được những gì thực sự xảy ra dưới những dòng ngắn gọn đó, bạn sẽ tiến nhanh hơn rất nhiều ở những phần sau, và thêm phần cảm ơn các kỹ sư của Angular đã tạo ra 1 framwork tuyệt vời đến thế nào

Chúc các bạn thành công, hẹn gặp lại các bạn ở những phần sau 😃


All Rights Reserved