[Angular 2 – Meteor]. 3. Quản lý dữ liệu và chuyển trang trong ứng dụng Angular Meteor.

Sau 3 bài viết trước thì chúng ta đã có được những khái niệm cơ bản trong Meteor và cả Angular, và chúng ta đã thao tác đưa dữ liệu từ server đến client. Trong bài viết hôm nay, chúng ta sẽ thực hiện điều ngược lại là thực hiện gửi dữ liệu lên từ client và trên server lưu xuống database, tới đây chúng ta sẽ thấy được kết quả đồng bộ dữ liệu của Meteor khi client gửi dữ liệu lên và Meteor đồng bộ dữ liệu đó với các client khác.

Tiếp theo đó cũng là 1 thành phần khá là quan trọng trong ứng dụng Web, đó chính là Routing và Multiple Views. Thực chất khi chúng ta sử dụng Meteor chính là chúng ta đang xây dựng ứng dụng Single Page, tuy nhiên trong nhiều trường hợp cần thiết, người dùng cần chia ứng dụng của chúng ta thành nhiều page và cho phép chúng ta thao tác qua lại giữa các page. Việc không thể tạo nhiều page trong ứng dụng dẫn đến sự hình thành của Multiple-Views và kéo theo đó là cả Routing. Cho phép người dùng có thể tương tác trên nhiều View và xác định vị trí hiện tại trong ứng dụng dựa vào Routing. Bài viết này chúng ta sẽ tập trung vào 2 vấn đề trên:

  • Quản lý dữ liệu (Data management): Thao tác với Collection trong Meteor để lưu trữ và truy vấn dữ liệu. Chúng ta sẽ phải tạo form trong một Component và sử dụng form và các sự kiện trong form để có thể thêm xóa dữ liệu.
  • Routing và Multiple-View: Chuyển đổi View giữa các trang và thay đổi URL của trình duyệt dựa vào Routing.

Chi tiết hơn chúng ta sẽ bắt đầu với phần 1: Quản lý dữ liệu.

I. Quản lý dữ liệu:

Từ bài trước, chúng ta đã có kết nối dữ liệu và hiển thị ra cho client, tiếp theo chúng ta sẽ tạo các Component cho phép chúng ta có thể thêm xóa dữ liệu. Với Angular 2, khi xây dựng ứng dụng tức là chúng ta đang xây dựng 1 cấu trúc cây của các Component với 1 Component root và các Component con, bài trước chúng ta đã có AppComponent và đó chính là root Component mà chúng ta đã định nghĩa. Bây giờ chúng ta sẽ tạo một Component PartiesFormComponent là Component con của AppComponent, trong Component này chúng ta sẽ sử dũng một khái niệm mới trong Angular 2 đó chính là Angular Form.

Tạo file client/imports/app/parties/parties-form.component.ts:

//Vị trí đặt file: client/imports/app/parties/parties-form.component.ts

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

import { Parties } from '../../../../both/collections/parties.collection';

import template from './parties-form.component.html';

@Component({
selector: 'parties-form',
template
})
export class PartiesFormComponent implements OnInit {
addForm: FormGroup;

constructor(
private formBuilder: FormBuilder
) {}

ngOnInit() {
this.addForm = this.formBuilder.group({
name: ['', Validators.required],
description: [],
location: ['', Validators.required]
});
}

addParty(): void {
if (this.addForm.valid) {
Parties.insert(this.addForm.value);

this.addForm.reset();
}
}
}

Trong file này có khá là nhiều thứ để chúng ta cùng nói với nhau, nên mình sẽ nói theo kiểu liệt kê để các bạn dễ nắm hơn và dễ xem lại hơn nhỉ.. :D..

1. Import:

  • Trong PartiesFormComponent, chúng ta sử dụng Angular Form để có thể gửi dữ liệu lên server nên chúng ta cần phải import nó vào. FormGroup là một class trong angular form, chứa các FormControl là các thuộc tính trong Form và cung cấp những phương thức cho phép thao tác với các thành phần trong form.
  • Tiếp theo là FormBuilder, cũng là 1 class của Angular Form. Dùng để tạo 1 FormGroup bằng phương thức group và tham số truyền vào chính là các thuộc tính cần có trong form. FormBuilder được sử dụng trong Angular như một Service. Ở đây nhóm phát triển Angular2 đã sử dụng mẫu thiết kế Builder (Buidler Design Pattern), chứa các phương thức cho phép tạo các thực thể của class FormGroup, FormControl và FormArray. Chi tiết hơn về 3 thành phần này các bạn xem thêm trong Angular Document nhé. Vì cũng không có gì là quá phức tạp cho lắm..
  • Cuối cùng là Validators là một class cung cấp các phương thức static để khi dùng các phương thức này chúng ta có thể validate các thuộc tính trong form, như required, minLength, maxLength… Và để sử dụng thì chúng ta thêm vào trong phần thuộc tính của phương thức FormBuilder.group. Với các loại validation mà Angular cung cấp các bạn có thể xem chi tiết hơn ở trang Document, link mình đã gắn ở trên nhé :D.

Ở đoạn tạo form mới, các thuộc tính được khai báo có dạng: : ['', Validators.] thì tham số ” chính là giá trị khởi tạo cho thuộc tính tương ứng trong form. Ví dụ: name: ['Hồ Quốc Toản'] thì khi hiển thị form sẽ có trường name được mặc định giá trị là Hồ Quốc Toản.

2. Khai báo Service trong Angular.

  • Là một từ khóa mới của Angular 2, cho phép người dùng sử dụng chúng để phục vụ cho một số chức năng đặc biệt nào đó tùy vào việc cài đặt, ví dụ như là HttpService, cung cấp các phương thức cho phép gửi request lên server để lấy data. Thì việc sử dụng Service trong Angular 2 được xây dựng theo hướng Dependency Injection. Khai báo từ Module, và từ đó khi sử dụng thì chỉ cần Inject vào Component/Directive để dùng. Khai báo ở Module thì mình chưa nói giờ, còn khai báo cho Component thì như dòng 18, khai báo trong danh sách các tham số của phương thức constructor. Dùng thì ở trong component cứ xem tên của tham số chính là một thuộc tính của Component và sử dụng những phương thức mà nó cung cấp.

3. ngOnInit.

  • Một trong những sự kiện của vòng đời (LifeCycle Hooks) của một Componet, được gọi sau khi Angular khởi tạo xong vùng dữ liệu (data-bound) cho các thuộc tính input (mấy cái thuộc tính từ các Component cha bạt qua chẳng hạn) và sau khi phương thức khởi tạo (constructor) hoàn tất. Trong đoạn code trên chúng ta sử dụng ngOnInit để khởi tạo 1 form model. Cú pháp khai báo phương thức ở dòng số 21, nhưng nếu các bạn để ý sẽ thấy còn 1 cái OnInit ở dòng export class PartiesFormComponent implements OnInit, thực ra chúng ta có thể không cần sử dụng implements này, ứng dụng vẫn hoạt động, và phương thức ngOnInit vẫn được gọi và thực thi như bình thường. Việc có thêm chỉ tường minh hơn và có được sự hỗ trợ tối đa từ trình duyệt trong quá trình viết code.

4. Các phương thức và thuộc tính trong form:

  • Dòng 30, 31, 33 sử dụng những phương thức mà Angular Form cung cấp, .valid trả về true false để xem form có validate hay không, .value trả về đối tượng chứa các thuộc tính trong form, và cuối cùng là .reset() phương thức dùng để reset các thuộc tính của form về giá trị ban đầu.

5. Collection Insert:

  • Cuối cùng là insert dữ liệu trong form vào database bằng phương thức insert của MongoObservable Collection.

Tiếp theo chúng ta sẽ tới phần Template của PartiesFormComponent. Tạo file client/imports/app/parties/parties-form.component.html với nội dung:

<form [formGroup]="addForm" (ngSubmit)="addParty()">
<label>Name</label>
<input type="text" formControlName="name">

<label>Description</label>
<input type="text" formControlName="description">

<label>Location</label>
<input type="text" formControlName="location">

<button type="submit">Add</button>
</form>

Trong template chúng ta sẽ gắn các formControl với các thuộc tính tương ứng khi khai báo form, binding directive formGroup với thuộc tính addForm (FormGroup) trong Component, và sử dụng directive ngSubmit của Angular để submit form. Các Directive trên được export từ ReactiveFormsModule, với việc xây dựng form dựa trên module này người ta gọi nó là Model-driven Forms. Và một loại form khác gọi là Template-driven form. Dưới đây mình sẽ nói thêm về 2 loại form này trong Angular 2, hẳn để dùng thì các bạn dùng cái nào cũng có thể giải quyết vấn dề được, tuy nhiên để giải quyết 1 cách linh hoạt, thì nếu biết cả 2 loại thì khi sử dụng các bạn sẽ dễ linh động hơn khi dùng hay còn gọi là… tùy hoàn cảnh :v..

Model-driven Forms:

Model-driven forms, là 1 loại form mà khi dùng chúng ta sẽ thao tác với nó dựa vào 1 thực thể của chính cái form (model) đó trong Component. Như trong trường hợp chúng ta đang sử dụng, trong Component có 1 thuộc tính addForm, là một thực thể FormGroup đại diện cho 1 model. Và chúng ta thao tác với dữ liệu trên form dựa vào thực thể này, tức là ở trong Component, chúng ta có thể sử dụng this.addForm. – một cách trực tiếp từ Component.

Ví dụ:

Component {
addForm: FormGroup;

init() {
this.addForm = new FormGroup({
anAttribute: new FormControl()
});
}

submitForm() {
log(this.addForm.value);
}
}
Template {
<form [formGroup]="addForm">
<input formControlName="anAttribute"/>
</form>
}

Như vậy, Template sử dụng 1 model Form (addForm) trong Component để render View cho người dùng, binding 1 chiều từ Component sang Template bằng cặp ngoặc vuông. Và khi muốn submit form thì chỉ cần submit và có thể lấy dữ liệu từ trong Component thông qua form model (addForm). Với model-driven forms thì như vậy, còn với template-driven thì như thế nào chúng ta sẽ tiếp tục.

Template-driven Forms:

Template-driven Forms, tồn tại với Angular từ thưở khai thiên lập địa. Thay vì có 1 biến form ở trong Component như Model-driven, thì Template-driven lại có biến form ở trong template luôn, vì vậy nên được gọi là.. template-driven 😀 :D.. Đơn giản nhỉ :D, thực chất nó chỉ có vậy, template chứa độc lập 1 cái form trong chính template, và khi submit thì nó mới truyền dữ liệu của form từ Template vào trong Component.

Component {
obj;

init() {
obj.anAttribute = '';
}

submitForm(addForm) {
log(addForm);
}
}
Template {
<form #addForm="ngForm" (ngSubmit)="submitForm(addForm.value)">
<input #anAttribute="ngModel" [(ngModel)]="obj.anAttribute"/>
</form>
}

Đoạn mã giả trên chúng ta đang tạo một form dữ liệu sử dụng ngForm để có thể tạo form, và sau đó khi submit thì dùng ngSubmit và dùng form trong template mà chúng ta vừa tạo để đưa dữ liệu vào trong Component và làm gì tiếp theo thì tùy vào yêu cầu business của từng người. Những điểm đặc biệt nhất của model-driven và template-driven forms đã được thể hiện ở trên, tuy nhiên không chỉ dừng lại ở đó, 2 loại form này còn có những đặc điểm riêng biệt khác dựa vào các tính năng mà directive tương ứng cung cấp. Cụ thể là template-driven forms thì dùng ngForm nên có tính năng là: có sẵn thuộc tính submitted trong để xác định được trạng thái của form là đã submit hay chưa, model-driven không làm được điều này. Thêm vào đó là tính linh động của template-driven form đôi khi mang lại lợi thế nhiều hơn là model-driven form khi sử dụng. Cụ thể nhất là việc valid các thuộc tính trong form, nếu sử dụng model-driven form, để valid 1 form-control thì chúng ta phải truy cập tới nó và kiểm tra: addForm.controls.anAttribute.valid. Còn ở template-driven form thì chúng ta chỉ cần khai báo biến cho chính cái form-control đó, #anAttribute="ngModel" và khi kiểm tra valid chúng ta chỉ cần: anAttribute.valid là xong. (Hãy tưởng tượng model-driven form có các thuộc tính sâu hơn thì chúng ta sẽ phải chấm khá là nhiều, addForm.controls.anAttribute.controls."child attribute".controls...).

Khó mà có thể nói hết về một thứ gì đó trong những bài viết như vậy, thay vào đó thì với các link tham khảo mình đã gắn vào thì các bạn sẽ tìm hiểu trực tiếp hơn để hiểu hơn về Template-driven Forms và Model-driven Forms, và trong quá trình làm việc với nó các bạn sẽ nhận ra được nhiều thứ hơn mà mình chưa có thể đề cập tới trong bài viết và cả những thứ mà mình chưa trải qua.

Tiếp theo chúng ta sẽ đưa Component chứa Form data vào ứng dụng của chúng ta bằng cách khai báo trong Module, vì sau này chúng ta sẽ có thêm một vài Component khai báo trong thư mục parties, nên bây giờ chúng ta xây dựng luôn 1 file index dùng để export 1 mảng các Component trong parties. File “`client/imports/app/parties/index.ts:

import { PartiesFormComponent } from './parties-form.component';

export const PARTIES_DECLARATIONS = [
PartiesFormComponent
];

Đơn giản chỉ là import PartiesFormComponent vào và export ra ở trong 1 mảng. Và bây giờ ở AppModule chúng ta sẽ add mảng Component này vào phần khai báo (declarations). Và cũng đừng quên 1 công việc quan trọng mà chúng ta đang còn dở dang, đó chính là khai báo FormsModule để sử dụng các Directive, Service… trong Module đó. File AppModule sau khi thêm vào các thành phần trên sẽ có nội dung như sau:

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

import { AppComponent } from './app.component';
import { PARTIES_DECLARATIONS } from './parties';

@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
AppComponent,
...PARTIES_DECLARATIONS
],
bootstrap: [
AppComponent
]
})
export class AppModule {}

Chúng ta thực hiện import FormsModule và ReactiveFormsModule những module cung cấp những thứ mà chúng ta cần để có thể sử dụng với form trong Angular 2. Và sau khi import thì chúng ta sẽ đưa chúng vào trong thuộc tính imports trong Decorator của AppModule để các Component trong Module đều có thể sử dụng được.

Và phần còn lại chính là thêm PartiesFormComponent vào trong Module, ở file index trong thư mục parties chúng ta đã export 1 mảng PARTIES_DECLARATIONS có chứa PartiesFormComponent, bây giờ chúng ta sẽ import file index vào, sử dụng mảng PARTIES_DECLARATIONS trên và đưa vào trong phần khai báo (declarations) của AppModule. Ở trên người ta sử dụng một đặc tính mới của ES6 (Javascript 2015) gọi là Spread syntax, bằng việc sử dụng syntax này, một mảng dữ liệu sẽ được tách ra thành từng phần tử riêng biệt. Ví dụ như trong trường hợp trên khi ta dùng ...PARTIES_DECLARATIONS tức là mang các phần tử trong PARTIES_DECLARATIONS ra và đặt vào vị trí đó.

Đó là về ý tưởng sử dụng, còn chi tiết hơn các bạn có thể xem thêm về Spread syntax và một thứ ngược lại với Spread là Rest Parameter, các bạn cũng có thể xem thêm cho biết để khi thấy người ta sử dụng sẽ bớt bỡ ngỡ hơn 😀 :D..

Xong rồi phải không mọi người nhỉ, bây giờ chúng ta thử start app xem thế nào nào :D..

5-parties-without-form
Kết quả hiển thị – Chưa có Form

Có vẻ mọi thứ không như ta mong đợi, trên trang View không thấy có gì thay đổi so với code trước đó, không có gì thêm. Dạo lại 1 vòng chúng ta sẽ thấy 1 thứ mà chúng ta chưa có nói tới. Selector của Component :D.. Ở trong file index.html (client/index.html) chứa thẻ

<app></app>

để AppComponent select nó và render template vào thẻ đó, thì giờ chúng ta cũng sẽ làm điều tương tự. PartiesFormComponent đã select thẻ

<parties-form></parties-form>

vậy bây giờ chúng ta sẽ thêm thẻ đó vào vị trí mà chúng ta muốn nó xuất hiện, mình muốn nó nằm ở phía trên danh sách parties trong AppComponent, vậy nên mình sẽ thêm vào template của AppComponent 1 thẻ parties-form và nội dung sẽ là:

<div>
<parties-form></parties-form>

<ul>
<li *ngFor="let party of parties | async">
{{party.name}}
<p>{{party.description}}</p>
<p>{{party.location}}</p>
</li>
</ul>
</div>

Nếu ứng dụng đang chạy thì nó sẽ tự động refresh lại trang, còn nếu các bạn có tắt rồi thì start lại và kết quả… tà đà.

5-parties-form
Kết quả hiển thị – Đã có Form

Và cuối cùng thì form cũng đã hiển thị lên trên danh sách parties và như vậy là các bạn có thể thêm dữ liệu vào ứng dụng từ View.


Đôi chút về thư mục imports trong Meteor:

Trước đây Meteor load hết toàn bộ các file trong thư mục imports lên (EAGER loading) và có thể dùng ở chỗ khác thoải mái, tuy nhiên như thế thì rất là phí tài nguyên khi phải load lên tất cả trong khi có những cái thì dùng nhiều, có những cái lại không bao giờ dùng hoặc là ít. Nên từ bản 1.3 Meteor đã thay đổi và không còn EAGER load ở thư mục này nữa, thay vào đó là khi nào cần dùng thì import vào và dùng thôi 😀 😀 (LAZY loading).

II. Routing và Multiple-Views:

Tiếp theo bài viết, như đã đề cập ở đầu bài viết thì chúng ta sẽ tiếp tục chia sẻ về Routing và Multiple-View, việc Routing giúp cho người duyệt web có thể biết được vị trí hiện tại của mình trong ứng dụng, cũng là một phần khá là quan trọng trong vấn đề trải nghiệm người dùng. Và chúng ta sẽ bắt đầu tìm hiểu nó bằng cách tạo 1 Component dùng để hiển thị danh sách các Party, từ danh sách Party này chúng ta sẽ xem thông tin chi tiết của từng Party bằng cách click vào Party cần xem, như thế 1 Component nữa chúng ta cần tạo đó chính là Component dùng để hiển thị thông tin chi tiết của 1 Party.

PartiesListComponent:

Component này dùng để hiển thị danh sách các Party, và cho phép người dùng xóa 1 Party bất kỳ trong danh sách. Như vậy chúng ta sẽ chuyển code hiển thị danh sách các Party từ AppComponent đem bỏ vào PartiesListComponent. AppComponent bây giờ chỉ còn dùng để làm Component root, để bao các Component khác theo một cấu trúc cây mà Angular 2 mong muốn, theo hướng thiết kế ứng dụng kiểu Component, thì việc tái sử dụng code cũng sẽ rất là tiện, chưa kể nó còn rất tiện cho việc thiết kế ứng dụng theo kiểu layout. Bắt đầu từ việc chuyển code từ AppComponent sang PartiesListComponent:

Tạo file: client/imports/app/parties/parties-list.component.ts và copy nội dung từ AppComponent sang PartiesListComponent, sau đó thay đổi các đường dẫn import cùng với selector… và nội dung của PartiesListComponent sẽ là:

import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';

import template from './parties-list.component.html';

@Component({
selector: 'parties-list',
template
})
export class PartiesListComponent {
parties: Observable<Party[]>;

constructor() {
this.parties = Parties.find({}).zone();
}

removeParty(party: Party): void {
Parties.remove(party._id);
}
}

Chúng ta thêm luôn phương thức cho phép xóa 1 Party dựa vào giá trị _id của nó, giá trị _id này được sinh ra bởi Mongo khi chúng ta thêm dữ liệu mới vào, hoặc là Mongo tự generate hoặc các bạn cũng có thể truyền vào cái _id cho đối tượng dử liệu mà các bạn muốn thêm(insert) vào database. Phương thức này nhận 1 tham số đầu vào là party (kiểu dữ liệu là Party) và không có kiểu dữ liệu trả về (return), phần nội dung của phương thức sử dụng của MongoObservable Collection để thực hiện xóa 1 đối tượng trong database có _id tương ứng. Còn lại là phần Template của Component.

Tạo file: client/imports/app/parties/parties-list.component.html, copy nội dung từ template của AppComponent sang và thêm button gọi tới phương thức remove đã cài đặt ở trên:

<div>
<parties-form></parties-form>

<ul>
<li *ngFor="let party of parties | async">
{{party.name}}
<p>{{ party.description }}</p>
<p>{{ party.location }}</p>
<button (click)="removeParty(party)">X</button>
</li>
</ul>
</div>

Không khác gì so với AppComponent lúc trước đúng không các bạn :)).. đây cũng là một tính năng đặc biệt của Component mà Angular có đề cập tới, đó là khả năng tái sử dụng(Reuse) mã nguồn, khi mà chỉ cần chuyển qua mà không phải thay đổi gì nhiều. Sau khi có PartiesListComponent, thì AppComponent (client/imports/app/app.component.ts) của chúng ta sẽ chỉ còn:

import { Component } from '@angular/core';
import template from './app.component.html';

@Component({
selector: 'app',
template
})
export class AppComponent { }

Và file template, client/imports/app/app.component.html:

<div></div>

Một cái nữa mà mình cần các bạn chuẩn bị trước khi gắn kết mọi thứ lại với nhau, như chúng ta đã đề ra thì có 1 trang danh sách các party, để hiện thị thông tin cần thiết của các party, và giờ chúng ta còn cần 1 cái nữa là 1 trang hiển thị thông tin chi tiết của Party. Chúng ta sẽ tạo Component PartyDetailsComponent.

PartyDetailsComponent:

Mục đích của việc tạo Component này là để hiển thị thông tin chi tiết của 1 Party, và mục đích sâu xa hơn là để áp dụng cái Routing vào :D, để từ danh sách các Party, chúng ta sẽ click vào Party cần xem thông tin và hiển thị ra trang details thôi 😀 :D.. Tạo file client/imports/app/parties/party-details.component.ts:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import 'rxjs/add/operator/map';

import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';

import template from './party-details.component.html';

@Component({
selector: 'party-details',
template
})
export class PartyDetailsComponent implements OnInit {
partyId: string;
party: Party;

constructor(
private route: ActivatedRoute
) {}

ngOnInit() {
this.route.params
.map(params => params['partyId'])
.subscribe(partyId => {
this.partyId = partyId

this.party = Parties.findOne(this.partyId);
});
}
}

Trong component PartyDetailsComponent này có thêm một số thành phần khá là mới mẻ, tuy nhiên mình xin phép được đề cập ở phía dưới sau, bây giờ các bạn chỉ cần hiểu mục đích của đoạn code trong ngOnInit là để lấy tham số _id truyền qua từ PartiesListComponent sang PartyDetailsComponent thông qua Routing và dùng tham số _id đó để tìm Party tương ứng và hiển thị ra cho người dùng thông qua template(client/imports/app/parties/party-details.component.html):

<h2>{{ party.name }}</h2>
<p>{{ party.description }}</p>
<p>{{ party.location }}</p>

Component cuối cùng mà chúng ta cần cũng đã xong, bây giờ chúng ta sẽ cùng nhìn lại chúng ta đã có gì, chúng ta cần gì và tiếp theo chúng ta sẽ làm gì.

  1. AppComponent, phần logic hiện tại đã không còn lại gì, chỉ còn lại template trơ trọi. Tạm thời thì nó sẽ chỉ có 1 thẻ div, tuy nhiên mục đích của chúng ta chính là bao các template của các Component khác trong thẻ div đó. Tới phần Routing các bạn sẽ thấy cụ thể.
  2. PartiesListComponent, phần logic sử dụng phần code logic trước đây của AppComponent, lấy danh sách các Party từ database và hiển thị lên cho người dùng thông qua template. Và cho phép người dùng có thể xoá bỏ Party bằng button X. Tiếp theo từ danh sách các Party này chúng ta sẽ cho người dùng click và đi tới trang details của Party.
  3. PartyDetailsComponent, Component này đơn giản chỉ là nhận id của 1 Party từ PartiesList và sau đó tìm trong database và hiển thị thông tin chi tiết của Party tương ứng, việc nhận id của 1 party từ PartiesListComponent được thực hiện thông qua Router và bay giờ chúng ta sẽ kết thúc với Router trong Angular 2.

Routing và Navigation:

Trong Angular, với các module, directive chính thì nằm trong package @angular/core, của form thì nằm trong @angular/form còn của router thì nằm trong @angular/router, bây giờ chúng ta sẽ tiến hành áp dụng Router vào và sử dụng. Bước đầu tiên chính là khai báo Routing, với khai báo này chúng ta sẽ gán cho các Component muốn hiển thị lên một đường dẫn tương ứng (url), để khi truy cập vào url đó, hoặc chuyển trang từ 1 Component khác thì người dùng có thể thấy được cái ô đường dẫn trên trình duyệt thay đổi theo ý mình muốn. Tạo file client/imports/app/app.routes.ts:

import { Route } from '@angular/router';

import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';

export const routes: Route[] = [
{ path: '', component: PartiesListComponent },
{ path: 'party/:partyId', component: PartyDetailsComponent }
];

Đoạn code trên thực hiện tạo 1 mảng các Object Route và trong đó, mỗi router tương ứng với mỗi Component, Component PartiesListComponent có pattern là ”, tức là khi truy cập vào chúng ta sẽ dùng url: localhost:3000/ với trường hợp chạy local. Và Component PartyDetailsComponent thì có pattern là ‘party/:partyId’, khi truy cập vào thì url: localhost:3000/party/anRTC77eLYbBAmrir, ở đây: partyId chính là một tham số truyền vào cho Component để sử dụng và truyền tham số sẽ có dạng là : và trong Component muốn lấy thì sẽ lấy dựa vào cái tên này. Tiếp tục bay vô khai báo cho anh Module.

Để sử dụng Router thì chúng ta cần import RouterModule, chứa các class cần thiết để hỗ trợ cho Routing. Sau khi import RouteModules thì kế đến đó là mang cái routes mà chúng ta đã định nghĩa ở trên import vào trong module để sử dụng. Như vậy file module (client/imports/app/app.module.ts) sẽ có nội dung như sau:

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

import { AppComponent } from './app.component';
import { PARTIES_DECLARATIONS } from './parties';

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

@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(routes)
],
declarations: [
AppComponent,
...PARTIES_DECLARATIONS
],
bootstrap: [
AppComponent
]
})
export class AppModule {}

RouterModule sẽ được import vào với tham số là các Router mà chúng ta đã định nghĩa ở trên trong app.routes. Với phương thức forRoot thì mỗi ứng dụng chỉ có 1 cái, còn các Module con muốn có Router thì sẽ sử dụng forChild để import vào. Chúng ta sẽ có dịp tìm hiểu và sử dụng cái này trong các bài sau. Như vậy là chúng ta đã khai báo và import xong cái Router. Còn thiếu gì không nhỉ? Làm sao để từ PartiesListComponent chúng ta truy cập được vào PartyDetailsComponent?

Một khái niệm tiếp theo trong Router mà các bạn có thể đọc chi tiết hơn ở trang Document của Angular, đó chính là RouteLink, thực ra nó cũng không có gì nhiều, là sự kết hợp sử dụng các Directive trong RouterModule để tạo ra 1 cái link, từ link này trong PartiesListComponent chúng ta sẽ chuyển trang được qua PartyDetailsComponent. Như vậy chúng ta sẽ thêm vào trong template của PartiesListComponent các thẻ <a> để có thể chuyển trang và cụ thể file template(client/imports/app/parties/parties-list.component.html) sẽ có nội dung:

<div>
<parties-form></parties-form>

<ul>
<li *ngFor="let party of parties | async">
<a [routerLink]="['/party', party._id]">{{party.name}}</a>
<p>{{party.description}}</p>
<p>{{party.location}}</p>
<button (click)="removeParty(party)">X</button>
</li>
</ul>
</div>

Thẻ anchor trên sử dụng 1 Directive được cung cấp từ RouterModule đó chính là routerLink, với cú pháp Binding 1 chiều gán mảng [‘/party’, party._id] cho directive routerLink. Với cú pháp như trên là chúng ta có thể chuyển trang tới trang PartyDetailsComponent đi kèm với 1 partyId là giá trị của party._id. Xong chưa nhỉ? Chúng ta đã tới được PartyDetailsComponent từ PartiesList. Việc mà chúng ta còn bỏ dở đó chính là truy vấn cái party tương ứng để hiển thị cho người dùng:

export class PartyDetailsComponent implements OnInit {
partyId: string;
party: Party;

constructor(
private route: ActivatedRoute
) {}

ngOnInit() {
this.route.params
.map(params => params['partyId'])
.subscribe(partyId => {
this.partyId = partyId

this.party = Parties.findOne(this.partyId);
});
}
}

Đoạn code trên là của PartyDetails mà chúng ta đã định nghĩa trước, giờ mình sẽ giải thích thêm, trong constructor, chúng ta đang sử dụng khái niệm Dependency Injection trong Angular 2. Service ActivatedRoute đã được định nghĩa trong thư viện @angular/router, giờ sử dụng thì chúng ta chỉ cần đưa vào constructor để Inject vào và sử dụng trong Component. Cái ActivatedRoute này sẽ cung cấp cho chúng ta những thuộc tính và phương thức cho phép chúng ta thao tác với cây route hiện tại. Hiện tại ở đây được gọi là 1 state, 1 quá trình chuyển qua chuyển lại giữa các trang tạo thành 1 vòng đời và cũng có 1 cái tên đó là RouterState, những cái này mình sẽ để cập chi tiết hơn trong series Angular 2 của Webbynat. Như vậy chúng ta sẽ dùng cái ActivatedRo này để lấy partyId từ PartiesList.

Đầu tiên với sự kiện ngOnInit thì chúng ta sẽ tạo 1 subscription để khi có data tới thì chúng ta sẽ nhận về. Từ subscription this.route.params, phương thức map mà RxJS cung cấp cũng được sử dụng ở đây, để map data từ stream ra, cũng không khác gì với Array là mấy, chỉ là nó lấy từ Stream, chứ ko phải từ Array. Và cuối cùng là findOne, tìm 1 đối tượng Party trong Database và gán cho thuộc tính party của Component để hiển thị cho người dùng.

Một chút thêm vào cho thú vị đó chính là chức năng quay về trang chủ từ trang details, chúng ta sẽ thêm 1 routerLink trong trang details, để khi click thì chúng ta có thể trở về trang chủ(trang '/'). Như vậy file template của PartyDetailsComponent sẽ có nội dung:

<h2>{{ party.name }}</h2>
<p>{{ party.description }}</p>
<p>{{ party.location }}</p>

<a [routerLink]="['/']">Back to home</a>

Xong việc di chuyển code từ AppComponent sang PartiesListComponent và khai báo PartyDetailsComponent, giờ phải khai báo 2 cái Component mới cho anh Module ảnh biết với, chứ ko là ảnh ko cho xài đâu, và chúng ta sẽ sửa file index.ts (client/imports/app/parties/index.ts) trong thư mục parties, vì hôm trước chúng ta đã khai báo 1 mảng các Component trong Module thông qua mảng PARTIES_DECLARATIONS sử dụng spread syntax, giờ thì chỉ việc import vào và bỏ trong mảng PARTIES_DECLARATIONS là oke :D.

import { PartiesFormComponent } from './parties-form.component';
import { PartiesListComponent } from './parties-list.component';
import { PartyDetailsComponent } from './party-details.component';

export const PARTIES_DECLARATIONS = [
PartiesFormComponent,
PartiesListComponent,
PartyDetailsComponent
];

Và cuối cùng là nơi để chúng ta bỏ các View vào để hiển thị, Angular gọi nó với 1 cái tên là Router Outlet, là 1 Component select thẻ router-outlet trong template để hiển thị Component tương ứng với URL khi Routing. Như vậy chúng ta sẽ thêm thẻ router-outlet vào trong template của Root Component đó là AppComponent(file: client/imports/app/app.component.html), file template sẽ có nội dung:

<div>
<router-outlet></router-outlet>
</div>

Start app và chờ đợi kết quả :D..

5-parties-form
Trang chủ (url: ‘/’)
5-party-details
Trang thông tin chi tiết (url: ‘/party/:partyId’)

Tổng kết:

Như vậy là chúng ta đã hoàn thành việc chia sẻ và tìm hiểu về quản lý dữ liệu, và gắn Route cho các Component. Trong bài thì các bạn cũng được thấy khá nhiều khái niệm rồi đúng không :D.. Mình cũng xin tổng kết lại một tí xíu.

Phần Dữ liệu, chúng ta tìm hiểu về khái niệm form trong Angular 2, với 2 loại là Model-driven form và Template-driven form. Về việc sử dụng 2 loại form này sẽ tùy vào cách sáng tạo của các bạn, các bạn có thể sử dụng chúng trong mọi hoàn cảnh cần sử dụng form, tuy nhiên với từng hoàn cảnh thì mỗi loại form sẽ phát huy một ưu điểm riêng. Việc đó là tùy vào khả năng tư duy lập trình cũng như sự sáng tạo trong lập trình của các bạn :D..

Bên cạnh đó thì chúng ta cũng được biết thêm về một số thao tác cơ bản với Collections trong Mongo sử dụng MongoObservable, chúng ta sẽ còn nghịch ngợm với Observable rất là nhiều nữa nên các bạn cứ thoải mái hưởng thụ những thứ ở hiện tại để khi đi vào chi tiết sẽ dễ tiếp cận hơn rất là nhiều.

Cuối cùng là về Routing, thì về khái niệm Routing nó cũng không khác gì so với những khái niệm Routing trong những công nghệ khác, nhất là vòng đời, RouterState, một vòng đời của 1 router được xuyên suốt giữa các View và chỉ mất đi khi chúng ta truy cập 1 trang khác từ trình duyệt, hoặc là chuyển trang trực tiếp tới 1 View khác (redirect). Bài viết còn đề cập về cách khai báo, sử dụng Routing để chúng ta có thể xây dựng ứng dụng Multi Views, và cả việc thay đổi url trên ô địa chỉ của trình duyệt web, cũng đóng góp quan trọng trong trải nghiệm người dùng.

Như vậy là chúng ta đã trải qua 4 bài viết, và cũng có được khá nhiều kiến thức về cả Angular và Meteor. Hy vọng bài viết sẽ giúp ích cho nhiều người có nhu cầu tìm hiểu về Angular và Meteor. và mình rất sẵn lòng để cùng thảo luận và chia sẻ thêm ở dưới comment, nên nếu có thắc mắc hay đóng góp gì thì các bạn comment ở dưới giúp mình nhé. Bài tiếp theo mình sẽ bàn về vấn đề binding dữ liệu trong Angular. Cũng là một khái niệm quan trọng trong Angular, để có thể tương tác real-time với dữ liệu (Reactive). Hẹn gặp lại mọi người trong bài viết tới: Binding Data, tương tác giữa Component và Template.

Hồ Quốc Toản

Tài liệu tham khảo:

[1] Angular 2 Meteor: Data Management

[2] Angular 2 Meteor: Routing and Multiple Views

[3] Forms in Angular

[4] Routing

3 thoughts on “[Angular 2 – Meteor]. 3. Quản lý dữ liệu và chuyển trang trong ứng dụng Angular Meteor.

  1. Pingback: [Angular 2 – Meteor]. 4. Data Binding, trói buộc dữ liệu của Template và code logic trong Component. – Webbynat

  2. Pingback: [Angular 2 – Meteor]. 4. Data Binding, trói buộc dữ liệu giữa Template và code logic trong Component. – Webbynat

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s