Angular Firebase with Oxygen Not Included part 4
ขณะนี้เรามีรายการ trailer ทั้งฝั่ง Realtime Database และ Cloud Firestore เมื่อเราต้องการแก้ไขแต่ละรายการรวมถึงลบรายการที่ต้องการจะทำได้อย่างไร part นี้มาดูกันครับ
ความเดิมตอนที่แล้ว
จาก 3 parts ก่อนหน้าในเมื่อเราสามารถฝ่าฟันมันมาได้ part นี้เราก็ต้องทำได้!
1 trailer ตอนนี้ประกอบด้วย name และ url ทั้งสองจะต้องแก้ไขได้และรายการ trailer ใดๆจะต้องสามารถลบได้ด้วย
มาสร้างปุ่ม แก้ไข และ ลบ กันก่อนดีกว่า
Edit button and Delete button of Realtime Database Trailers
เริ่มจาก Realtime Database ก่อนครับ
เพิ่ม Edit button
เปิดไฟล์ realtimedb-trailers.component.html
เดิมที 1 trailer หน้าตาจะเป็นแบบนี้
เพื่อให้เกิดความเก๋ไก๋ เราจะวางปุ่มแก้ไขไว้ทางขวาสุด เมื่อต้องการแก้ไข name และ url ก็ให้กดเข้าไป หรือต้องการลบก็ให้กดปุ่มแก้ไขก่อนเช่นกัน
วางปุ่มแก้ไขไว้ขวาสุดแบบนี้
โค้ดที่เปลี่ยนแปลงตรงส่วนนี้จึงมีเท่านี้
...<div class="p-2 d-flex"> <div>{{t.name}}</div> <div class="ml-auto"> <button class="btn btn-warning">edit</button> </div></div>...
จากนั้นใส่ click function ลงไป
<button class="btn btn-warning" (click)="onClickedEdit(t)">edit</button>
หัวใจของ function นี้คือ t ซึ่งหมายถึงออบเจ็กต์ trailer
จากนั้นเปิดไฟล์ realtimedb-trailers.component.ts
เพิ่ม onClickedEdit
onClickedEdit(trailer: Trailer) { console.log(trailer);}
ทดสอบกดปุ่มแก้ไข
:)
ค้นหา key
ลองไปดูที่ฐานข้อมูล สิ่งที่เราต้องการสำหรับการแก้ไขคือ key (หรือก็คือ id) ของ trailer ใดๆซึ่ง Firebase เป็นผู้สร้างให้
คำถามคือ แล้วมันอยู่ที่ไหนในฐานข้อมูล?
แล้วจะนำมาใช้ได้อย่างไร?
ให้ย้อนกลับไปที่แต่ละรายการของ trailer นั้นมีที่มาจากไหน มาจากเมธอดนี้ครับ
this.trailers$ = this.db.list<Trailer>('trailers').valueChanges();
valueChanges ให้ข้อดีคือความเรียบง่าย ทว่ามันไม่มี key มาให้
ต้องเปลี่ยนไปใช้ snapshotChanges แทน
แก้ไขโค้ดส่วนนี้ให้เป็นแบบนี้ครับ
this.trailers$ = this.db.list<Trailer>('trailers').snapshotChanges()
จากนั้นเติมมันด้วย pipe เพื่อจะได้แงะเอา key ออกมา
this.trailers$ = this.db.list<Trailer>('trailers').snapshotChanges().pipe();
ภายใน pipe นี้เราจะใช้ rxjs/operators ที่ชื่อ map ดังนั้น import เข้ามาก่อน
import { map } from 'rxjs/operators';
และ
this.trailers$ = this.db.list<Trailer>('trailers').snapshotChanges().pipe( map(trailers => trailers.map(t => { return { ...t.payload.val(), id: t.key, } })));
กระบวนการข้างต้นคือใช้ map สร้างผลลัพธ์ใหม่ที่ภายหลังเพิ่ม id ให้ด้วย ออบเจ็กต์หรือผลลัพธ์ที่สร้างใหม่นี้ก็คือคลาส Trailer ของเรานั่นแหละครับ
เอาล่ะ ขอให้ไปเพิ่ม id แก่คลาส Trailer ที่ไฟล์ trailer.ts เป็นรูปแบบ option ไปก่อน เนื่องจากมีโค้ดอื่นเรียกใช้ Trailer เหมือนกัน พวกมันเหล่านั้นจะได้ไม่พัง
export class Trailer { id?: string; name: string; url: string;}
? คือ option มีความหมายว่า กำหนดค่าให้หรือไม่ก็ได้
ทดลองกดปุ่มแก้ไขอีกครั้ง นั่นไงได้ id มาแล้ว
มาถึงตรงนี้ได้ break กันนิด โค้ดทั้งหมดต้องไม่พังนะครับ หน้าอื่นๆก็ยังคงต้องใช้งานได้เป็นปกติ
เมื่อเรามี id แล้วก็สามารถนำไปแก้ไขตัว trailer ได้
:)
เรียกหน้าแก้ไขมาแสดง
จำได้ไหม part ที่ผ่านมา เราได้กำหนด path ของ routes ไปว่า
{ path: 'realtimedb/trailers/:id/edit', component: RealtimedbTrailersEditComponent },
ดังนั้นกดปุ่ม onClickedEdit ครั้งใดก็ให้ไปเรียกหน้านี้มาแสดงนะ
เปิดไฟล์ realtimedb-trailers.component.ts
ที่ constructor เพิ่ม Angular Router เข้ามา
import { Router } from '@angular/router';
และ
constructor( private db: AngularFireDatabase, private fb: FormBuilder, private sanitizer: DomSanitizer, private router: Router,) { }
จากนั้นมองไปที่ฟังก์ชัน onClickedEdit จัดโค้ดไปว่า
onClickedEdit(trailer: Trailer) { this.router.navigate([`realtimedb/trailers/${trailer.id}/edit`]);}
หมายเหตุ เครื่องหมายที่ผมใช้คือ ` กับ ` นะครับ สังเกตดีๆ เพราะเครื่องหมายนี้จะทำให้แทรกตัวแปรด้วย ${} ได้
จากนั้นทดสอบกดปุ่มแก้ไข
:)
หน้าแก้ไขและลบ Trailer
และแล้วเราก็มาถึงหน้าแก้ไขและลบ trailer ที่ได้สร้างไว้ล่วงหน้าแล้วเสียที ดีใจใช่ไหมล่ะ! (ส่ายหน้า) องค์ประกอบของหน้านี้มีอะไรบ้าง มาดู
- ต้องมี form
- ภายใน form มีช่อง input สำหรับ name และ url
- ภายใน form มีปุ่มบันทึก (save)
- ด้านล่างของ form ให้มีปุ่มลบ (delete)
- เมื่อลบ trailer สำเร็จต้องกลับไปหน้ารายการทั้งหมด
เปิดไฟล์ realtimedb-trailers-edit.component.html (สังเกตนะครับ ว่าเป็นไฟล์ที่มีชื่อต่อท้าย -edit.component)
<div class="container"> <div class="row p-5"> <form [formGroup]="form" class="col col-sm-6"> <div class="form-group"> <label for="name">name: </label> <input id="name" class="form-control form-control-lg" formControlName="name"> </div> <div class="form-group"> <label for="url">url: </label> <input id="url" class="form-control" required formControlName="url"> </div> <button class="btn btn-primary" (click)="onClickedSave()">save</button> </form> </div> <div class="row float-right"> <button class="btn btn-danger" (click)="onClickedDelete()">delete</button> </div></div>
:)
เปิดไฟล์ realtimedb-trailers-edit.component.ts
เขียนโค้ดประกาศตัวแปร form ฟังก์ชันสำหรับ onClickedSave และ onClickedDelete
ใครจำไม่ได้ให้ย้อนไปดูโค้ด part ก่อนหน้านี้นะ
import { FormGroup, FormBuilder } from '@angular/forms';
และ
...form: FormGroup;constructor( private fb: FormBuilder,) { }ngOnInit(): void { this.form = this.fb.group({ name: [''], url: [''] });}onClickedSave() {}onClickedDelete() {}...
เริ่มต้นด้วยการจับ trailer id ที่อยู่บน url ของ browser address bar มาก่อน
Angular ใช้ ActivatedRoute interface
ที่ constructor นำมันเข้ามา ตั้งชื่อว่า route แล้วกัน
import { ActivatedRoute } from '@angular/router';
และ
constructor( private fb: FormBuilder, private route: ActivatedRoute,) { }
จากนั้นที่ ngOnInit ใช้ pipe จับเอา ParamMap แล้วขอสิ่งนี้ (id) มาจาก url ของ browser address bar อีกที
ngOnInit(): void { this.form = this.fb.group({ name: [''], url: [''] });
this.route.paramMap.pipe();...
จากนั้นประกาศตัวแปร trailerId
ให้ประกาศไว้ถัดจาก form: FormGroup;
แบบนี้
...form: FormGroup;private trailerId: string;...
จากนั้นนำมันไปใช้
this.route.paramMap.pipe( switchMap(params => { this.trailerId = params.get('id'); }));...
จากนั้นนำ db เข้ามา
constructor( private fb: FormBuilder, private route: ActivatedRoute, private db: AngularFireDatabase,) { }
จากนั้นนำไปใช้
this.route.paramMap.pipe( switchMap(params => { this.trailerId = params.get('id'); return this.db.object(`trailers/${this.trailerId}`).valueChanges(); }));...
เห็นอะไรไหมครับ หลังจากได้ id มาเก็บไว้ในตัวแปร trailerId เราก็นำมันไปค้นหาในฐานข้อมูลต่อเลย สิ่งที่เราได้ก็คือ Observable
ยังไม่จบนะ ถัดจากนั้นเราจะหยุดฟัง observale นั้นแล้วก็ให้ subscribe นำผลลัพธ์ที่ได้ไปกำหนดให้กับ form
โค้ดทั้งหมดในส่วนนี้คือ
ngOnInit(): void { this.form = this.fb.group({ name: [''], url: [''] }); this.route.paramMap.pipe( switchMap(params => { this.trailerId = params.get('id'); return this.db.object(`trailers/${this.trailerId}`).valueChanges(); }) ).subscribe((trailer: Trailer) => { this.name.setValue(trailer.name); this.url.setValue(trailer.url); });}
และแน่นอนผมว่าต้องมีคนถามหา this.name.setValue กับ this.url.setValue มาได้อย่างไร
ก็เหมือน part ที่ผ่านมานั่นแหละครับ ผมเขียนไว้ก่อนจะปิด } คลาส
get name(): AbstractControl { return this.form.controls.name; }get url(): AbstractControl { return this.form.controls.url; }
ทำเหล่านี้ครบและถูกต้องแล้วก็จะไม่เกิด error ใดๆนะครับ
ลองเล่นดู
:)
แก้ไข Trailer
การแก้ไขหรือการลบสามารถหาอ่านได้ ที่นี่
onClickedSave() { const ref = this.db.object(`trailers/${this.trailerId}`); ref.set({ name: this.name.value, url: this.url.value, });}
ทดลองแก้ไข name
กดปุ่ม save แล้วกลับไปหน้ารายการทั้งหมด
:)
ลบ Trailer
โอ้ง่ายเหลือเกิน
onClickedDelete() { const ref = this.db.object(`trailers/${this.trailerId}`); ref.remove();}
ผมจะลบวีดีโอแรกนะครับ
อ่อ เดี๋ยวสิ ลืมไปว่าลบสำเร็จมันต้องกลับไปหน้ารายการทั้งหมดให้เอง แหมๆ
ที่ constructor เพิ่ม Angular Router
constructor( private fb: FormBuilder, private route: ActivatedRoute, private db: AngularFireDatabase, private router: Router,) { }
กลับไปที่ onClickedDelete ตรงหลังฟังก์ชัน remove เพิ่ม then
ref.remove().then(()=>{ this.router.navigate(['realtimedb/trailers']);});
ผลทดสอบ รายการที่ลบได้หายไปแล้ว
แต่ๆๆ แต่ปรากฏ error แบบนี้
มันพังเพราะว่ามันได้ subscribe url (บน browser address bar) เพื่อขอค่า id ซึ่งเป็นธรรมดาเมื่อมีการเปลี่ยนแปลงใดๆกับ url มันก็จะทำงาน
ให้เพิ่ม filter ลงไป เพื่อกรองเอาเฉพาะกรณีที่ trailer ออบเจ็กต์มีอยู่จริงในฐานข้อมูลเท่านั้น
import { filter } from 'rxjs/operators';
และ
this.route.paramMap.pipe( switchMap(params => { this.trailerId = params.get('id'); return this.db.object(`trailers/${this.trailerId}`).valueChanges();}), filter(value => !!value)).subscribe((trailer: Trailer) => { this.name.setValue(trailer.name); this.url.setValue(trailer.url);});
:)
ลาไปนอนแล้วครับ ไว้เจอกันใหม่