網路程式設計

第七課 Cloud Firestore資料庫

購物車範例

Outline

  • NoSQL
  • 關於RxJS 之 Observable
  • 購物車範例

SQL CRUD

Create

Read

Update

Delete

NoSQL ?

Push / Add

List / Object

Update / Set

Remove

RxJS

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

購物車範例

  • Firebase資料庫設定
  • Bootstrap準備工作
  • 購物車範例

購物車範例後台部份

產品列表(資料庫查詢)

購物車範例後台部份

新增產品(資料庫新增)

購物車範例後台部份

新增產品(資料庫修改)

購物車範例

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

app.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

注意其更新狀態:安裝外掛,新增模組,新增元件...

產品新增Create

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

產品新增Create

<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、表單欄位驗證

產品列表Read

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

產品列表Read

<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

Material Design Icons

...
  <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>

編輯產品Update

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

編輯產品Update

<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">&times;</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(彈出視窗)、修改資料庫內容、表單與新增功能雷同

刪除產品Delete

修改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

刪除產品Delete

修改item-list.component.ts 重點:刪除必須使用id

  • 改為訂閱getItemsWithKey()
  • 連帶修改product.ts, product.sevice.ts
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

刪除產品Delete

<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

期末上機考練習

  • 設計會員資料表:參考product.ts
  • 設計資料庫新增、查詢、刪除、修改功能:參考product.service.ts
  • 使用資料庫功能:參考item-home, item-list, item-detail

網路程式設計

By Leuo-Hong Wang

網路程式設計

第七課:購物車範例

  • 1,162