第七課 Cloud Firestore資料庫
購物車範例
Create
Read
Update
Delete
Push / Add
List / Object
Update / Set
Remove
Angular擴充函式庫
處理非同步資料流
Observable
http://1.bp.blogspot.com/-sPmt4lsJ-II/U74c1Ij3ASI/AAAAAAAAAGE/5n-QtE42COI/s1600/image1.png
https://yakovfain.files.wordpress.com/2017/08/ch5_producer_observable_subscribers.png
產品列表(資料庫查詢)
新增產品(資料庫新增)
新增產品(資料庫修改)
ng g service products/shared/product
ng g class products/shared/product
ng g component products/item-form
ng g component products/item-list
ng g component products/item-detail
npm install ngx-bootstrap --save
ng new ShoppingCart
cd ShoppingCart
npm install firebase angularfire2 --save
ng g module app-routing --flat --module=app
ng g component home
建立專案
firebase資料庫
routing模組
資料庫存取服務
資料欄位界面
產品清單元件
產品編輯
新增產品
首頁元件
ng g class products/shared/product
export class Product {
name: string;
description: string;
price: number;
timeStamp: number; // 時間標記
images: string[];
// 設定時間標記,時間標記做為排序依據
setTimeStamp(ts):void {
this.timeStamp = ts;
}
}
目的:good practice to define data object
product.ts
ng g service products/shared/product
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Product } from './product';
@Injectable()
/*
CRUD
1. 讀取所有項目 (a list of items)
2. 讀取單一項目
3. 新增一個新項目
4. 修改某一個現有項目
5. 刪除某一個項目
6. 刪除所有項目
*/
export class ProductService {
private productsPath: string = '/products';
private itemDoc: any;
items: Observable<any[]>;
item: Observable<Product>;
constructor(private afs: AngularFirestore) { }
// 新增一個新項目
createItem(item: Product): void {
const timeStamp = Date.now(); // 紀錄時間
item.setTimeStamp(timeStamp); // 時間做為主鍵
this.afs.collection(this.productsPath).add(JSON.parse(JSON.stringify(item)));
}
// 讀取所有項目:最多10筆
getItems(): Observable<any[]> {
this.items = this.afs.collection(this.productsPath,
ref => ref.orderBy('timeStamp').limit(10)
).valueChanges();
return this.items;
}
// 修改指定項目
updateItem(item: Product) {
this.itemDoc = this.afs.collection(this.productsPath,
ref => ref.where('timeStamp',"==", item.timeStamp).limit(1));
this.itemDoc.update(item);
}
}
product.service.ts
ng g module app-routing --flat --module=app
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ItemFormComponent } from './products/item-form/item-form.component';
import { ItemListComponent } from './products/item-list/item-list.component';
import { ItemDetailComponent } from './products/item-detail/item-detail.component';
const routes: Routes = [
{path: '', component: HomeComponent},
{path: 'addform', component: ItemFormComponent},
{path: 'products', component: ItemListComponent},
{path: 'products/:pid', component: ItemDetailComponent }
];
@NgModule({
imports:[
RouterModule.forRoot(routes)
],
exports:[
RouterModule
]
})
export class AppRoutingModule {}
app-routing.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ModalModule } from 'ngx-bootstrap/modal';
import { AppComponent } from './app.component';
import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './/app-routing.module';
import { HomeComponent } from './home/home.component';
import { HeaderComponent } from './header/header.component';
import { ItemFormComponent } from './products/item-form/item-form.component';
import { ItemListComponent } from './products/item-list/item-list.component';
import { ItemDetailComponent } from './products/item-detail/item-detail.component';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
HeaderComponent,
ItemFormComponent,
ItemListComponent,
ItemDetailComponent
],
imports: [
BrowserModule,
FormsModule,
ModalModule.forRoot(),
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFirestoreModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
app.module.ts
注意其更新狀態:安裝外掛,新增模組,新增元件...
ng g component products/item-form
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Product } from "../shared/product";
import { ProductService } from "../shared/product.service";
import { Router } from '@angular/router';
@Component({
selector: 'app-item-form',
templateUrl: './item-form.component.html',
styleUrls: ['./item-form.component.css'],
providers: [ ProductService ]
})
export class ItemFormComponent implements OnInit {
item: Product = new Product();
constructor(private ps: ProductService, private router: Router) { }
ngOnInit() {
}
createItem(){
this.ps.createItem(this.item);
this.item = new Product();
this.router.navigate(['products']); // 轉至頁面
}
goBack(){
this.router.navigate(['products']);
}
}
item-form.component.ts
<div class="container">
<div class="form-group">
<label class="col-form-label">產品名稱</label>
<input placeholder="產品名稱" class="form-control" [(ngModel)]="item.name" required #name="ngModel" autofocus>
</div>
<div class="form-group">
<label class="col-form-label">產品介紹</label>
<textarea placeholder="介紹文字" class="form-control" [(ngModel)]="item.description" rows="4" required #description="ngModel"></textarea>
</div>
<div class="form-inline">
<label class="col-form-label">價格</label>
<input placeholder="價格" class="form-control ml-3" [(ngModel)]="item.price" required type="number" #price="ngModel">
</div>
<div *ngIf="name.dirty">
<span *ngIf='name.errors; then errors'></span>
</div>
<div *ngIf="description.dirty">
<span *ngIf='description.errors; then errors'></span>
</div>
<div *ngIf="price.dirty">
<span *ngIf='price.errors; then errors'></span>
</div>
<button class="btn btn-primary my-5" (click)='createItem()' [disabled]="!name.valid || !description.valid || !price.valid">新增</button>
<button class="btn btn-success my-5" (click)='goBack()'>返回</button>
</div>
<ng-template #errors>
<p class="text-danger">欄位未填!</p>
</ng-template>
item-form.component.html
重點:資料庫、ngModel、表單欄位驗證
ng g component products/item-list
import { Component, OnInit } from '@angular/core';
import { Product } from '../shared/product';
import { ProductService } from '../shared/product.service';
import { ItemDetailComponent } from '../item-detail/item-detail.component';
@Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css'],
providers: [ ProductService ]
})
export class ItemListComponent implements OnInit {
items:Product[];
constructor(private ps: ProductService) { }
ngOnInit() {
this.ps.getItems().subscribe(
resp => {
console.log(resp);
this.items = resp;
}
)
}
}
item-list.component.ts
<div class="container">
<button class="btn btn-success my-5" routerLink='/addform'>新增商品</button>
<table class="table table-striped table-success">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">產品名稱</th>
<th scope="col">產品介紹</th>
<th scope="col">價格</th>
<th scope="col"></th>
</tr>
</thead>
<tr *ngFor="let item of items; let i = index">
<td>{{ i+1 }} </td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.price }}</td>
<td>
<app-item-detail [item]='item'></app-item-detail>
<button><i class="mdi mdi-delete" aria-hidden="true"></i>刪除</button>
</td>
</tr>
</table>
</div>
item-list.component.html
重點:routerLink <app-item-detail>尚未建立、使用mdi
...
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/2.1.19/css/materialdesignicons.min.css">
</head>
<body>
<app-root></app-root>
....
準備工作:必須在index.html加上一行css
<button>
<i class="mdi mdi-delete" aria-hidden="true"></i>
刪除
</button>
使用:在button內加上<i></i>
ng g component products/item-detail
import { Component, OnInit, TemplateRef, Input } from '@angular/core';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap';
import { Product } from '../shared/product';
import { ProductService } from '../shared/product.service';
@Component({
selector: 'app-item-detail',
templateUrl: './item-detail.component.html',
styleUrls: ['./item-detail.component.css'],
providers: [ ProductService ]
})
export class ItemDetailComponent implements OnInit {
@Input() item: Product;
public modalRef: BsModalRef;
constructor(private modalService: BsModalService, private ps: ProductService) { }
public openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template);
}
updateItem(){
this.modalRef.hide();
}
ngOnInit() {
}
}
item-detail.component.ts
<button type="button" class="btn btn-primary" (click)="openModal(template)">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
編輯
</button>
<ng-template #template>
<div class="modal-header">
<h4 class="modal-title pull-left">{{item.name}}</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="col-form-label">產品名稱</label>
<input placeholder="產品名稱" class="form-control" [(ngModel)]="item.name" required #name="ngModel" autofocus>
</div>
<div class="form-group">
<label class="col-form-label">產品介紹</label>
<textarea placeholder="介紹文字" class="form-control" [(ngModel)]="item.description" rows="4" required #description="ngModel"></textarea>
</div>
<div class="form-inline">
<label class="col-form-label">價格</label>
<input placeholder="價格" class="form-control ml-3" [(ngModel)]="item.price" required type="number" #price="ngModel">
</div>
<div *ngIf="name.dirty">
<span *ngIf='name.errors; then errors'></span>
</div>
<div *ngIf="description.dirty">
<span *ngIf='description.errors; then errors'></span>
</div>
<div *ngIf="price.dirty">
<span *ngIf='price.errors; then errors'></span>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary my-5" (click)='updateItem()' [disabled]="!name.valid || !description.valid || !price.valid">修改</button>
<button type="button" class="btn btn-default" (click)="modalRef.hide()">取消</button>
</div>
</ng-template>
item-detail.component.html
重點:modal(彈出視窗)、修改資料庫內容、表單與新增功能雷同
修改item-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Product } from '../shared/product';
import { ProductService } from '../shared/product.service';
import { ItemDetailComponent } from '../item-detail/item-detail.component';
@Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css'],
providers: [ ProductService ]
})
export class ItemListComponent implements OnInit {
items:Product[];
constructor(private ps: ProductService) { }
ngOnInit() {
this.ps.getItems().subscribe(
resp => {
console.log(resp);
//this.items = resp;
}
);
this.ps.getItemsWithKey().subscribe(
resp => {
console.log( resp);
this.items = resp;
}
)
}
deleteItem(id: string) {
console.log('deleted');
this.ps.deleteItem(id);
}
}
item-list.component.ts
修改item-list.component.ts 重點:刪除必須使用id
export class Product {
id: string;
name: string;
description: string;
price: number;
timeStamp: number; // 時間標記
images: string[];
// 設定時間標記,時間標記做為排序依據
setTimeStamp(ts):void {
this.timeStamp = ts;
}
}
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { tap,map } from 'rxjs/operators';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Product } from './product';
@Injectable()
/*
CRUD
1. 讀取所有項目 (a list of items)
2. 讀取單一項目
3. 新增一個新項目
4. 修改某一個現有項目
5. 刪除某一個項目
6. 刪除所有項目
*/
export class ProductService {
private productsPath: string = '/products';
private itemDoc: any;
items: Observable<any[]>;
item: Observable<Product>;
constructor(private afs: AngularFirestore) { }
// 新增一個新項目
createItem(item: Product): void {
const timeStamp = Date.now(); // 紀錄時間
item.setTimeStamp(timeStamp); // 時間做為主鍵
this.afs.collection(this.productsPath).add(JSON.parse(JSON.stringify(item)));
}
// 讀取所有項目:最多10筆
getItems(): Observable<any[]> {
this.items = this.afs.collection(this.productsPath,
ref => ref.orderBy('timeStamp').limit(10)
).valueChanges();
return this.items;
}
getItemsWithKey(): Observable<any[]> {
this.items = this.afs.collection(this.productsPath,
ref => ref.orderBy('timeStamp').limit(10)
).snapshotChanges().map(
actions => {
return actions.map( resp =>{
const data = resp.payload.doc.data() as Product;
const id = resp.payload.doc.id;
return {id, ...data};
}
)
}
);
return this.items;
}
deleteItem(id: string) {
this.itemDoc = this.afs.doc(this.productsPath+'/'+id);
this.itemDoc.delete();
}
// 修改指定項目
updateItem(item: Product) {
this.itemDoc = this.afs.collection(this.productsPath,
ref => ref.where('timeStamp',"==", item.timeStamp).limit(1));
this.itemDoc.update(item);
}
}
product.ts
product.service.ts
<div class="container">
<button class="btn btn-success my-5" routerLink='/addform'>新增商品</button>
<table class="table table-striped table-success">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">產品名稱</th>
<th scope="col">產品介紹</th>
<th scope="col">價格</th>
<th scope="col"></th>
</tr>
</thead>
<tr *ngFor="let item of items; let i = index">
<td>{{ i+1 }} </td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.price }}</td>
<td>
<app-item-detail [item]='item'></app-item-detail>
<button type="button" class="btn btn-danger" (click)="deleteItem(item.id)">
<i class="mdi mdi-delete" aria-hidden="true"></i>
刪除
</button>
</td>
</tr>
</table>
</div>
item-list.component.html