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

Đây là bài thứ 4 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 lại các bài viết khác ở đây:

Ở bài viết trước chúng ta đã học về Angular 2 data binding và hôm nay bạn sẽ học về routing, đó là cách điều hướng giữa các phần khác nhau của ứng dụng.

Code mẫu

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

Cùng xem lại ứng dụng của chúng ta

Đến thời điểm này, chúng ta đã phát triển một ứng dụng Angular 2 nhỏ. Với 2 component, PeopleListComponet hiển thị danh sách các nhân vật và PersonDetailsComponent hiển thị thông tin chi tiết về nhân vật được chọn.

Bây giờ hãy tưởng tượng bạn đang làm việc với UX designer và anh ấy phàn nàn với bạn là: Không! Cậu đang làm cái gì vậy? Có quá nhiều thông tin trên cùng một trang, người dùng của chúng ta sẽ bị quá tải! Và bạn nói: hãy bình tĩnh. Mình có giải pháp! và bạn bắt đầu thiết kế lại ứng dụng, chia nó làm 2 view, một cho danh sách các nhân vật và cái còn lại để hiển thị thông tin chi tiết.

Angular 2 routing

Angular 2 cung cấp cho bạn khả năng chia ứng dụng thành các view riêng biệt và bạn có thể điều hướng giữa các view thông qua routing. Nó cho phép bạn điều hướng người dùng tới các component khác nhau dựa trên url.

Nếu bạn mở file systemjs.config.js bạn sẽ thấy router trong số các packages của Angular 2:

var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'forms',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',                    // <===== here!
    'router-deprecated',
    'upgrade',
  ];

Nếu bạn đã làm việc với Angular 1 và ngRoute bạn sẽ thấy phần còn lại của bài viết này khá quen thuộc, điểm khác biệt lớn nhất là thay vì mapping các route với các controller, Angular 2 map các route tới các component, và Angular 2 thiên về khai báo nhiều hơn.

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

Thiết lập route people list 

Angular 2 cấu hình các route thông qua một file cấu hình gồm:

  • Một mảng Routes sẽ chứa tập hợp các route.
  • Một export cung cấp router tới phần còn lại của ứng dụng.

Chúng ta sẽ bắt đầu với việc tạo file app.routes.ts trong thư mục app và import interface Routes từ module @angular/router:

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

Bây giờ, chúng ta sẽ định nghĩa route mặc định của ứng dụng là danh sách các nhân vật Star Wars. Trước khi làm việc đó chúng ta cần import PeopleListComponet:

import { PeopleListComponent } from './people-list.component';

Và tạo một mảng Routes:

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

Như bạn thấy, route đầu tiên map path persons tới component PeopleListComponent. Chúng ta nói với Angular 2 rằng, đây là route mặc định bằng cách thêm một cấu hình, map với một path rỗng và redirect tới path persons.

Bước tiếp theo là làm cho routes của chúng ta có thể sử dụng trong phần còn lại của ứng dụng. Trước hết, chúng ta cần import RouterModule từ module @angular/router:

import { Routes, RouterModule }  from '@angular/router';

Và export các route đã định nghĩa như sau:

export const routing = RouterModule.forRoot(routes);

Toàn bộ file cấu hình route app.routes.ts sẽ như thế này:

import { Routes, RouterModule } from '@angular/router';
import { PeopleListComponent } from './people-list.component';

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

export const routing = RouterModule.forRoot(routes);

Bây giờ, chúng ta có thể cập nhật ứng dụng để sử dụng các route đã định nghĩa trong file cấu hình. Hãy import routing trong file app.module.ts:

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

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 ],
  declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Cần lưu ý, mặc dù bạn thường định nghĩa routing ở mức ứng dụng trong file cấu hình, nhưng bạn cũng có thể làm điều đó ở mức component để tối đa hóa khả năng tái sử dụng và độc lập.

Mặc dù, đã cung cấp các thông tin về route cho ứng dụng, chúng ta vẫn sử dụng component <people-list> trực tiếp trong template của AppComponent. Cái chúng ta thực sự muốn là lựa chọn component được hiển thị dựa trên đường dẫn trong trình duyệt. Để làm điều đó, chúng ta sử dụng router-outlet directive, một Angular 2 Routing directive để hiển thị route active (giống ng-view):

<h1>{{title}}</h1>
<router-outlet></router-outlet>

Toàn bộ AppComponent sẽ như thế này:

import { Component } from '@angular/core';
import { PeopleService } from './people.service';

@Component({
  selector: 'my-app',
  template: `
  <h1> {{title}} </h1>
  <router-outlet></router-outlet>
  `,
  providers: [PeopleService]
})
export class AppComponent {
  title:string = 'Star Wars Peoplez!';
}

Nếu bạn gõ lệnh npm start, bạn sẽ thấy app bị lỗi. Sử dụng dev tools của trình duyệt bạn sẽ thấy thông báo lỗi trong console như sau:

Subscriber.ts:243 Uncaught EXCEPTION: Error during instantiation of LocationStrategy! (Router -> Location -> LocationStrategy).
ORIGINAL EXCEPTION: No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.

Bạn chỉ cần bổ sung thêm thẻ base trong phần head của file index.html như dưới đây:

<html>
  <head>
    <title>Angular 2 QuickStart</title>
    <base href="/">
    <!-- etc... -->
 </head>
<!-- etc... -->
</html>

Thẻ này cho phép api history.pushState của HTML5, giúp Angular 2 cung cấp các URL không có tiền tố #.

Bây giờ nếu gõ lệnh npm start một lần nữa bạn sẽ thấy ứng dụng làm việc. Và url trên trình duyệt sẽ như thế này:

http://localhost:3000/persons

Điều này có nghĩa là routing đã làm việc như mong đợi.

Thiết lập person details route

Bây giờ, chúng ta sẽ thiết lập person details route và thay đổi workflow của ứng dụng, chúng ta sẽ hiển thị thông tin chi tiết trong một view hoàn toàn khác.

Chúng ta bắt đầu bằng việc import PersonDetailsComponent và định nghĩa một route mới trong app.routes.ts:

import { Routes, RouterModule } from '@angular/router';
import { PeopleListComponent } from './people-list.component';

// import PersonDetailsComponent
import { PersonDetailsComponent } from './person-details.component';

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/persons/:id' to person details component
  {
    path: 'persons/:id',
    component: PersonDetailsComponent
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

export const routing = RouterModule.forRoot(routes);

Chúng ta đã định nghĩa route map /persons/:id tới PersonDetailsComponent. :id là tham số, được sử dụng để xác định nhân vật sẽ hiển thị thông tin chi tiết.

Điều đó có nghĩa là, chúng ta cần cập nhật thuộc tính id trong interface Person như sau:

export interface Person {
  id: number;
  name: string;
  height: number;
  weight: number;
}

Và service PeopleService cũng cần cập nhật thêm thuộc tính id trong mảng PEOPLE:

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

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

@Injectable()
export class PeopleService{
  getAll() : Person[] {
    return PEOPLE;
  }
}

Tạo các liên kết route

Bây giờ, chúng ta đã định nghĩa route nhưng chúng ta muốn người dùng có thể truy cập tới trang hiển thị thông tin chi tiết khi họ click vào một nhân vật trong view hiển thị danh sách các nhân vật. Làm thế nào chúng ta có thể làm điều đó?

Angular 2 cung cấp directive [routerLink] giúp bạn tạo ra các liên kết cực kỳ đơn giản.

Chúng ta sẽ cập nhập template của PeopleListComponet để sử dụng [routerLink] thay vì event (click). Chúng ta cũng cần xóa phần tử <person-details> trong template.

 

<ul>
    <li *ngFor="let person of people">
      <a href="#" [routerLink]="['/persons', person.id]">
      {{person.name}}
      </a>
    </li>
</ul>

Trong phần source code phía trên bạn có thể thấy cách chúng ta liên kết một mảng các tham số route với directive routerLink, một tham số là đường dẫn của route /persons và tham số còn lại là id thực sự. Với 2 tham số này Angular 2 có thể tạo ra các liên kết phù hợp (VD: /person/2).

Chúng ta cũng cần xóa bỏ một vài đoạn code không cần thiết như các hàm xử lý sự kiện (click) và thuộc tính selectedPerson. PeopleListComponent sau khi cập nhật sẽ như thế này:

import { Component, OnInit } from '@angular/core';
import { Person } from './person';
import { PeopleService } from './people.service';

@Component({
  selector: 'people-list',
  template: `
  <!-- this is the new syntax for ng-repeat -->
  <ul>
    <li *ngFor="let person of people">
        <a href="#" [routerLink]="['/persons', person.id]">
      {{person.name}}
      </a>
    </li>
  </ul>
  `
})
export class PeopleListComponent implements OnInit{
  people: Person[] = [];

  constructor(private peopleService : PeopleService){ }

  ngOnInit(){
    this.people = this.peopleService.getAll();
  }
}

Ok, bây giờ hãy quay trở lại trình duyệt. Nếu bạn di chuột lên tên của mỗi nhân vật bạn sẽ nhìn thấy nó trỏ tới liên kết chính xác.

Nếu bạn click vào tên nhân vật nó sẽ không làm việc. Bởi vì PersonDetailsComponent không biết cách nhận id từ route, nhận một nhân vật với id và hiển thị nó trong view.

Tiếp theo, hãy làm điều đó!

Tách các tham số từ routes

Trong phần trước PersonDetailsComponent sử dụng thuộc tính person nhận dữ liệu về nhân vật được chọn. Khi sử dụng route chúng ta sẽ không làm như vậy.

Chúng ta sẽ cập nhập PersonDetailsComponent để tách thông tin từ route, nhận dữ liệu phù hợp sử dụng PeopleService và hiển thị nó.

Angular 2 routing cung cấp service ActivateRoute cho mục đích này, lấy các tham số route. Chúng ta có thể import nó từ module @angular/router và tiêm nó vào PersonDetailsComponent thông qua hàm khởi tạo: 

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

export class PersonDetailsComponent{
    constructor(private peopleService: PeopleService,
               private route: ActivatedRoute){
    }
    // more codes...
}

Bây giờ chúng ta có thể sử dụng nó để nhận tham số id từ url và lấy thông tin về nhân vật từ PeopleService. Chúng ta sẽ làm điều đó trong ngOnInit:

export class PersonDetailsComponent implements OnInit {
    person: Person;

    // more codes...

    ngOnInit(){
        this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }
}

Chú ý, route.params trả lại một observable, một pattern để xử lý các thao tác bất động bộ. Phương thức subscribe giúp chúng ta nạp một route và nhận các tham số. 

Để tránh memory leaks chúng ta có thể sử dụng unsubscrible cho observable route.params khi Angular destroy PersonDetailsComponent. Chúng ta có thể tận dụng lợi thế của onDestroy:

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

export class PersonDetailsComponent implements OnInit {
    // more codes...
    sub: any;

    ngOnInit(){
        this.sub = this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }

    ngOnDestroy(){
        this.sub.unsubscribe();
    }
}

PersonDetailsComponent khi hoàn thành sẽ như thế này:

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

import { Person } from './person';
import { PeopleService } from './people.service';

@Component({
  selector: 'person-details',
  template: `
  <!-- new syntax for ng-if -->
  <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>
  `
})
export class PersonDetailsComponent implements OnInit, OnDestroy {
    person: Person;
    sub: any;

    constructor(private peopleService: PeopleService,
               private route: ActivatedRoute){
    }

    ngOnInit(){
        this.sub = this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }

    ngOnDestroy(){
        this.sub.unsubscribe();
    }
}

Chúng ta cũng cần cập nhật PeopleService để cung cấp một phương thức mới để lấy dữ liệu về một nhân vật bằng id, phương thức get(id: number) dưới đây sẽ làm điều đó:

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

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

@Injectable()
export class PeopleService{
  getAll() : Person[] {
    return PEOPLE;
  }
  get(id: number) : Person {
    return PEOPLE.find(p => p.id === id);
  }
}

Chúng ta đã hoàn thành! Bây giờ nếu click vào một nhân vật, bạn sẽ thấy thông tin chi tiết về nhân vật đó.

Nhưng làm thế nào bạn có thể trở lại view chính (view danh sách các nhân vật). Bạn có thể làm điều đó bằng cách nhấn nút back của trình duyệt. Nhưng nếu bạn muốn có một nút back ở phía dưới thông tin chi tiết thì sao?

Trở lại danh sách các nhân vật

Bây giờ, chúng ta muốn tạo ra một nút cho phép người dùng trở lại view chính khi anh/cô ấy click vào nó. Angular 2 Routing cung cấp service Router giúp bạn làm điều đó.

Chúng ta bắt đầu bằng cách thêm nút vào template và liên kết nó với phương thức goToPeopleList:

 <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>
 <! -- NEW BUTTON HERE! -->
 <button (click)="gotoPeoplesList()">Back to peoples list</button>

Chúng ta có thể tiêm service này vào PersonDetailsComponent thông qua hàm khởi tạo. Chúng ta bắt đầu bằng cách import nó từ @angular/router:

import { ActivatedRoute, Router} from '@angular/router';

Và sau đó tiêm nó:

export class PersonDetailsComponent implements OnInit, OnDestroy {
    // other codes...
    constructor(private peopleService: PeopleService,
                private route: ActivatedRoute,
                private router: Router){
    }
}

Tiếp theo là phương thức goToPeopleList để điều hướng trở lại view chính:

export class PersonDetailsComponent implements OnInit, OnDestroy {
    // other codes...

    gotoPeoplesList(){
        let link = ['/persons'];
        this.router.navigate(link);
    }
}

Chúng ta gọi phương thức router.navigate và truyền cho nó các tham số cần thiết giúp Angular 2 routing xác định nơi chúng ta muốn tới. Trong trường hợp này, route không yêu cầu bất kỳ tham số nào, chúng ta chỉ cần tạo một mảng với đường dẫn /persons.

Nếu bạn trở lại trình duyệt và kiểm tra, bạn sẽ thấy mọi thứ hoạt động đúng như mong đợi. Bây giờ, bạn đã học Angular 2 routing.

Đây là source code đầy đủ của PersonDetailsComponent:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Person } from './person';
import { PeopleService } from './people.service';

@Component({
  selector: 'person-details',
  template: `
  <!-- new syntax for ng-if -->
  <section *ngIf="person">
    <h2>You selected:  {{person.name}}  </h2>
    <h3>Description</h3>
    <p>
       {{person.name}}  weights  {{person.weight}} and is  {{person.height}} tall.
    </p>
    <! -- NEW BUTTON HERE! -->
    <button (click)="gotoPeoplesList()">Back to peoples list</button>
  </section>
  `
})
export class PersonDetailsComponent implements OnInit, OnDestroy {
    person: Person;
    sub: any;

    constructor(private peopleService: PeopleService,
                private route: ActivatedRoute,
                private router: Router){
    }

    ngOnInit(){
        this.sub = this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }

    ngOnDestroy(){
        this.sub.unsubscribe();
    }

    gotoPeoplesList(){
        let link = ['/persons'];
        this.router.navigate(link);
    }
}

Một tùy chọn khác là bạn có thể sử api window.history trong phương thức goToPeopleList như bên dưới:

gotoPeoplesList(){
    window.history.back();
}

Bạn muốn đọc nhiều hơn về Angular 2 routing?

Kết luận

Bạn đã học về component, service, dependency injection, sự khác biệt giữa các tùy chọn data binding và routing.

Phần tiếp theo chúng ta sẽ học Forms và Validation!