การใช้งาน Route Guard กับการยกเลิกการเปลี่ยนแปลง ใน Angular ตอนที่ 9

เขียนเมื่อ 6 ปีก่อน โดย Ninenik Narkdee
guard angular router candeactivate

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ guard angular router candeactivate

ดูแล้ว 6,001 ครั้ง


สำหรับเนื้อหาต่อไปนี้ เราจะมาดูการใช้งาน route guard ในส่วนของการใช้งาน CanDeactivate สำหรับจัดการ
กับการเปลี่ยนแปลงที่ไม่มีการบันทึกหรือ ต้องการยืนยันการบันทึกก่อนออกจากหน้าดังกล่าว เป็นเนื้อหาต่อจาก
ตอนที่แล้วเกี่ยวกับการใช้งาน route guard ทบทวนได้ที่
    ใช้งาน Route Guard กำหนด Authorized ให้ Route ใน Angular ตอนที่ 8 http://niik.in/848 
 
โดยทั่วไปแล้ว ก่อนออกจากหน้าแก้ไขข้อมูล เมื่อเรามีการเปลี่ยนแปลงข้อมูลในฟอร์มใดๆ แล้วเกิดเปลี่ยนใจ ไม่บันทึก
ข้อมูลนั้น และต้องการออกไปยังหน้าอื่น จะพบว่าบางโปรแกรม หรือบาง App มีการแจ้งเพื่อให้ผู้ใช้ยืนยันก่อนที่จะละทิ้ง
ข้อมูลที่มีการแก้ไข ไปยังหน้าอื่น ทั้งนี้ก็เพื่อป้องกันการออกจากหน้านั้นๆ โดยไม่ได้ตั้งใจ ทำให้ข้อมูลที่กรอกหรือที่
ต้องการเปลี่ยนแปลงหายไป ต้องมาทำการกรอกข้อมูลใหม่ หรือเพื่อยืนยันให้ผู้ใช้บันทึกข้อมูลนั้นๆ ก่อนในกรณีผู้ใช้ลืม
ที่จะกดปุ่มบันทึกข้อมูล แบบนี้เป็นต้น
    ลักษณะการแจ้งเตือนส่วนใหญ่ก็จะเป็นการแสดงการแจ้ง confirm เพื่อเป็นตัวเลือกการดำเนินการ สำหรับการอ้างอิง
คำสั่ง confirm ใน Angular นั้นเราต้องอ้างอิงผ่านตัวแปร global ชื่อ window ดังนั้นก่อนอื่น เราจะมาสร้าง service สำหรับ
กำหนดการแสดงแจ้ง confirm ให้เราสร้าง dialog service สำหรับใช้งานใน app โดยจะให้คืนค่าเป็น Observable ที่มี
ค่าเป็น true หรือ false โดยเราจะสร้าง service นี้ไว้ใน root ของ app และ inject เข้ามาใช้งานผ่าน AppModule
    สร้างไฟล์ dialog.service.ts ด้วยคำสั่ง
 
C:\projects\simplerouter>ng g service dialog
 
จากนั้น กำหนดคำสั่งสำหรับการทำงานการแจ้ง confirm ดังนี้
 
ไฟล์ dialog.service.ts
 
import { Injectable } from '@angular/core';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class DialogService {

  constructor() { }
  
  // สร้างฟังก์ชั่นทำคำสั่งแจ้ง confirm โดยรับค่า message เพื่อระบุข้อความที่ต้องการแจ้ง
  confirm(message?: string): Observable<boolean> {
    const confirmation = window.confirm(message || 'Is it OK?');
    return Observable.of(confirmation);
  };  

}

 
สังเกตว่า message ที่เป็น argument ของฟังก์ชั่น confirm() มีการต่อท้ายด้วยเครื่องหมายคำถาม นั้นหมายถึงว่า 
จะมีหรือไม่มี ค่านี้ส่งเข้ามาก็ได้ โดยถ้ามีค่าส่งเข้ามา ก็จะใช้ค่าที่ส่งเข้ามา แสดงใน confirm dialog แต่ถ้าไม่มี 
ก็ใช้ค่าของ ข้อความว่า 'Is it OK?' แสดงแทน
    สำหรับ confirm dialog ของ browser นั้นเราจะเรียกผ่านคำสั่ง window.confirm() คืนค่าไปยังตัวแปร confirmation
โดยค่าที่ได้เป็น true ถ้าเลือก ok หรือ false ถ้าเลือก cancel จากนั้นเราจะคืนค่าตัวแปร confirmation ผ่านการใช้งาน
คำสั่ง Observable.of() เพื่อส่งออกค่าที่เป็น Observable เพราะฟังก์ชั่น confirm() ใน DialogService ที่เราสร้าง
เรากำหนดให้คืนค่าเป็น Observable<boolean>
    เนื่องจากเราจะใช้งาน dialog service นี้ภายใน app จากส่วนไหนก็ได้ ให้เรา import DialogService เข้ามาในไฟล์
app.module.ts และกำหนดใน providers เพื่อไว้สำหรับ inject หรือนำไปใช้งานใน component อื่นๆ ต่อไปได้
 
ในการใช้งาน canDeactivate interface ของ route guard นั้นจะแตกต่างจากการใช้งาน canActivate และ canActivateChild
เพราะการใช้งานของสองรูปแบบในตอนที่ผ่านมา จำเป็นจะต้องใช้งานกับ AuthService เพื่อเช็คสถานะการล็อกอินเพื่อจำกัด
สิทธิ์การเข้าถึงหน้า admin แต่กรณีของ canDeactivate นั้น เป็นเพียงการทำคำสั่งก่อนที่จะออกจากหน้าที่ต้องการเท่านั้น
ซึ่งโดยมากจะใช้กับหน้าที่มีการแก้ไขหรือมีฟอร์มกรอกข้อมูล และมีการเปลี่ยนแปลงข้อมูลเกิดขึ้น ดังนั้นเราจะต้องสร้าง
guard service สำหรับใช้งานกับ canDeactivate ขึ้นมาอีกอัน โดยไว้ใน root ของ app ให้เราสร้าง ด้วยคำสั่งดังนี้
 
C:\projects\simplerouter>ng g service can-detactivate-guard
 
เราจะได้ไฟล์ can-detactivate-guard.service.ts ให้กำหนดโค้ดดังนี้
 
ไฟล์ can-detactivate-guard.service.ts
 
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

// สร้าง interface เพื่อกำหนด CanDeactivate ให้รองรับข้อมูลแบบ Observable
export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDetactivateGuardService implements CanDeactivate<CanComponentDeactivate> {

  constructor() { }

  canDeactivate(component: CanComponentDeactivate) {
    // ถ้า route path ใน component มีการใช้งาน canDeactivate ให้เช็คว่า
    // component นั้นมีคำสั่ง canDeactivate หรือไม่ ก็มีให้คืนค่าการทำคำสั่ง canDeactivate()
    // กรณีอื่นๆ ให้คืนค่า true
    return component.canDeactivate ? component.canDeactivate() : true;
  }  

}
 
 
ต่อไปเราจะมาลองกำหนดและใช้งานดู จะขอสมมติใช้งานกับฟอร์มหน้า login ที่เราได้สร้างไว้ในตอนที่ผ่านมา โดยเสมือน
ว่าเมื่อผู้ใช้คลิกไปเมนู admin ก็จะไปยังฟอร์มล็อกอิน ให้หน้าฟอร์มนี้จำลองเป็นฟอร์มกรอกข้อมูลใดๆ เมื่อมีการคลิกลิ้งค์
ไปหน้าอื่น หรือทำการล็อกอินแล้วมีการลิ้งค์ไปหน้า admin ก็คือการเปลี่ยนหน้าเพจหรือการออกจากหน้าที่เป็นหน้าฟอร์ม
ข้อมูล (สมมติเท่านั้น ในความเป็นจริงเราจะใช้กับหน้าฟอร์มแก้ไขข้อมูลมากกว่า) ก็จะทำการขึ้นแจ้งเตือนว่าต้องการออก
จากหน้านั้น โดยยกเลิกการเปลี่ยนแปลงค่าใดๆ ในฟอร์มหรือไม่ ถ้าตอบ Cancel ก็คือให้อยู่หน้านั้นต่อ แต่ถ้าตอบ Ok ก็จะไป
ยังหน้าที่คลิกเลือกหรือหน้าที่มีการลิ้งค์ไป ลักษณะการทำงานประมาณนี้
 
ให้เราทำต่อไป ดังนี้ คือให้ import CanDetactivateGuardService เข้ามาในไฟล์ routing หลัก app-routing.module.ts
ทั้งนี้ เพื่อ ให้สามารถ inject service นี้เข้าไปในระหว่างขั้นตอนการเปลี่ยนเส้นทางหรือเปลี่ยนหน้าเพจไปหน้าต่างๆ ดังนี้
 
ไฟล์ app-routing.module.ts 
 
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
 
import { HomeComponent } from './home/home.component';
import { PagenofoundComponent } from './pagenofound/pagenofound.component';
import { ContactComponent } from './contact/contact.component';

import { CanDetactivateGuardService } from './can-detactivate-guard.service';
 
const appRoutes: Routes = [
  {
    path: 'contact',
    component: ContactComponent,
    outlet: 'popup'
  },  
  { path: 'home', component: HomeComponent },
  { path: '',
    redirectTo: '/home',
    pathMatch: 'full'
  },
  { path: '**', component: PagenofoundComponent }
];
 
@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: false } // <-- debugging purposes only set true
    )
  ], 
  exports: [
    RouterModule
  ],
  providers:[
    CanDetactivateGuardService
  ]
})
export class AppRoutingModule { }
 
จากนั้นเรียกใช้งานและกำหนดค่าในไฟล์ login-routing.module.ts โดยกำหนดค่าลงไปในส่วนของ canDeactivate
property ให้กับ path /login ที่เราจะใช้งาน
 
ไฟล์ login-routing.module.ts 
 
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login.component';
import { AuthService } from '../auth.service';

import { CanDetactivateGuardService } from '../can-detactivate-guard.service';

// กำหนด route path ให้กับ login component
const loginRoutes: Routes = [
  {
    path:'login',
    component:LoginComponent,
    canDeactivate: [CanDetactivateGuardService],
  }
];

@NgModule({
  imports: [RouterModule.forChild(loginRoutes)],
  exports: [RouterModule],
  providers:[ // กำนหด providers
    AuthService // เนื่งอจากเราจะมีการใช้งาน AuthService 
  ]
})
export class LoginRoutingModule { }
 
และสุดท้ายแก้ไขไฟล์ component ที่มีการใช้งาน canDeactivate ซึ่งก็คือไฟล์ login.component.ts
โดยให้เพิ่มโค้ดตามบรรทัดที่ highlight ไว้
 
ไฟล์ login.component.ts 
 
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs/Observable';

// import AuthService สำหรับตรวจสอบสถานะการล็อกอินมาใช้งาน
import { AuthService } from '../auth.service';
import { DialogService } from '../dialog.service';

@Component({
  //selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  public loginMessage:string; // กำหนดตัวแปรสำหรับเก็บข้อความบอกสถานะการทำงาน

  // injext AuthService และ Router มาใช้งาน
  constructor(
    private authService:AuthService,
    private router:Router,
    private dialog:DialogService ) { }

  // ฟังก์ชั่นนี้ เป้นคนละตัวกับคำสั่ง login() ใน AuthService ตัวนีจะทำงานเมื่อผู้ใช้คลิกที่ปุ่ม submit
  // เป็นฟังก์ชั่นที่ทำงานในหน้าล็อกอิน เมื่อสมมติผู้ใช้กรอกช้อมูลเรียบร้อยแล้ว และกดปุ่มล็อกอิน
  login(){
    
    // แสดงข้อความผ่านตัวแปร dddd ว่ากำลังพยายามล็อกอิน เนื่องจากเราใช้วิธีการ delay สถานะการล็อกอิน
    // ในไฟล์ auth.service.ts ข้อความนี้จึงแสดงอยู่ภายใน 3 วินาที
    this.loginMessage = 'Trying to log in ...';
    
    // เนื่องจากค่า true ที่เรา delay เป็น Observable เราต้องทำการ subscribe 
    // รอค่าที่ Observable จะส่งออกมา
    this.authService.login().subscribe(() => {
      
      // เมื่อมีการส่งค่ากลับมา และเป็น true แสดงว่ามีการล็ฮกอินแล้ว
      if (this.authService.isLoggedIn) {
        
        // ดึงค่า url ที่พยายามเข้าใช้งานก่อนล็อกอิน มากำหนดหน้าที่จะลิ้งค์ไป
        // ถ้าไม่มีค่ากำหนดเป็นค่าหน้าหลักที่ต้องการ ในที่นี้กำหนดว่า ถ้าไม่มีค่าให้ไปที่ /admin
        let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/admin';

        // ลิ้งค์ไปยังหน้าที่กำหนด
        this.router.navigate([redirect]);
      }   
    }); 
  }

  // สร้างฟังก์ช่ัน canDeactivate คืนค่าเป็น Observable 
  canDeactivate(): Observable<boolean> | boolean {
    // ถามผู้ใช้ผ่าน dialog service และคืนค่า true หรือ false 
    // จากการตัดสินใจของผู้ใช้งาน
    return this.dialog.confirm('ยกเลิกการเปลี่ยนแปลง แล้วออกจากหน้านี้?');
  }

  // ฟังก์ชั่นนี้สร้างให้กับปุ่ม cancel แต่ในที่นี้ไม่ได้ทำอะไรพิเศษ
  cancel(){
    console.log('Cancel');
  }

  ngOnInit() {

  }

}
 
 
บรรทัดที่ 3 import Observable มาใช้งานเนื่องจากเรามีการใช้ข้อมูลแบบ Observable
ที่ได้จากฟังก์ชั่น และการคืนค่าจากการทำงานของฟังก์ชั่นตามที่ได้กำหนดไว้
บรรทัดที่ 4 import dialog service ที่ใช้สร้าง confirm dialog ให้ผู้ใช้ตัดสินใจเลือก
บรรทัดที่ 22 inject dialog service เข้ามาใช้งานใน component class ผ่านตัวแปร dialog
บรรทัดที่ 50 -54 สร้างฟังก์ชั่นสำหรับเรียกใช้งานเมื่อมีการใช้งาน canDeactivate
 
จะขอลำดับการทำงานของไฟล์ทั้ง 3 คือไฟล์ can-deactivate-guard.service.ts, login-routing.module.ts
และ login.component.ts ที่สัมพันธ์กัน ดังนี้ 
    เมื่อผู้ใช้เข้ามาจาก path /login ตามรูปแบบการกำหนด route ในไฟล์ login-routing.module.ts
 
ไฟล์ login-routing.module.ts บางส่วน
 
// กำหนด route path ให้กับ login component
const loginRoutes: Routes = [
  {
    path:'login',
    component:LoginComponent,
    canDeactivate: [CanDetactivateGuardService],
  }
];
 
ตัว canDeactivate ซึ่งเปรียบเสมือน callback function ที่ทำงานทันทีที่เข้ามายัง path นี้ โดยทำงาน
ผ่านการเรียกใช้ CanDetactivateGuardService ที่อยู่ในไฟล์ can-deactivate-guard.service.ts เมื่อ
CanDetactivateGuardService ถูกเรียกใช้งาน คำสั่ง canDeactivate ในไฟล์นี้ ก็จะถูกเรียกให้ทำงานทันทีด้วย
 
ไฟล์ can-detactivate-guard.service.ts บางส่วน
 
// สร้าง interface เพื่อกำหนด CanDeactivate ให้รองรับข้อมูลแบบ Observable
export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDetactivateGuardService implements CanDeactivate<CanComponentDeactivate> {

  constructor() { }

  canDeactivate(component: CanComponentDeactivate) {
    // ถ้า route path ใน component มีการใช้งาน canDeactivate ให้เช็คว่า
    // component นั้นมีคำสั่ง canDeactivate หรือไม่ ก็มีให้คืนค่าการทำคำสั่ง canDeactivate()
    // กรณีอื่นๆ ให้คืนค่า true
    return component.canDeactivate ? component.canDeactivate() : true;
  }  

}
 
จะเห็นว่าเราพยายามให้มีการใช้งานข้อมูลเป็นแบบ Observable หรือรูปแบบข้อมูลที่มีการแยกลำดับขั้นตอนของการคืน
ค่าข้อมูลหรือ event ต่างๆ ที่เกิดขึ้นอย่างเป็นลำดับสังเกตเห็นได้ง่าย เป็นข้อมูลที่เกิดขึ้นในทันที หรือเกิดขึ้นในลำดับ
ตามมาภายหลังก็ได้ ด้วยเหตุผลที่ต้องการให้ค่าที่ได้จากคำสั่ง canDeactivate() นี้เป็น Observable จีงสร้าง interface
ขึ้นมาเพื่อกำหนดรูปแบบของค่าที่จะได้จากการ return ซึ่งหากเราไม่กำหนด interface ข้างต้น เราสามารถ นำค่ามา
กำหนดในส่วนของฟังก์ชันเลยก็ได้ โดยตัดส่วนการกำหนด interface ออก จะได้ในรูปแบบดังนี้
 
ไฟล์ login-routing.module.ts บางส่วน ตัดการกำหนด interface
 
@Injectable()
export class CanDetactivateGuardService {
// implements CanDeactivate<CanComponentDeactivate>
  constructor() { }

  canDeactivate(component): Observable<boolean> | Promise<boolean> | boolean {
    // ถ้า route path ใน component มีการใช้งาน canDeactivate ให้เช็คว่า
    // component นั้นมีคำสั่ง canDeactivate หรือไม่ ก็มีให้คืนค่าการทำคำสั่ง canDeactivate()
    // กรณีอื่นๆ ให้คืนค่า true
    return component.canDeactivate ? component.canDeactivate() : true;
  }  

}
 
การทำงานของคำสั่ง canDeactivate() มีการส่ง component เข้าไปในฟังก์ชั่น โดยคำสั่งที่คืนค่าแบบ Observable
ก็เหมือนลักษณะว่า ให้ตรวจจับโดยรอค่าที่ได้จาก การ return ที่เป็น Observable โดยค่าที่ return ก็เป็นการสั่งให้
component ที่ส่งเข้ามา ซึ่งก็คือ LoginComponent ไปเรียกใช้คำสั่ง canDeactivate() ด้วยรูปแบบการเรียกผ่าน
คำสั่ง component.canDeactivate() ดังนั้นเมื่อ LoginComponent เรียกใช้คำสั่ง canDeactivate() ก็คือคำสั่งที่อยู่
ในไฟล์ login.component.ts ที่เราสร้างไว้
 
ไฟล์ login.component.ts บางส่วน
 
  // สร้างฟังก์ช่ัน canDeactivate คืนค่าเป็น Observable 
  canDeactivate(): Observable<boolean> | boolean {
    // ถามผู้ใช้ผ่าน dialog service และคืนค่า true หรือ false 
    // จากการตัดสินใจของผู้ใช้งาน
    return this.dialog.confirm('ยกเลิกการเปลี่ยนแปลง แล้วออกจากหน้านี้?');
  }
 
คำสั่ง canDeactivate() ที่อยูในไฟล์ login.component.ts ก็คืนค่าเป็น Observable เหมือนกัน เป็นค่าที่รอการตัดสินใจ
จากผู้ใช้ เมื่อมีการขึ้น dialog confirm การยกเลิกการเปลี่ยนแปลง ถ้าผู้ใช้เลือก Cancel ก็จะ return false กลับไปยัง
CanDetactivateGuardService ในไฟล์ can-detactivate-guard.service.ts และ CanDetactivateGuardService 
ก็จะคืนค่า false ไปยัง property ที่ชื่อ canDeactivate ที่อยู่ในไฟล์ login-routing.module.ts ที่กำหนด route path
โดยถ้า canDeactivate property นี้เป็น false ก็จะไม่มีการเปลี่ยนหน้า หรือก็คืออยู่หน้าเดิม 
    ในทางตรงข้ามถ้าผู้ใช้เลือก Ok ลำดับการส่งข้อมูลกลับออกมา ก็จะเป็นค่า true ไล่ดำดับการทำงานขึ้นมาเช่นเดียว
กันกับที่อธิบายข้างต้น แต่ในขึ้นสุดท้าย เมื่อ canDeactivate property มีค่าเป็น true ก็หมายถึงว่าผู้ใช้ไม่สนใจการเปลี่ยน
แปลงที่ทำขึ้น และต้องการออกไปหน้าอื่น จึงมีการเปลี่ยนหน้าไปยังหน้าที่ผู้ใช้เลือกหรือลิ้งค์ไป 
 
สามารถดูการทำงานได้ที่ demo 1 คลิกไปที่เมนู admin จากนั้นเปลี่ยนไปคลิกที่เมนู home ก็จะมีการขึ้นแจ้งให้ผู้ใช้
ยืนยัน (ในที่นี้เราไม่ได้ตรวจจับว่ามีการเปลี่ยนแปลงข้อมูลจริงหรือไม่) ว่ายกเลิกการเปลี่ยนแปลงที่เกิดขึ้นหรือไม่
หรือกรณีกดปุ่ม ล็อกอินก็จะขึ้นแจ้งเหมือนกัน ( ซึ่งในการใช้งานจริง เราจะไม่ใช่กับหน้าล็อกอิน )
 
สำหรับเนื้อหาในตอนหน้า เราจะมาดูต่อเกี่ยวกับ route guard ในการทำทวนการใช้งาน resolve และการใช้งาน 
CanLoad Guard รายละเอียดจะเป็นยังไง รอติดตาม
 




กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง











เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ