เนื้อหาในตอนที่ 6 นี้เราจะมาดูเพิ่มเติมเกี่ยวกับ routing ต่อจากตอนที่ 5 ที่ผ่านมา โดยใช้ user module
ประกอบคำอธิบาย ทบทวนตอนที่แล้วได้ที่บทความ
การจัดการกับ Child Routes ของ Routing ใน Angular ตอนที่ 5 http://niik.in/845
https://www.ninenik.com/content.php?arti_id=845 via @ninenik
โดยเนื้อหาในตอนนี้ เรามาดูในส่วนของการใช้งาน component ซ้ำ หรือการ re-use component กรณีที่เรามีการเปลี่ยน
path ของ route หลังเรียกแสดงข้อมูลแล้ว โดยข้อมูลที่มาแสดงจะอยู่ใน outlet ย่อยอีกที ยกตัวอย่างกรณีของตอนที่แล้ว
ถ้าเราเรียกผ่าน route path เป็น /users user-list component มาแสดงใน outlet ที่อยู่ใน user-center component user-center component มาแสดงใน outlet ที่อยู่ใน app component ถ้าเราคลิกดูรายละเอียด user แต่ละคน route path ก็จะเป็น สมมติ /users/1 user-detail component มาแสดงใน outlet ที่อยู่ใน user-center component user-center component มาแสดงใน outlet ที่อยู่ใน app component
รูปแบบของการใช้งาน outlet ในการแสดงจากเนื้อหาตอนที่แล้วก็จะเป็นในรุปแบบข้างบน
กรณีดูรายละเอียดของ user แต่ละคน จะทำให้ user-detail จะมาแทน user-list ในตำแหน่ง outlet
ที่อยู่ใน user-center component
แต่สำหรับเนื้อหาในตอนต่อไปนี้ จะมี outlet เพิ่ม รวมแล้วมีทั้งหมด 3 ตัว คือใน app component, user-center component
และเพิ่มมาใน user-list component
ถ้าเราเรียกผ่าน route path เป็น /users user-list component มาแสดงใน outlet ที่อยู่ใน user-center component user-center component มาแสดงใน outlet ที่อยู่ใน app component ถ้าเราคลิกดูรายละเอียด user แต่ละคน route path ก็จะเป็น สมมติ /users/1 user-detail component มาแสดงใน outlet ที่อยู่ใน user-list component user-list component มาแสดงใน outlet ที่อยู่ใน user-center component user-center component มาแสดงใน outlet ที่อยู่ใน app component
รูปแบบการแสดง view ใน outlet จะซ้อนกันเป็นขั้นๆ ในกรณีที่สองนี้ เมื่อเราคลิกดูรายละเอียด user แต่ละคนใน user-list
รายละเอียดของ user ใน user-detail ก็จะแสดงใน outlet ที่อยู่ใน user-list ข้อมูลก็จะต่อจากข้อมูลของ รายการ user
แต่พอเราเปลี่ยน จะไปดู อีก user โดยคลิกเปลี่ยน route path โดยที่ไม่ได้ back กลับมายัง path /users ก่อน นั้น
สมมติเป็น /users/2 ตอนนี้จะเกิดปัญหาขึ้น เพราะ user-detail component มีการถูกเรียกใช้งาน และดูข้อมูลในขั้นตอนการ
สร้างและเรียกใช้งานในครั้งแรกไปแล้ว จึงไม่มีการส่งค่าหรือดึงข้อมูลใหม่มาแสดง ด้วยเหตุผลนี้ จึงจำเป็นต้องใช้ข้อมูล
ในรูปแบบ Observable มาใช้งาน เพื่อให้เกิดการ re-use component ที่ถูกใช้งานอยู่ต่อได้
มาดูรูปแบบ การกำหนด route path ของบทความ ก่อนหน้า และบทความที่จะใช้ในตอนนี้
ไฟล์ user-routing.module.ts ของตอนที่แล้ว (ตัดมาบางส่วน)
const userRoutes: Routes = [ { path:'users', component:UserCenterComponent, children:[ { path: '', component: UserListComponent }, { path:':id', component:UserDetailComponent } ] } ];
ไฟล์ user-routing.module.ts ที่จะใช้ในตอนนี้ (บางส่วน)
const userRoutes: Routes = [ { path:'users', component:UserCenterComponent, children:[ { path: '', component: UserListComponent, children:[ { path:':id', component:UserDetailComponent } ] // end children } ] // end children } ];
สังเกตว่าในโค้ดของเนื้อหาที่จะใช้ในตอนนี้ จะมีการกำหนด children ให้กับ user-list component ที่มีการเพิ่ม
outlet ย่อยเข้ามาอีกอัน
ดังนั้นสิ่งแรกที่เราจะเพิ่มก็คือ กำหนด outlet เข้ามาในไฟล์ view ของ user-list compoent
ไฟล์ user-list.component.html
<p> user-list works! </p> <table class="table"> <thead> <tr class="active"> <th>#</th> <th>Firstname</th> <th>Lastname</th> <th>View</th> </tr> </thead> <tbody> <tr *ngFor="let user of users$ | async; let i=index;" [class.warning]="highlightId == user.id" > <td>{{ i+1 }}</td> <td>{{ user.firstname }}</td> <td>{{ user.lastname }}</td> <td> <button type="button" [routerLink]="['/users', user.id]" class="btn btn-sm btn-success"> View </button> </td> </tr> </tbody> </table> <router-outlet></router-outlet>
ในบรรทัดที่ 12 เราเปลี่ยนมาเป็นตัวแปร users$ ที่เป็น Observable (ให้เข้าใจคร่าวๆ ว่า Observable รูปแบบข้อมูลหนึ่ง)
และกำหนดให้ได้มาข้อมูลแบบ async หรือก็คือได้ข้อมูลมาทีหลังตามลำดับขั้นตอน
บรรทัดที่ 17 ปรับรูปแบบการเชื่อมโยงข้อมูล แบบ property binding ใหม่ เราสามารถใช้รูปแบบเดิมได้
รูปแบบเดิมคือ
routerLink="/users/{{ user.id }}"
เปลี่ยนมาเป็น
[routerLink]="['/users', user.id]"
ทั้งสองรูปแบบ ให้ผลลัพธ์เหมือนกัน
สุดท้ายบรรทัดที่ 23 จะเห็นว่าเรามีการกำหนด outlet เพิ่มเข้ามาอีก เป็นอันที่ 3 ดังนั้น การแสดงข้อมูลของ user
ก็จะแสดง ต่อจากตารางรายการ user ทั้งหมด
หมายเหตุ: การกำหนดตัว $ ต่อท้ายตัวแปรใดๆ นั้น เพื่อให้เป็นที่สังเกตโดยง่ายว่าตัวแปรนั้นเป็น Observable
ต่อมาก็แก้ไขส่วนของไฟล์ user-list.component.ts เป็นดังนี้
ไฟล์ user-list.component.ts
import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable'; import { User } from './user'; import { UserService } from './user.service'; @Component({ //selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent implements OnInit { public users$:Observable<User[]>; private highlightId:number|string; constructor( private userService: UserService, private route: ActivatedRoute, private router: Router ) { } ngOnInit() { this.users$ = this.route.paramMap .switchMap((params: ParamMap) => { this.highlightId = +params.get('id'); return this.userService.getUsers(); }); } }
บรรทัดที่ 3 กับ 4 เป็นเรียก import operator หรือคำสั่งในการจัดการกับ Observable และ import Observable
เข้ามาใช้งาน ตามลำดับ
บรรทัดที่ 6 import User class มาใช้งาน เพื่อที่จะใช้ในการกำหนดใน Observable
บรรทัดที่ 17 เป็นการกำหนดตัวแปร users$ สำหรับเก็บข้อมูล Observable ของ user ที่เป็น Array
public users$:Observable<User[]>;
บรรทัดที่ 27 - 31 เป็นส่วนของการดึงข้อมูล user ทั้งหมด รูปแบบข้างต้น เป็นการเขียนโค้ดให้กระชับ
เนื่องจาก paramMap นั้น เป็น Observable เราสามารถจัดการโดยใช้ operator ที่ชื่อ swichMap
ดึงข้อมูลที่สนใจได้ รูปแบบข้างต้น เราสามารถเขียนแยกแต่ละส่วนได้เป็นดังนี้
this.highlightId = this.route.paramMap .switchMap((params: ParamMap) => { return params.get('id'); }); this.users$ = this.userService.getUsers();
แต่หากรูปแบบข้างต้นนี้ เราต้องไปเปลี่ยน highlightId ให้เป็น Observable ด้วย
โดยเปลี่ยนเป็น
private highlightId:Observable<number|string>;
เพราะค่าที่ return ออกมา จะต้องเป็น Observable
แต่ในที่นี้เราจะใช้รูปแบบตามโค้ดที่กำหนด
this.users$ = this.route.paramMap .switchMap((params: ParamMap) => { this.highlightId = +params.get('id'); return this.userService.getUsers(); });
เนื่องจากเรากำหนด users$ เป็น Observable อยู่แล้ว ค่าที่ return จาก swithMap จึงสามารถ
นำมารับค่าได้เลย ส่วนตัวแปร highlightId ก็ใช้เป็นแบบปกติ รับค่า params.get('id')
การกำหนดเครื่องหมาย + ด้านหน้าก็เพื่อแปลงค่า id ที่ได้เป็น number แล้วเก็บไว้ในตัวแปร highlightId
ต่อไปก็ส่วนของไฟล์ view แสดงผลรายละเอียด user ไฟล์ user-detail.component.html
ไฟล์ user-detail.component.html
<p> user-detail works! </p> <table class="table" *ngIf="user"> <tr> <th class="active text-right" width="120">ID</th> <td>{{ user.id }}</td> </tr> <tr> <th class="active text-right" width="120">Firstname</th> <td>{{ user.firstname }}</td> </tr> <tr> <th class="active text-right" width="120">Lastname</th> <td>{{ user.lastname }}</td> </tr> <tr> <th class="active text-right" width="120">Gender</th> <td>{{ user.gender }}</td> </tr> <tr> <th class="active text-right" width="120">Age</th> <td>{{ user.age }}</td> </tr> </table> <button class="btn" type="button" routerLink="/users">Back 1</button> <button class="btn btn-info" type="button" (click)="gotoUsers()">Back 2</button> <button class="btn btn-warning" type="button" (click)="gotoUsers2()">Back 3</button>
ไฟล์นี้เราเปลี่ยนโค้ดนิดหน่อยที่บรรทัดที่ 3 จาก *ngFor เป็น *ngIf เนื่องจากข้อมูลที่เราจะส่งมา
ในตัวแปร user ไม่ได้เป็นแบบ Array แล้ว เป็น User Object ปกติ
คำสั่ง *ngIf ข้างต้น การทำงานก็คือ ถ้าตัวแปร user มีข้อมูล ตารางรายละเอียดข้อมูลของ user
ก็จะแสดง
ต่อไปก็ส่วนของไฟล์จัดการข้อมูล user-detail.component.ts
ไฟล์ user-detail.component.ts
import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { User } from './user'; import { UserService } from './user.service'; @Component({ //selector: 'app-user-detail', templateUrl: './user-detail.component.html', styleUrls: ['./user-detail.component.css'] }) export class UserDetailComponent implements OnInit { public user:User; // เดิมใช้เป็นแบบ array - public users:User[]; private id:number|string; constructor( private userService:UserService, private route: ActivatedRoute, private router: Router ) { } ngOnInit() { this.route.data .subscribe((data: { user: User }) => { this.id = data.user.id; this.user = data.user; }); } gotoUsers() { this.router.navigate(['/users']); } gotoUsers2(){ this.router.navigate(['/users',{id:this.id,more:'test'}]); } }
บรรทัดที่ 3 เรา import Observable เข้ามาใช้งาน เนื่องจากเราจะมีการ ดำเนินการกับ Observable
โดยให้รอรับค่าหลังจากคำสั่ง subscribe() ในบรรทัดที่ 27
บรรทัดที่ 16 เราเปลี่ยนรูปแบบข้อมูลที่แสดงเป็น User Object ธรรมดา แทนรูปแบบเดิมที่เป็น array
บรรทัดที่ 26 - 30 เป็นการจัดการในส่วนของข้อมูล Observable เนื่องจาก ActivatedRoute ที่อ้างอิงการใช้งาน
ผ่านตัวแปร route เป็น Observable โดยข้อมูลจะถูกส่งผ่าน route.data และรอรับค่าการดำเนินการ ในคำสั่ง subscribe()
การ subscribe ข้อมูล Observable จะมี Observer หรือผู้สังเกตการณ์ คอยทำหน้าที่จัดการกับข้อมูลของ Observable
ที่ถูกส่งออกมา ผ่าน callback function ที่อาจจะเกิดขึ้น โดย Observer ก็คือ Object หนึ่ง ที่มี 3 callback ประกอบด้วย
next, error และ complete ซึ่ง Observer คอยทำหน้าที่สังเกตข้อมูล Observable และส่งค่าออกมาผ่าน callback function
ที่เป็น next เท่านั้น ส่วน error จะส่งแจ้ง error ออกมา และ complete จะไม่มีการส่งค่าใดๆ ออกมา
ดูตัวอย่าง Observer
var observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; observable.subscribe(observer);
ถ้าเราเอาค่าตัวแปร Observer มาแทนค่าในการ subscribe จะได้เป็น
observable.subscribe({ next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), });
รูปแบบข้างต้นเป็นการใช้งาน arrow function เราลองเปลี่ยนมาเป็นรูปแบบ function ปกติจะได้เป็น
observable.subscribe({ next: function(x){ console.log('Observer got a next value: ' + x) }, error: function(err){ console.error('Observer got an error: ' + err) }, complete: function(){ console.log('Observer got a complete notification') } });
และโดยปกติ เมื่อใช้คำสั่ง เราจะใช้วิธีกำหนด callback เป็น argument แทนการกำหนดเป็น Observer object
observable.subscribe( function(x){ // next }, function(err){ // error }, function(){ // complete } );
ซึ่งโดยมากแล้วจะใช้เฉพาะ ใน next callback ที่มีการส่งข้อมูล Observable ออกมา
เรามาเทียบกับส่วนโค้ดของเรา
this.route.data .subscribe((data: { user: User }) => { this.id = data.user.id; this.user = data.user; });
this.route.data ก็คือ Observable ที่ได้มาเมื่อเราคลิกเปลี่ยน path ของ route ผ่าน ActivatedRoute
x ก็คือ data: { user: User } เป็น object Obsevable ที่จะถูกส่งเข้าใน callback function
อ้างอิงผ่านตัวแปร data
ส่วนของ next callback function จะใช้รูปแบบ arrow function
(data: { user: User }) => { this.id = data.user.id; this.user = data.user; }
หรือรูปแบบฟังก์ชั่นปกติเป็นดังนี้
function(data: { user: User }){ this.id = data.user.id; this.user = data.user; }
สังเกตว่าในส่วนของฟังก์ชัน ngOnInit() เราจะไม่ได้มีการ เรียกใช้งานฟังก์ชั่นดึงข้อมูล
ของ user โดยตรงเหมือนตัวอย่างในตอนที่ผ่านมา แต่จะใช้วิธี สร้างตัวสังเกตการณ์ คอยรับ
ค่าผ่าน callback function แทน
ต่อไปเรามาดูในส่วนของการจัดการข้อมูล ในไฟล์ user.service.ts
ไฟล์ user.sevice.ts
import { Injectable } from '@angular/core'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import { Observable } from 'rxjs/Observable'; import { User } from './user'; import { USERS } from './mock-users'; @Injectable() export class UserService { constructor() { } getUsers(){ return Observable.of(USERS); // รูปแบบเดิมใช้เป็น return USERS; } getUser(id:number|string){ return this.getUsers() .map(users => users.find(user => user.id === +id)); } }
บรรทัดที่ 2 ,3 และ 4 เป็นการใช้งาน Observable Operataor และ Observable class
บรรทัดที่ 15 เราทำการส่งค่ารายชื่อข้อมูล user ออกเป็นรูปแบบข้อมูล Observable ตามที่เราได้กำหนด
ในหน้าการแสดงไว้แล้วในไฟล์ user-list
บรรทัดที่ 20 เนืองจากค่าที่ได้จากฟังก์ชั่น getUsers() จะเป็น Observable ดังนั้น เราจึงสามารถใช้
operator map() ในการจัดการข้อมูล Observable ได้ โดยใช้หา ข้อมูล user ที่ user.id ตรงกับ
ค่า id ที่ส่งเข้ามาตรวจสอบ พอได้ข้อมูลแล้ว ก็จะ return users นั้นๆ ออกไป
ต่อไปเป็นส่วนสำคัญที่ใช้สำหรับส่งค่าไปยังไฟล์ user-detail.component.ts ในส่วนที่รอรับค่าจาก Observable
คือการใช้งาน resoleving route data โดยการกำหนด route resolver จำทำให้เราสามารถทำคำสั่งดึงข้อมูล
ที่จำเป็นมาใช้ ก่อนที่ route จะถูกใช้งาน ในที่นี้เราต้องใช้เป็น service ดังนั้นให้เราสร้างไฟล์
resolver service ขึ้นมาด้วยคำส่ัง
C:\projects\simplerouter>ng g service /user/user-detail-resolver
จากนั้นกำหนดโค้ดดังนี้
ไฟล์ user-detail-resolver.service.ts
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/take'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { User } from './user'; import { UserService } from './user.service'; @Injectable() export class UserDetailResolverService { constructor( private userService: UserService, private router: Router ) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User> { let id = route.paramMap.get('id'); return this.userService.getUser(id).take(1).map(users => { if (users) { return users; } else { // id not found this.router.navigate(['/users']); return null; } }); } }
ในไฟล์ resolver service นี้เราต้องมี resovle method หรือ ฟังก์ชั่น resolve() จะไม่ขอลงรายละเอียดในฟังก์ชั่น แต่การทำงาน
คือ ทำการเรียก ส่งค่า id เข้าไปเรียกข้อมูล Observable ของ user ที่มี user.id ตรงกับ ค่า id ที่ส่งผ่าน route โดยถ้าพบข้อมูล
ก็ให้ return เป็น users object กลับออกไป แต่ถ้าไม่พบ ให้ เปลี่ยนไป path รายการ user ทั้งหมดแทน
และส่วนสุดท้ายคือการใช้งาน resolver route เราต้องนำมาใช้งานในการกำหนด route ในไฟล์ user-routing.module.ts
โดยปรับไฟล์เล็กน้อยดังนี้
ไฟล์ user-routing.module.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { UserCenterComponent } from './user-center.component'; import { UserListComponent } from './user-list.component'; import { UserDetailComponent } from './user-detail.component'; import { UserDetailResolverService } from './user-detail-resolver.service'; const userRoutes: Routes = [ { path:'users', component:UserCenterComponent, children:[ { path: '', component: UserListComponent, children:[ { path:':id', component:UserDetailComponent, resolve: { user: UserDetailResolverService } } ] // end children } ] // end children } ]; @NgModule({ imports: [RouterModule.forChild(userRoutes)], providers: [ UserDetailResolverService ], exports: [RouterModule] }) export class UserRoutingModule { }
บรรทัดที่ 7 import UserDetailResolverService มาใช้งาน
บรรทัดที่ 21 - 23 เป็นการกำหนด resolver route ให้กับ path ':id' ผ่าน resolve property
โดย key ชื่อ user เป็น ตัวแปรไว้รับค่าที่ได้จาก UserDetailResolverService ค่านี้เราจะเอา
ไปเรียกใช้งานในไฟล์ user-detail.component.ts ในส่วนของข้อมูล
this.route.data .subscribe((data: { user: User }) => { this.id = data.user.id; this.user = data.user; });
ตัวแปร user คือค่าที่เราต้องกำหนดให้ตรงกับที่กำหนดใน resolve property
บรรทัดที่ 33 - 35 เป็นการกำหนด provicers หรือ service class ที่เรามีการใช้งาน
เท่านี้เราก็สามารถแก้ปํญหาการใช้งาน component ซ้ำ หรือการ re-use component ผ่านการกำหนด resolve route
และ การใช้งาน Observable paramMap ใน demo ด้านล่าง จะมีตัวอย่างทั้งสองกรณี demo 1 คือรูปแบบการใช้งาน
ที่ไม่มีปัญหา สามารถแสดง รายละเอียดของ user โดยคลิกเลือกที่รายการ โดยไม่ต้อง back หรือกลับมาหน้าหลัก
ส่วนตัว demo 2 จะเป็นกรณีปัญหาที่เกิดขึ้น คือเราสามารถแสดงรายละเอียด user ได้เพียงครั้งเดียว แต่พอคลิกเลือกดู
รายการ user อื่น โดยไม่ได้กด back มาหน้ารายการหลักก่อน ข้อมูลก็จะไม่แสดง ถึงแม้ route path จะเปลี่ยนไปก็ตาม