มาเขียน Angular เชื่อมต่อกับ Firebase กันเถอะ CRUD #2 (ตอนที่ 2)

Phatcharaphan Ananpreechakun
Angular in Thailand
8 min readSep 9, 2017

ก่อนจะเข้ามาอ่านบทความนี้อย่าลืม เข้าไปอ่าน “มาเขียน Angular เชื่อมต่อกับ Firebase กันเถอะ CRUD #1 ตอนที่ 1” กันก่อนน๊า เดี๋ยวจะไม่เข้าใจเข้าไปอ่านได้ที่นี่เลย

หลังจากที่เราได้เพิ่มข้อมูล และนำข้อมูลที่ได้จาก Firebase มาแสดงแล้ว ต่อไปเรามาทำการลบข้อมูล และแก้ไขข้อมูลกัน เมื่อทำทั้งสองอย่างเสร็จแล้ว ก็มาลอง Refactor code กันนิดๆๆหน่อยๆเนอะ ที่จริงการ Refactor code แบบไม่ได้เขียน Unit test ไว้นี่มีความเสี่ยงน่ะ เราอาจจะทดสอบได้ไม่หมดทั้งระบบ เพราะว่าการ Refactor code นั้นทำเพื่อเปลี่ยนแปลงโครงสร้างของ Code แต่ว่าพฤติกรรมการทำงานของระบบก็ยังคงเหมือนเดิมน่ะจ๊ะ แต่นี่ระบบเรายังมีอยู่ไม่กี่หน้าจอและเราอยากจะมาลอง Refactor ส่วนไหนที่เป็นการดึงข้อมูล หรือว่า action ที่มาจาก Firebase ให้เขียนเป็น Service กัน

มี Requirement บอกไว้ว่าให้สามารถลบข้อมูลจากหน้าจอที่แสดงข้อมูลได้ ให้เพิ่มช่องสำหรับใส่ปุ่มเพื่อลบข้อมูลดังภาพ

  • เมื่อเราได้รับ Requirement สำหรับลบข้อมูลก็ไปทำกันเลย
  • เริ่มจากทำเพิ่มหน้าจอสำหรับกดปุ่มลบข้อมูลกันก่อนเลยที่ไฟล์ home.component.html
  • Code ที่เป็นตัวหนาสีเข้มๆๆน๊า ที่เป็น Code ที่เพิ่มเมื่อกี้ ซึ่งดูจาก Code แล้วเราเพิ่มอีก 1 column สำหรับ Action เพื่อให้มีปุ่มสำหรับเพิ่มข้อมูล และใน tag button นั้นจะมี method สำหรับส่งข้อมูลให้มี Action click สำหรับลบข้อมูลและนำข้อมูลที่ต้องการไปลบ
<a routerLink="/addWiki">Add wiki</a><table class='table'><thead><tr><td>Action</td><td>Name</td><td>Description</td></tr></thead><tbody><tr *ngFor="let data of wikis"><td><button type="button" (click)="delWiki(data)" >Delete</button></td><td>{{data.value.name}}</td><td>{{data.value.description}}</td></tr></tbody></table>
  • มาดูหน้าจอกันหน้าตาจะออกมาเป็นแบบไหน
  • เมื่อเราทำหน้าจอเสร็จแล้ว เรามาสร้าง Method สำหรับลบข้อมูลกันในไฟล์ home.component.ts
import { Component, OnInit } from '@angular/core';import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';@Component({selector: 'app-home',templateUrl: './home.component.html',styleUrls: ['./home.component.css']})export class HomeComponent implements OnInit {wikiList: AngularFireList<any>;wikis: any[];constructor(db: AngularFireDatabase) {this.wikiList = db.list('wikis');}ngOnInit() {this.wikiList.snapshotChanges().map(actions => {return actions.map(action => ({ key: action.key, value: action.payload.val() }));}).subscribe(items => {this.wikis = items;});}delWiki(data) {console.log(data);}}
  • ลองเข้าไปดูที่หน้าจอกัน เมื่อเรา Click ที่ข้อมูลที่เราเลือกจะเห็นข้อมูลใน Console ซึ่งมีทั้ง Key ของข้อมูลที่เราเลือกและข้อมูลอื่นๆ
  • เมื่อเรามีข้อมูลพร้อมแล้ว เราก็มาเขียนลบข้อมูลกันเลย ตัวหนาๆน่ะจ๊ะ Code ที่เพิ่มเข้ามาใหม่
  • ใน Library ของ AngularFireList จะมี Method remove ไว้สำหรับลบข้อมูล โดยจะส่ง Key ที่บ่งบอกว่าเป็นข้อมูลของแถวที่เราเลือกนั่นก็คือ key ซึ่งจะเป็น key ของแต่ละข้อมูลที่เราเลือก
import { Component, OnInit } from '@angular/core';
import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
wikiList: AngularFireList<any>;
wikis: any[];
constructor(db: AngularFireDatabase) {
this.wikiList = db.list('wikis');
}
ngOnInit() {
this.wikiList.snapshotChanges().map(actions => {
return actions.map(action => ({ key: action.key, value: action.payload.val() }));
}).subscribe(items => {
this.wikis = items;
});
}
delWiki(data) {
this.wikiList.remove(data.key);
}
}
  • เมื่อเราเขียน Code เสร็จแล้ว เราก็มาทดสอบที่หน้าจอกันเลย เมื่อลองเอาทั้งสองหน้าจอมาดูทั้งหน้าจอที่เราทำหน้าจอสำหรับลบข้อมูลกับหน้าของ Firebase นั้น เมื่อเราลบข้อมูลจะเห็นว่า ทาง Firebase ก็จะลบข้อมูลแบบ Realtime เลย สังเกตจาก แดงๆทางฝั่ง Firebase กำลังลบข้อมูลเลยหลังจากที่ทำการกดปุ่มลบข้อมูล
  • เมื่อเสร็จมีการลบข้อมูลแล้ว ต่อไปเราไปทำการแก้ไขข้อมูลกันดีกว่า
  • อย่างแรกเลยเราใส่ Action ปุ่มสำหรับแก้ไขข้อมูลกันเลยในไฟล์ home.component.html
<a routerLink="/addWiki">Add wiki</a><table class='table'><thead><tr><td>Action</td><td>Name</td><td>Description</td></tr></thead><tbody><tr *ngFor="let data of wikis"><td><button type="button" (click)="editWiki(data)" >Edit</button><button type="button" (click)="delWiki(data)" >Delete</button></td><td>{{data.value.name}}</td><td>{{data.value.description}}</td></tr></tbody></table>
  • จะเห็นว่าหน้าจอของเราจะมีปุ่มให้สามารถแก้ไขข้อมูลของแต่ละแถวได้ดังภาพ
  • เมื่อเรามีปุ่มสำหรับแก้ไขข้อมูลแล้ว เราก็มาสร้าง method สำหรับแก้ไขข้อมูลกันในไฟล์ home.component.ts
  • ซึ่งใน Method ที่เราสร้างขึ้นมานั้น จะมีการส่งไปยังอีก path /editWiki/ตามด้วย Key ของแถวที่เราเลือก ซึ่งใน router ของ angular จะมีให้สามารถใช้งาน link ไปยัง path ที่เราต้องการซึ่งก็อย่าลืม import เข้ามายัง class ของเราด้วยน่ะจ๊ะ
  • ในส่วนของ [`/editWiki/${data.key}`] /editWiki เป็น path ที่เราจะเข้าไปหน้าที่สามารถแก้ไขข้อมูลและทำการส่ง Key เข้าไปด้วย เมื่อก่อน ถ้าเราจะต่อข้อความใน javascript เราจะเขียนด้วย + เพื่อต่อความ ในนี้เราจะใช้ ES2015 Template String เพื่อทำการต่อข้อความ โดยใช้ `` ครอบสิ่งที่เราอยากจะต่อข้อความและสิ่งที่จะส่งเข้ามาต่อจะต้องใช้ ${} เพื่อแทรกส่วนของ javascript เข้าไปในข้อความ
import { Component, OnInit } from '@angular/core';import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';import { Router } from '@angular/router';@Component({selector: 'app-home',templateUrl: './home.component.html',styleUrls: ['./home.component.css']})export class HomeComponent implements OnInit {wikiList: AngularFireList<any>;wikis: any[];constructor(db: AngularFireDatabase, private router: Router) {this.wikiList = db.list('wikis');}ngOnInit() {this.wikiList.snapshotChanges().map(actions => {return actions.map(action => ({ key: action.key, value: action.payload.val() }));}).subscribe(items => {this.wikis = items;});}editWiki(data) {this.router.navigate([`/editWiki/${data.key}`]);}delWiki(data) {this.wikiList.remove(data.key);}}
  • เมื่อเรามี Path เพิ่มขึ้นมาอีกหนึ่งตัวแล้ว อย่าลืมเข้าไปเพิ่มใน Routes กันด้วยน่ะจ๊ะ ในไฟล์ app.module.ts
  • ซึ่ง Path /editwiki นั้น เราอยากที่จะใช้ร่วมกับ component ตัวเดิมนั่นก็คือ AddWikiComponent และส่ง id เข้าไปยัง component นี้เมื่อ path เป็น /editWiki
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule} from '@angular/forms';
import { AngularFireModule} from 'angularfire2';
import { firebaseConfig } from './../environments/firebase.config';
import { AngularFireDatabaseModule } from 'angularfire2/database';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{path: 'addWiki', component: AddWikiComponent},
{path: 'editWiki/:id', component: AddWikiComponent},
{path: '', component: HomeComponent},
{path: '**', redirectTo: '/', pathMatch: 'full'}
];
@NgModule({
declarations: [
AppComponent,
AddWikiComponent,
HomeComponent
],
imports: [
BrowserModule,
FormsModule,
AngularFireModule.initializeApp(firebaseConfig),
AngularFireDatabaseModule,
RouterModule.forRoot(routes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
  • เมื่อเรามาดูที่หน้าจอจะเห็นว่ามี title ที่เราต้องแก้ไขให้ตรงกับ path ที่เข้ามาและจะต้องแสดงข้อมูลที่ต้องการแก้ไขใน input text และ textarea ด้วย
  • อะๆๆ เรามาดึงข้อมูล By key จาก Firebase กัน เพื่อให้สามารถนำข้อมูลมาแสดงในหน้าจอได้ มาเขียน Code ในไฟล์ add-wiki.component.ts
  • อย่างแรกเราต้องดึง id มาจาก path /editWiki/-KtRd5_y5rXKc6CeHrKy กันก่อนน๊าา ไม่งั้นจะไม่มี id ในการดึงข้อมูลจาก Firebase น๊าา
  • เราต้อง import ActivatedRoute เพื่อใช้คำสั่งในการดึง id จาก paramMap ตามด้วย get(“parameter”) ที่เรา config routes ในไฟล์ app.component.ts ยังจำกันได้ไหมเอ่ยย ??? editWiki/:id ตรงนี้แหละที่เป็นชื่อ parameter ที่เราจะส่งไปยัง method this.route.snapshot.paramMap.get(“id”)
  • ในหน้านี้เราอยากให้แสดง ข้อความว่า Edit wiki ถ้ามี id มีค่า ถ้าไม่มีค่าก็จะใช้ค่า Default ที่ set ไว้ ชื่อ title
  • เราประกาศตัวแปรชื่อ wiki: any={}; จะมี type เป็น any ซึ่ง type any เนี่ยยมันสามารถที่จะแทนชนิดข้อมูลอะไรก็ได้
  • ใน this.db.object(‘/wikis/’+ id) จะเป็นการดึงข้อมูล Object wikis จาก Firebase และส่ง id เข้าไปเพื่อดึงข้อมูล id นั้นมา และตามด้วย .subscribe ซึ่งจะคล้าย ๆ Promise().then ก็จะรอ response มา เพื่อ set ค่าใส่ในตัวแปร wiki
import { Component, OnInit } from '@angular/core';import { NgForm } from '@angular/forms';import {AngularFireDatabase} from 'angularfire2/database';import {ActivatedRoute} from '@angular/router';@Component({selector: 'app-add-wiki',templateUrl: './add-wiki.component.html',styleUrls: ['./add-wiki.component.css'],})export class AddWikiComponent implements OnInit {wiki: any= {};
title: string = "Add Wiki";
constructor(private db: AngularFireDatabase, private route: ActivatedRoute) { }ngOnInit() {const id = this.route.snapshot.paramMap.get("id");if(id){
this.title = "Edit wiki";
this.getWikiByKey(id);}}addWiki(data: NgForm){ this.db.list("wikis").push(data.value);}getWikiByKey(id){this.wiki = this.db.object('wikis/' + id).snapshotChanges().map(res => { return res.payload.val();});}}
  • เมื่อเราเขียน Code สำหรับดึงข้อมูลมาจาก Firebase เสร็จแล้ว เราก็นำข้อมูลนั้นมาแสดงกันเลยย และนำ title มาแสดงด้วยน๊าาา เพราะว่าเราอยากให้แสดงคำว่า Edit wiki เมื่อมี id ส่งมาให้เราด้วย
  • เราใส่ [(ngModel)] ตามชื่อ wiki ที่เรา set ข้อมูลไว้แล้วตามด้วย key ที่ส่งมาจาก Firebase เมื่อ wiki มีข้อมูลและ key ตรงตามที่เรา set ค่าไว้ข้อมูลก็จะแสดงไปยังสิ่งที่เรา set ไว้
<h1>{{title}}</h1><form (ngSubmit)="addWiki(wikiForm)" #wikiForm="ngForm"><label for="name"> Name</label><input type="text" name="name" ngModel [(ngModel)]="wiki.name"><label for="description"> Description</label><textarea name="description" ngModel [(ngModel)]="wiki.description"></textarea><button type="submit">Submit</button></form><a routerLink="/">Back To Wiki List</a>
  • เมื่อเราเขียน Code เสร็จแล้ว ไปดูผลลัพธ์กันเลยย
  • เย้ๆๆๆ ข้อมูลแสดงใน input text และ textarea แล้วว
  • เมื่อข้อมูลแสดงไปยัง input text และ textarea แล้วเราไปทำการเขียน Code ให้สามารถแก้ไขข้อมูลไปยัง Firebase กันเถอะ ไปที่ไฟล์ add-wiki.component.ts
  • จะเห็นว่า method .update และส่งข้อมูล id , ตามด้วยข้อมูลที่ต้องการแก้ไข เพื่อส่งไปยัง Firebase เพื่อ update ข้อมูล
import { Component, OnInit } from '@angular/core';import { NgForm } from '@angular/forms';import {AngularFireDatabase} from 'angularfire2/database';import {ActivatedRoute} from '@angular/router';@Component({selector: 'app-add-wiki',templateUrl: './add-wiki.component.html',styleUrls: ['./add-wiki.component.css'],})export class AddWikiComponent implements OnInit {wiki: any= {};title: string = "Add Wiki";id;constructor(private db: AngularFireDatabase, private route: ActivatedRoute) { }ngOnInit() {this.id = this.route.snapshot.paramMap.get("id");if(this.id){    this.getWikiByKey(this.id);    this.title = "Edit wiki";}}addWiki(data: NgForm){if(this.id){this.db.list("wikis").update(this.id,data.value);}else{this.db.list("wikis").push(data.value);}}getWikiByKey(id){this.wiki = this.db.object('wikis/' + id).snapshotChanges().map(res => {return res.payload.val();});}}
  • เมื่อเราเขียน Code เสร็จลองไปเล่นที่หน้าเว็บกัน จะเห็นว่าข้อความเปลี่ยนแล้วว
  • แต่ว่าเมื่อเรา แก้ไขข้อมูลหรือว่าเพิ่มข้อมูลสำเร็จอยากให้ไปยังหน้าจอแสดงข้อมูลเลย งั้นเราก็ไปเขียน Code กันนไปที่ไฟล์ add-wiki.component.ts
  • เมื่อรอจน update หรือ insert ข้อมูลสำเร็จก็จะกลับไปยัง path / นั่นก็คือหน้าแสดงข้อมูลของเรา this.router.navigate([‘/’]);
import { Component, OnInit } from '@angular/core';import { NgForm } from '@angular/forms';import {AngularFireDatabase} from 'angularfire2/database';import {ActivatedRoute, Router} from '@angular/router';@Component({selector: 'app-add-wiki',templateUrl: './add-wiki.component.html',styleUrls: ['./add-wiki.component.css'],})export class AddWikiComponent implements OnInit {wiki: any= {};title: string = "Add Wiki";id;constructor(private db: AngularFireDatabase, private route: ActivatedRoute, private router: Router) { }ngOnInit() {this.id = this.route.snapshot.paramMap.get("id");if(this.id){this.getWikiByKey(this.id);this.title = "Edit wiki";}}addWiki(data: NgForm){if(this.id){this.db.list("/wikis").update(this.id,data.value).then(this.goToHome);}else{this.db.list("/wikis").push(data.value).then(this.goToHome);}}getWikiByKey(id){this.wiki = this.db.object('wikis/' + id).snapshotChanges().map(res => {return res.payload.val();});}goToHome=()=>{this.router.navigate(['/']);}}
  • เมื่อเขียน Code เสร็จแล้วก็ลองเล่นกันเลยนะจ๊ะ ก็จะเห็นว่าเมื่อเพิ่มหรือแก้ไขข้อมูลสำเร็จก็จะกลับไปยังหน้าแรก
  • ต่อไปจะเห็นว่า component ของเรามีภาระเยอะแยะเลย ไหนจะดึงข้อมูล ไหนจะเพิ่มข้อมูล และอื่นๆๆจาก Firebase และมี Library ของ Firebase ใช้งานกัน สอง component แล้ว ซึ่งถ้ามี component อื่นอยากใช้อีกเราก็ต้อง import Firebase แล้วก็เขียนแบบเดิมอีก ซึ่งมันก็ทำให้เขียนซ้ำซ้อนอีกแล้วว และอยากให้ component ของเรา Focus เพียงหน้าที่เดียว นั่นก็คือ control สิ่งต่างๆของ template component ของเรา
  • เราเลยจะต้อง Refactor code ให้ของที่มาจาก Firebase ให้เขียนเป็น Service แล้วค่อย import เข้ามายัง component ที่ต้องการเพื่อง่ายต่อการแก้ไขด้วยน่ะจ๊ะ ไม่งั้นเราคงต้องเข้าไปแก้ไขทุกๆๆหน้าถ้าเกิดวันหนึ่งเราอยากจะเปลี่ยน Api สมมุตินะจ๊ะ สมมุติ
  • เรามา Refactor code กันเลย ด้วยกันสร้าง Service กันด้วยคำสั่ง
ng g s service/FirebaseService
  • เข้าไปเขียน Code ใน /service/firebase-service.service.ts ให้มีการดึงข้อมูลและการลบข้อมูลก่อนเลย
import { Injectable } from '@angular/core';import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';import { Observable } from 'rxjs/Observable';@Injectable()export class FirebaseService {wikiList: AngularFireList<any>;constructor(private db: AngularFireDatabase) {this.wikiList = db.list('wikis');}getWikiList(): Observable<any[]> {return this.wikiList.snapshotChanges().map(actions => {return actions.map(action => ({ key: action.key, value: action.payload.val() }));});}removeWiki(id): void {this.wikiList.remove(id);}}
  • เมื่อเราสร้าง service แล้ว อย่าลืมนำ service ที่สร้างใส่ providers ในไฟล์ app.module.ts กันด้วยนะจ๊ะ
  • อย่างแรกเลย import FirebaseService เข้าไปก่อน
import { FirebaseService } from ‘./service/firebase-service.service’;
  • เมื่อ import เสร็จแล้วก็นำ FirebaseService ไปใส่ใน providers
@NgModule({declarations: [AppComponent,AddWikiComponent,HomeComponent],imports: [BrowserModule,FormsModule,AngularFireModule.initializeApp(firebaseConfig),AngularFireDatabaseModule,AppRoutingModule],providers: [FirebaseService],bootstrap: [AppComponent]})
  • เมื่อเรา import เข้า module แล้วต่อไปเราก็สามารถใช้ FirebaseService ได้ทั้งโปรเจคเราแล้วว
  • เรามาเริ่มจากหน้าแสดงข้อมูลกันก่อนเลย home.component.ts ให้เปลี่ยนการดึงข้อมูลและการลบข้อมูลให้มาจาก FirebaseService กัน
import { Component, OnInit } from '@angular/core';import { Router } from '@angular/router';import { FirebaseService } from '../services/firebase-service.service';@Component({selector: 'app-home',templateUrl: './home.component.html',styleUrls: ['./home.component.css']})export class HomeComponent implements OnInit {wikis: any[];constructor(private router: Router, private firebaseService: FirebaseService) {}ngOnInit() {this.firebaseService.getWikiList().subscribe(items => {this.wikis = items;});}editWiki(data) {this.router.navigate([`/editWiki/${data.key}`]);}delWiki(data) {this.firebaseService.removeWiki(data.key);}}
  • เมื่อเสร็จแล้วก็ไปลองทดสอบที่หน้าจอกันก่อนน่ะ ถ้าไม่มี Error ใดๆๆก็มาต่อกัน
  • เรามาต่อกันที่หน้า เพิ่มข้อมูลและแก้ไขข้อมูลกัน
  • เรามาสร้าง method สำหรับ เพิ่มข้อมูล แก้ไขข้อมูล ข้อมูลโดย Key กันที่ไฟล์ firebase-service.service.ts
import { Injectable } from '@angular/core';import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';import { Observable } from 'rxjs/Observable';@Injectable()export class FirebaseService {wikiList: AngularFireList<any>;constructor(private db: AngularFireDatabase) {this.wikiList = db.list('wikis');}getWikiList(): Observable<any[]> {return this.wikiList.snapshotChanges().map(actions => {return actions.map(action => ({ key: action.key, value: action.payload.val() }));});}getWiki(id): Observable<any> {return this.db.object('wikis/' + id).snapshotChanges().map(res => {return res.payload.val();});}removeWiki(id): void {this.wikiList.remove(id);}addWiki(data) {return this.wikiList.push(data);}editWiki(id, data) {return this.wikiList.update(id, data);}}
  • เมื่อมี method แล้วเราก็ไปเปลี่ยนการ เพิ่มข้อมูล แก้ไขข้อมูล ข้อมูลโดย Key ที่ไฟล์ add-wiki.component.ts
import { Component, OnInit } from '@angular/core';import { NgForm } from '@angular/forms';import {ActivatedRoute, Router} from '@angular/router';import {FirebaseService} from '../service/firebase-service.service';@Component({selector: 'app-add-wiki',templateUrl: './add-wiki.component.html',styleUrls: ['./add-wiki.component.css'],})export class AddWikiComponent implements OnInit {wiki: any= {};title: string = "Add Wiki";id;constructor(private firebaseService: FirebaseService, private route: ActivatedRoute, private router: Router) { }ngOnInit() {this.id = this.route.snapshot.paramMap.get("id");if(this.id){this.getWikiByKey(this.id);this.title = "Edit wiki";}}addWiki(data: NgForm){if(this.id){
this.firebaseService.editWiki(this.id,data.value).then(this.goToHome);
}else{ this.firebaseService.addWiki(data.value).then(this.goToHome);}}getWikiByKey(id){this.firebaseService.getWiki(id).subscribe(data =>{ this.wiki = data;});}goToHome=()=>{this.router.navigate(['/']);}}

เสร็จแล้ววว เมื่อเรามาถึงจุดๆๆนี้ เราก็จะพอที่จะไปทำ Feature อื่นๆๆที่เราต้องการกันได้พอสมควรเนอะ

หวังว่าจะมีประโยชน์กับผู้ที่เข้ามาอ่านน๊าาา ถ้าผิดพลาดไหนส่วนไหนบอกได้น๊าาและแก้ไขให้ด้วยน๊าากลัวจะเข้าใจผิด

สามารถดู source code ได้ที่นี่ https://github.com/phatpan/wiki

ชอบเขียนกระทู้เพราะมันจะทำให้เราได้ทบทวนสิ่งที่เราอ่านมาและเมื่อเราลืมก็สามารถกลับมาอ่านได้อีกน๊าาา บ๊ายๆเจอกันกระทู้ต่อไป

--

--