第八課 Firestore資料庫存取(How-to)
❶ 新增專案
❷ 啟用Cloud Firestore資料庫
❸ 新增應用程式 / 取得Firestore連線設定值
選擇 Cloud Firestore
留意安全性規則設定
以鎖定模式啟動
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}以測試模式啟動
任何人都不能讀寫!
任何人都能讀寫!
修改安全性規則
做好資料庫權限控管
測試期採用
複製反白區域
準備貼至Angular專案
❶ 安裝Firebase相關套件
❷ 資料庫連線設定
ng new DatabaseApp
cd DatabaseApp
ng add @angular/fireAngular v. 13.0.0
firebase: v. 9.4.0
@angular/fire: v. 7.2.0
2021/12/14
@angular/fire v. 7.x.x 與Angular v. 12, v.13不完全相容!
ng add @angular/fire 執行成功最後會出現Firebase專案列表上下箭頭選擇專案
正常狀況
套件安裝成功!
準備列出你的Firebse專案列表
Failed...
npm i -g firebase-tools解決方法:
(1) 安裝firebase管理工具,從cmd登入firebase控制台
firebase logout
firebase login # 會跳出Google登入畫面錯誤狀況排除
(2) 安裝好後,先登出,再重新登入
(3) firebase控制台登入成功後,在專案資料夾內再執行一次:
ng add @angular/fire錯誤狀況排除
上下箭頭選擇專案
export const environment = {
production: false,
firebaseConfig: {
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: '專案id.firebaseapp.com',
databaseURL: 'https://專案id.firebaseio.com',
projectId: '專案id',
storageBucket: '專案id.appspot.com',
messagingSenderId: 'XXXXXXXXXXXX'
}
};檢視environments/environment.ts
ng add @angular/fire會自動完成此連線設定
...
import { AngularFireModule } from '@angular/fire/compat';
import { environment } from '../environments/environment';
import { AngularFireAuthModule } from '@angular/fire/compat/auth';
import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
...
imports: [
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
AngularFirestoreModule
],
...app.module.ts(修改後)
...
import { initializeApp,provideFirebaseApp } from '@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth,getAuth } from '@angular/fire/auth';
import { provideFirestore,getFirestore } from '@angular/fire/firestore';
...
imports: [
...
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore())
],
...
app.module.ts(修改前)
重要!請修改
主要模組設定檔
2個集合
prodcuts集合內含6個文件
此文件有4個欄位
1張資料表
6筆紀錄
5個欄位
1. Root Collections
2. Embedded Data
mood欄位值為
兩個欄位的文件
3. Subcollections
除欄位外,文件內也可包含子集合
關聯式資料庫之relationships
1個員工分配1台電腦
1個部門負責很多個專案
每台電腦只屬於某個員工
適用 Embedded Data
每個專案只屬於1個部門
1個員工負責很多個專案
每個專案隸屬於多個員工
可用 Root Collections
Subcollections
或 Embedded Data
經常查詢某部門專案 也
經常條件式查詢所有專案
專案查詢功能不涉及跨部門
Firestore資料模型設計方式
專案資料量少又欄位簡單
或 Root Collections
資料量少又欄位簡單
資料量大
額外加上第三個Collection
包含employeeId, projectId
在各自Collection內加入Embedded Data
在每個員工下重複儲存專案資料
在每個專案下重複儲存員工資料
Subcollections
將上述embedded data改成subcollections
銷售代表資料表
客戶資料表
1對多
1對多
工作項目資料表
1對多
ng g interface model/customer
ng g service service/customer
ng g c customer/customer-list
ng g c customer/customer-detail
ng g c customer/customer-formng new MyCRM
cd MyCRM
ng add @angular/fire
ng g c dashboard建立專案
firebase資料庫
資料庫存取服務
資料欄位定義
客戶清單
單一客戶資料
客戶資料維護表單(modal)
首頁
會員管理: customer-list
新增客戶: customer-form
Modal視窗
❶前往Firebase控制台,建立新資料集合
❷安裝firebase, @angular/fire套件
Ⓐ準備工作
(1-1) 前往Firebase控制台,建立新專案或開啟舊專案
新增專案,即可建立資料庫
(1-2) 建立網頁「應用程式」
(1-3) 啟用firestore資料庫,新增集合
ng add @angular/fire(2-1) 選擇要加入(app.module.ts)的功能
Authentication(認證服務)
Firestore(資料庫)
Y
選擇Google帳戶,允許存取
(2-2) 讓firebase CLI取得權限
(2) 安裝firebase, @angular/fire套件流程
firebase: v. 9.4.0
@angular/fire: v. 7.2.0
2021/12/11
(2-3) 複製授權碼,貼到下一個問題
(2-4) 選擇專案
(2-5) 選擇應用程式
授權碼
(2-5-1) 若無應用程式,到Firebase主控台進行新增
選取「網頁」平台
任何名稱皆可
不要勾選
輸入暱稱後即可「註冊應用程式」
(2-6) 完成後,會更新下列檔案
export const environment = {
production: false,
firebaseConfig: {
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: '專案id.firebaseapp.com',
databaseURL: 'https://專案id.firebaseio.com',
projectId: '專案id',
storageBucket: '專案id.appspot.com',
messagingSenderId: 'XXXXXXXXXXXX'
}
};ng add @angular/fire會自動完成此連線設定
(2-6-1) 檢視Firebase應用程式連線設定(已自動加入)
environment.ts片段
...
import { AngularFireModule } from '@angular/fire/compat';
import { environment } from '../environments/environment';
import { AngularFireAuthModule } from '@angular/fire/compat/auth';
import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
...
imports: [
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
AngularFirestoreModule
],
...app.module.ts(修改後)
...
import { initializeApp,provideFirebaseApp } from '@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth,getAuth } from '@angular/fire/auth';
import { provideFirestore,getFirestore } from '@angular/fire/firestore';
...
imports: [
...
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore())
],
...
app.module.ts(修改前)
(2-6-2) 重要!修改專案主要模組設定檔(angularfire v.7.x.x有相容問題)
☑ 安裝ng-bootstrap(modal視窗)
☑ Argon Dashboard CSS/Javascript設定(版型)
☑ 路徑設定
☑ 加入FormsModule(表單模組)
☑ Font Awesome icon連結設定(icon來源)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NewFormExample</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Favicon -->
<link href="/assets/img/brand/favicon.png" rel="icon" type="image/png">
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">
<!-- Icons -->
<link href="/assets/vendor/nucleo/css/nucleo.css" rel="stylesheet">
<link href="/assets/vendor/@fortawesome/fontawesome-free/css/all.min.css" rel="stylesheet">
<!-- Argon CSS -->
<link type="text/css" href="/assets/css/argon.min.css" rel="stylesheet">
</head>
<body>
<app-root></app-root>
<!-- Core -->
<script src="/assets/vendor/jquery/dist/jquery.min.js"></script>
<script src="/assets/vendor/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- Argon JS -->
<script src="/assets/js/argon.min.js"></script>
</body>
</html>加入 ArgonDashboard 相關css檔, js檔(index.html)
index.html
npm i --legacy-peer-deps @ng-bootstrap/ng-bootstrap加入ng-bootstrap套件(處理modal視窗)
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
// ...[略]...
@NgModule({
// ...[略]...
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
FormsModule
],
entryComponents: [ CustomerFormComponent ],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
加入外部元件: NgbModule,FormsModule (app.module.ts)
app.module.ts
⓵ 引入模組名稱
⓶ 注入模組
import { CustomerDetailComponent } from './customer/customer-detail/customer-detail.component';
import { CustomerListComponent } from './customer/customer-list/customer-list.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'customer', component: CustomerListComponent },
{ path: 'customer/:cid', component: CustomerDetailComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
❹ 在app-routing.module.ts設定多個路徑
app-routing.module.ts
⓶ 三個路徑
⓵ 首頁
⓷ 引入元件名稱
<router-outlet></router-outlet>❺ 在app.component.html使用router-outlet
app.component.html
刪除其他內容
或是加入導覽列Navbar, 頁尾Jumbotrum
❻ 加入Font Awesome 圖案css連結(可不加,Argon Dashboard已包含)
<!doctype html>
<html lang="en">
<head>
<!--...略--->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css">
</head>
<body>
<!--...略--->
</body>
</html>index.html
css 連結
<div class="container">
<div class="card-columns text-center my-5">
<div class="card p-3">
<i class="fas fa-users fa-5x"></i>
<div class="card-body">
<h4 class="card-title">
<a class="btn btn-lg btn-primary text-light" [routerLink]="['/customer']">會員管理</a>
</h4>
<p class="card-text">會員資料管理</p>
</div>
</div>
<div class="card p-3">
<i class="fas fa-user-circle fa-5x"></i>
<div class="card-body">
<h4 class="card-title">
<a class="btn btn-lg btn-secondary text-light">員工管理</a>
</h4>
<p class="card-text">銷售代表管理,工作指派等</p>
</div>
</div>
<div class="card p-3">
<i class="fas fa-book-open fa-5x"></i>
<div class="card-body">
<h4 class="card-title">
<a class="btn btn-lg btn-success text-light">待辦事項</a>
</h4>
<p class="card-text">待辦事項行事曆</p>
</div>
</div>
</div>
</div> <a class="btn btn-lg btn-primary" [routerLink]="['/customer']">
會員管理
</a>dashboard.component.html
簡化版(僅建立資料模型檔, 無資料界面檔)
☑ 建立 customer.ts (資料模型檔)
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 查詢全部資料
☞ 新增一筆資料
☞ 修改一筆資料
☞ 刪除一筆資料
☞ 查詢一筆資料
☑ 撰寫個別頁面(使用CRUD服務)
第一階段
查詢全部資料
☑ 建立 customer.ts (資料模型檔)
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 查詢全部資料
☑ 撰寫個別頁面(使用CRUD服務)
☞ 客戶列表(customer-list)
❶ 已建立之customers集合(資料表): 用來儲存客戶資料
❷ 每一客戶文件(紀錄): 包含5個欄位,如下圖
複合欄位的類型:map
export interface Customer {
name: string; // 姓名
date_init_contact: { // 初次接觸日期
day: number,
month: number,
year: number
};
cell_phone: string; // 手機
background_info?: string; // 背景資訊
customer_id: string; // 客戶編號
}
// 增加firestore特有的"亂數id"欄位
export interface CustomerId extends Customer {
id: string;
}
models/customer.ts
⓶ 日期格式
⓵ 5個欄位
⓷ Firestore特有欄位
❶ 建立資料模型檔: 設定欄位
客戶資料表: 5個欄位
第6個id欄位:此為外加Firestore欄位
Firestore管理平台
id: 用來建立資料
...[略]...
export class CustomerService {
customerRef: AngularFirestoreCollection<Customer>; // DB參照點
customers: Observable<CustomerId[]> | undefined; // 動態資料流(供訂閱)
constructor(private db: AngularFirestore) {
// 設定資料庫集合參照點
this.customerRef = this.db.collection<Customer>('customers');
}
// 新增一筆會員資料
addCustomer(customer: Customer): Promise<any> {
return new Promise((resovle, reject) => {}); // 暫時使用
}
// 刪除一筆會員資料
removeCustomer(id: string): Promise<any> {
return new Promise((resovle, reject) => {}); // 暫時使用
}
// 修改一筆會員資料
updateCustomer(id: string, data: Customer): Promise<any> {
return new Promise((resovle, reject) => {}); // 暫時使用
}
// 取得所有會員資料
getCustomers(): Observable<CustomerId[]> {
}
}
⓵ 2個屬性
⓶ 建構子: 設定「資料集合」連結
⓷ 預計有4個方法
customer.service.ts預計功能
❷ 建立服務檔: 提供CRUD等存取功能
...[略]...
import { AngularFirestoreCollection } from '@angular/fire/compat/firestore';
import { AngularFirestore} from '@angular/fire/compat/firestore';
import { AngularFirestoreDocument } from '@angular/fire/compat/firestore';
...[略]...
export class CustomerService {
// 資料庫集合參照點
customerRef: AngularFirestoreCollection<Customer> ;
// 所有客戶的資料流
customers: Observable<CustomerId[]>;
...[略]...
customer.service.ts
AngularFirestoreCollection : 資料集合的參考點
AngularFirestore : 資料庫服務
AngularFirestoreDocument : 資料文件的參考點
所需import之@angular/fire套件: 共3個
❸ 檢視服務檔所需import有無遺漏
完成「查詢全部資料」功能(customer.service.ts)
「客戶列表」(customer-list.component) 頁面後之畫面
注意:手動輸入時,所有欄位都要建立
export class CustomerService {
...
// 取得所有會員資料
getCustomers(): Observable<CustomerId[]> {
return this.customers = this.customerRef.valueChanges({idField: 'id'});
}
}查詢所有資料/未排序
文件id
排序版
export class CustomerService {
...
// 取得所有會員資料
getCustomers(): Observable<CustomerId[]> {
const dref = this.db.collection<Customer>('customers', ref => {
return ref.orderBy('customer_id');
});
return dref.valueChanges({ idField: 'id' });
}
}此處以customer_id欄位排序,預設是小排到大(asc)
第5行建立另一種類型的db參照
需要加第二個函式型的參數,在參數內呼叫orderBy()進行排序
Ⓒ查詢資料
import { CustomerId } from './../../models/customer';
import { CustomerService } from './../../services/customer.service';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-customer-list',
templateUrl: './customer-list.component.html',
styleUrls: ['./customer-list.component.css']
})
export class CustomerListComponent implements OnInit {
customers: Observable<CustomerId[]>; // 所有客戶的資料流
subscription: any;
constructor(private cs: CustomerService) {
this.customers = this.cs.getCustomers(); // 取得所有客戶的資料流
}
ngOnInit() {}
}
customer-list.component.ts(第一版)
注意:資料流並非直接可顯示的「資料」
HTML頁面使用時必須加上「非同步」指令
import { CustomerId } from './../../models/customer';
// .... 省略 ....//
export class CustomerListComponent implements OnInit {
customers: Observable<CustomerId[]>;
subscription: any;
constructor(private cs: CustomerService) {
this.customers = this.cs.getCustomers();
}
ngOnInit() {}
// 彈出視窗: 新增一筆客戶資料
openModal() {}
// 編輯一筆客戶資料
editCustomer(c: CustomerId) {}
// 刪除一筆客戶資料
deleteCustomer(c: CustomerId) {}
}
customer-list.component.ts(第二版)
頁面提供三個功能:新增、修改、刪除
<div class="container">
<div class="row my-5">
<!-- 列1:按鈕列 -->
<div class="col">
<button class="btn btn-lg btn-danger mr-4" (click)="openModal()">新增客戶</button>
<button class="btn btn-lg btn-success" [routerLink]="['/dashboard']">返回首頁</button>
</div>
</div>
<!-- 列2:資料列-->
<div class="row">
<div class="col">
<table class="table table-striped table-inverse">
<thead class="thead-inverse text-center">
<tr>
<th>客戶編號</th>
<th>姓名</th>
<th>初次接觸日期</th>
<th>手機號碼</th>
<th>背景資訊</th>
<th>編輯</th>
</tr>
</thead>
<tbody class="text-center">
<tr *ngFor="let c of customers | async">
<td>{{ c.customer_id }}</td>
<td>{{ c.name }}</td>
<td>{{ c.date_init_contact.year }} / {{ c.date_init_contact.month }} / {{ c.date_init_contact.day }}</td>
<td>{{ c.cell_phone }}</td>
<td>{{ c.background_info }}</td>
<td>
<i class="fas fa-edit" (click)="editCustomer(c)"></i>
<i class="fas fa-trash-alt" (click)="deleteCustomer(c)"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<!-- 列3:分頁(pagination)-->
</div>
</div>customer-list.component.html (完整版)
<tr *ngFor="let c of customers | async">
| : pipeline指令,將前面的輸出,作為後面指令的輸入參數
async : 「非同步」指令,必須等資料到才會顯示
完成新增功能
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 新增一筆資料
☑ 撰寫個別頁面(使用CRUD服務)
☞ 完成新增Modal視窗(customer-form)
☞ 修改「客戶列表」(customer-list)
customer.service.ts加入「新增功能」
customer-list.component.html:新增按鈕
customer-form:輸入表單(modal視窗)
關於Modal視窗
ng add @ng-bootstrap/ng-bootstrap
❶ 若未安裝,請安裝ng-bootstrap
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
// ...[略]...
@NgModule({
// ...[略]...
imports: [
BrowserModule,
AppRoutingModule,
NgbModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
❷ 修改app.module.ts,加入NgbModule
app.module.ts
⓵ 引入模組名稱
⓶ 注入模組
npm i --legacy-peer-deps @ng-bootstrap/ng-bootstrap...
import { CustomerFormComponent } from './customer/customer-form/customer-form.component';
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
CustomerListComponent,
CustomerDetailComponent,
CustomerFormComponent
],
entryComponents: [ CustomerFormComponent ],
...
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }修改app.module.ts
❸ 修改app.module.ts,加入entryComponents設定
指定modal視窗是哪一個元件
暫時不修改!!
dismiss()
關閉modal頁面
回傳處理結果
❷使用NgbActiveModal
呼叫dismiss()
元件頁面
❶使用NgbModal
呼叫open()開啟視窗
Modal頁面
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ContactmodalComponent } from '../contactmodal/contactmodal.component';
export class ContactComponent implements OnInit {
constructor(
private modal: NgbModal,
) { }
ngOnInit() {
}
openContactModal() {
const modalRef = this.modal.open(ContactmodalComponent, {
size: 'lg'
});
}
}
假設主頁面為contact
modal頁面為contactmodal
<button class="btn btn-sm btn-outline-secondary" (click)="openContactModal()">
<i class="fas fa-user-alt"></i> 新增聯絡人
</button>
open()函式來自NgbModal
...
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-contactmodal',
templateUrl: './contactmodal.component.html',
styleUrls: ['./contactmodal.component.css']
})
export class ContactmodalComponent implements OnInit {
constructor(
public activeModal: NgbActiveModal,
) {
}
ngOnInit() {
}
contactSave() {
// 儲存資料進資料庫 或 回傳資料至主頁面...
this.activeModal.close('Y');
}
}
<button type="submit" class="btn btn-outline-primary" (click)="contactSave()">儲存</button>
<button type="reset" class="btn btn-outline-dark">清除</button>
<button type="button" class="btn btn-dark" (click)="activeModal.dismiss('cancel')">取消</button>
dismiss()函式來自NgbActiveModal
可以從html檔呼叫dismiss()?
服務參數必須設定為public
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ContactmodalComponent } from '../contactmodal/contactmodal.component';
export class ContactComponent implements OnInit {
constructor(
private modal: NgbModal,
) { }
ngOnInit() {
}
openContactModal() {
const modalRef = this.modal.open(ContactmodalComponent, {
size: 'lg'
});
// 送出資料到modal
modalRef.componentInstance.data = {name: '書名', price: 300};
modalRef.componentInstance.id = id;
}
}
使用componentInstance設定要傳遞到modal的參數
此處設定了data物件、與id兩個參數
...
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-contactmodal',
templateUrl: './contactmodal.component.html',
styleUrls: ['./contactmodal.component.css']
})
export class ContactmodalComponent implements OnInit {
@Input() data: any; // 資料
@Input() id: string; // id
// this.data, this.id便可直接使用
constructor(
public activeModal: NgbActiveModal,
) {
}
ngOnInit() {
}
contactSave() {
// 儲存資料進資料庫 或 回傳資料至主頁面...
this.activeModal.close('Y');
}
}
使用@Input()元件
export class CustomerService {
fake_cid = 1 // 模擬顧客編號, 每次重新執行都會歸1
...
// 新增一筆會員資料
addCustomer(customer: Customer): Promise<any> {
customer.customer_id = 'C' + ('0000' + this.fake_cid).slice(-4);
this.fake_cid ++; // 遞增
return this.customerRef.add(customer);
}
...
}customer.service.ts
constructor(private db: AngularFirestore) {
// 設定資料庫集合參照點
this.customerRef = this.db.collection<Customer>('customer');
}<div class="container">
<div class="row my-5">
<!-- 列1:按鈕列 -->
<div class="col">
<button class="btn btn-lg btn-danger mr-4" (click)="openModal()">
新增客戶
</button>
<button class="btn btn-lg btn-success" [routerLink]="['/dashboard']">
返回首頁
</button>
</div>
</div>
<!--省略-->
</div>customer-list.component.html(片段)
確認新增客戶有無綁定click事件
...[略]...
export class CustomerListComponent implements OnInit {
customers: Observable<CustomerId[]> | undefined;
subscription: any;
constructor(private cs: CustomerService,
private modal: NgbModal
) { }
// 開啟客戶視窗
openModal() {
const ref = this.modal.open(CustomerFormComponent, {
size: 'lg'
});
this.subscription = ref.componentInstance.result.subscribe(
result => {
console.log('表單輸入資料為:', result);
this.cs.addCustomer(result);
}
);
}
...[略]...
}
customer-list.component.ts(第三版)
加入NgbModal服務:提供彈出視窗功能
open(): 開啟Modal視窗
訂閱Modal視窗的回傳結果
注意: result為表單頁發佈的資料
(customer-form的ts檔必須定義)
...
import { CustomerFormComponent } from './customer/customer-form/customer-form.component';
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
CustomerListComponent,
CustomerDetailComponent,
CustomerFormComponent
],
entryComponents: [ CustomerFormComponent ],
...
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }修改app.module.ts
暫時不修改!!
//... 省略不顯示....
export class CustomerFormComponent implements OnInit {
@Output() result: EventEmitter<Customer> = new EventEmitter<Customer>();
customer: Customer = { // 儲存表單各位欄位的資料,供表單ngModel使用
name: '',
date_init_contact: {day:0, month:0 ,year:0 },
cell_phone: '',
background_info: '',
customer_id: ''
};
constructor(public activeModal: NgbActiveModal) { }
ngOnInit() {
}
save() {
this.result.emit(this.customer);
this.activeModal.close('儲存'); // 關閉視窗,回傳訊息'儲存'
}
}
customer-form.component.ts(第一版)
注意: result為表單頁打算回傳的資料
發佈回傳資料,訂閱者可收到通知
<div class="modal-header">
<h4 class="modal-title">聯絡人</h4>
</div>
<div class="modal-body">
<form #cform="ngForm">
<div class="form-group">
<div class="row">
<label for="name" class="col-2 col-form-label">客戶姓名<span class="text-danger">*</span></label>
<input type="text" class="col-10 form-control" [(ngModel)]="customer.name" name="name" #name="ngModel" placeholder="聯絡人姓名" required>
</div>
<div *ngIf="name.dirty">
<span *ngIf="name.errors; then errors"></span>
</div>
</div>
<div class="form-group">
<div class="row">
<label for="date" class="col-2 col-form-label">初次聯繫<span class="text-danger">*</span></label>
<input type="date" class="col-10 form-control" [(ngModel)]="customer.date_init_contact" name="date" #date="ngModel" ngbDatepicker #d="ngbDatepicker"
required>
</div>
<div *ngIf="date.dirty">
<span *ngIf="date.errors; then errors"></span>
</div>
</div>
<div class="form-group">
<div class="row">
<label for="cell_phone" class="col-2 col-form-label">手機號碼<span class="text-danger">*</span></label>
<input type="text" class="col-10 form-control" [(ngModel)]="customer.cell_phone" name="phone" #phone="ngModel" placeholder="10碼" required minlength="10"
maxlength="10">
</div>
<div *ngIf="phone.dirty">
<span *ngIf="phone.errors; then phone_errors"></span>
</div>
</div>
<div class="form-group row">
<label for="background" class="col-2 col-form-label">背景說明</label>
<textarea type="text" class="col-10 form-control" [(ngModel)]="customer.background_info" name="background"
placeholder="聯絡人背景, 非必要" rows="4">
</textarea>
</div>
<div class="modal-footer">
<small class="text-danger mr-auto">* 必填</small>
<button type="submit" class="btn btn-outline-primary" (click)="save()" [disabled]="!cform.form.valid">儲存</button>
<button type="reset" class="btn btn-outline-dark">清除</button>
<button type="button" class="btn btn-outline-dark" (click)="activeModal.dismiss('cancel')">取消</button>
</div>
</form>
</div>
<ng-template #errors>
<p class="text-danger">欄位未填!</p>
</ng-template>
<ng-template #phone_errors>
<p class="text-danger">手機號碼需10碼!</p>
</ng-template>
customer-form.component.html(第一版)
含欄位錯誤檢查
(FormsModule式的錯誤檢查)
修改資料
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 修改一筆資料
☑ 撰寫個別頁面(使用CRUD服務)
☞ 修改Modal視窗(customer-form)
☞ 修改「客戶列表」(customer-list)
customer.service.ts加入「修改一筆資料」
customer-form:修改資料視窗
customer-list: 加入修改、刪除功能
傳入編輯的資料
export class CustomerService {
...
// 修改一筆會員資料
updateCustomer(id: string, data: Customer): Promise<any> {
const docRef: AngularFirestoreDocument
= this.db.doc<Customer>(`customers/${id}`);
return docRef.set(data, {merge: true});
}
}修改一筆資料
需傳入id
文件id
${id} 在ts檔的作用相當於 用於html檔的 {{ id }}
docRef.set():設定文字為新值
{merge: true}:只更新修改的欄位
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { Customer, CustomerId } from 'src/app/_models/customer';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-customer-form',
templateUrl: './customer-form.component.html',
styleUrls: ['./customer-form.component.css']
})
export class CustomerFormComponent implements OnInit {
@Input() cdoc: CustomerId;
@Output() result: EventEmitter<Customer> = new EventEmitter<Customer>();
customer: Customer = {
name: '',
date_init_contact: null,
cell_phone: '',
background_info: '',
customer_id: ''
};
constructor(public activeModal: NgbActiveModal) { }
ngOnInit() {
if (this.cdoc) {
this.customer = {
name: this.cdoc.name,
date_init_contact: this.cdoc.date_init_contact,
cell_phone: this.cdoc.cell_phone,
background_info: this.cdoc.background_info,
customer_id: this.cdoc.customer_id
};
}
}
save() {
this.result.emit(this.customer);
this.activeModal.close('儲存');
}
}
customer-form.component.ts
要能從customer-list將資料傳入
<div class="modal-header">
<h4 class="modal-title">聯絡人</h4>
</div>
<div class="modal-body">
<form #cform="ngForm">
<div class="form-group">
<div class="row">
<label for="name" class="col-2 col-form-label">客戶姓名<span class="text-danger">*</span></label>
<input type="text" class="col-10 form-control" [(ngModel)]="customer.name" name="name" #name="ngModel" placeholder="聯絡人姓名" required>
</div>
<div *ngIf="name.dirty">
<span *ngIf="name.errors; then errors"></span>
</div>
</div>
<div class="form-group">
<div class="row">
<label for="date" class="col-2 col-form-label">初次聯繫<span class="text-danger">*</span></label>
<input type="date" class="col-10 form-control" [(ngModel)]="customer.date_init_contact" name="date" #date="ngModel" ngbDatepicker #d="ngbDatepicker"
required>
</div>
<div *ngIf="date.dirty">
<span *ngIf="date.errors; then errors"></span>
</div>
</div>
<div class="form-group">
<div class="row">
<label for="cell_phone" class="col-2 col-form-label">手機號碼<span class="text-danger">*</span></label>
<input type="text" class="col-10 form-control" [(ngModel)]="customer.cell_phone" name="phone" #phone="ngModel" placeholder="10碼" required minlength="10"
maxlength="10">
</div>
<div *ngIf="phone.dirty">
<span *ngIf="phone.errors; then phone_errors"></span>
</div>
</div>
<div class="form-group row">
<label for="background" class="col-2 col-form-label">背景說明</label>
<textarea type="text" class="col-10 form-control" [(ngModel)]="customer.background_info" name="background"
placeholder="聯絡人背景, 非必要" rows="4">
</textarea>
</div>
<div class="modal-footer">
<small class="text-danger mr-auto">* 必填</small>
<button type="submit" class="btn btn-outline-primary" (click)="save()" [disabled]="!cform.form.valid">儲存</button>
<button type="reset" class="btn btn-outline-dark">清除</button>
<button type="button" class="btn btn-outline-dark" (click)="activeModal.dismiss('cancel')">取消</button>
</div>
</form>
</div>
<ng-template #errors>
<p class="text-danger">欄位未填!</p>
</ng-template>
<ng-template #phone_errors>
<p class="text-danger">手機號碼需10碼!</p>
</ng-template>
customer-form.component.html(最終版)
import { CustomerId } from './../../_models/customer';
import { CustomerService } from './../../_services/customer.service';
import { Component, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CustomerFormComponent } from '../customer-form/customer-form.component';
import { Observable } from 'rxjs';
import { ConfirmModalComponent } from 'src/app/_modal/confirm-modal/confirm-modal.component';
@Component({
selector: 'app-customer-list',
templateUrl: './customer-list.component.html',
styleUrls: ['./customer-list.component.css']
})
export class CustomerListComponent implements OnInit {
customers: Observable<CustomerId[]>;
subscription: any;
edit: boolean;
constructor(private cs: CustomerService,
private modal: NgbModal
) { }
ngOnInit() {
this.customers = this.cs.getCustomers();
}
// 開啟客戶視窗
openModal(cdoc?: CustomerId) {
const ref = this.modal.open(CustomerFormComponent, {
size: 'lg'
});
// 傳資料進來,代表是[編輯]功能, 而非新增
if (cdoc) {
ref.componentInstance.cdoc = cdoc;
this.edit = true; // 編輯
} else {
this.edit = false; // 新增
}
this.subscription = ref.componentInstance.result.subscribe(
result => {
console.log('表單輸入資料為:', result);
if (this.edit) {
this.cs.updateCustomer(cdoc.id, result);
} else {
this.cs.addCustomer(result);
}
}
);
}
editCustomer(c: CustomerId) {
this.openModal(c);
}
deleteCustomer(c: CustomerId) {
const cmref = this.modal.open(ConfirmModalComponent, {
size: 'lg'
});
cmref.componentInstance.data = c;
cmref.result
.then(
(result) => {
this.cs.removeCustomer(c.id);
}
)
.catch(
(reason) => {
console.log('Cancel原因', reason);
});
}
}
customer-list.component.ts(最終版)
// 開啟客戶視窗
openModal(cdoc?: CustomerId) {
const ref = this.modal.open(CustomerFormComponent, {
size: 'lg'
});
// 傳資料進來,代表是[編輯]功能, 而非新增
if (cdoc) {
ref.componentInstance.cdoc = cdoc;
this.edit = true; // 編輯
} else {
this.edit = false; // 新增
}
this.subscription = ref.componentInstance.result.subscribe(
result => {
console.log('表單輸入資料為:', result);
if (this.edit) {
this.cs.updateCustomer(cdoc.id, result);
} else {
this.cs.addCustomer(result);
}
}
);
}
editCustomer(c: CustomerId) {
this.openModal(c);
}1. 更新openModal() 函式
2. 編輯函式editCustomer()
cdoc?代表此參數可有可無
判斷呼叫openModal()時有無給參數
有參數: 轉送到modal視窗進行編輯
呼叫openModal()時給了參數c
訂閱modal視窗的result資料流
無參數: 代表新增
檢查edit旗號
deleteCustomer(c: CustomerId) {
const cmref = this.modal.open(ConfirmModalComponent, {
size: 'lg'
});
cmref.componentInstance.data = c;
cmref.result
.then(
(result) => {
this.cs.removeCustomer(c.id);
}
)
.catch(
(reason) => {
console.log('Cancel原因', reason);
});
}
3. 刪除函式deleteCustomer()
必須另外新增modal元件:
ng g c modal/confirm-modal刪除資料
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 刪除一筆資料
☑ 撰寫個別頁面(使用CRUD服務)
☞ 建立確認Modal視窗(customer-modal)
☞ 修改「客戶列表」(customer-list)
ng g c modal/confirm-modalng g c modal/confirm-modal
customer.service.ts加入「刪除一筆資料」
confirm-modal:刪除確認視窗
customer-list:呼叫刪除資料功能
export class CustomerService {
...
// 刪除一筆會員資料
removeCustomer(id: string): Promise<any> {
const docRef: AngularFirestoreDocument = this.db.doc<Customer>(`customers/${id}`);
return docRef.delete();
}
}刪除一筆資料
需傳入id
文件id
${id} 在ts檔的作用相當於 用於html檔的 {{ id }}
docRef.delete():永久刪除該筆資料
...
import { ConfirmModalComponent } from './_modal/confirm-modal/confirm-modal.component';
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
CustomerListComponent,
CustomerDetailComponent,
CustomerFormComponent,
ConfirmModalComponent
],
entryComponents: [ CustomerFormComponent, ConfirmModalComponent ],
...
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }修改app.module.ts
暫時不修改!!
// ...略...
export class CustomerListComponent implements OnInit {
// ...略...
deleteCustomer(c: CustomerId) {
const cmref = this.modal.open(ConfirmModalComponent, {
size: 'lg'
});
cmref.componentInstance.data = c;
cmref.result
.then(
(result) => {
this.cs.removeCustomer(c.id);
}
)
.catch(
(reason) => {
console.log('Cancel原因', reason);
});
}
}customer-list.component.ts
import { CustomerId } from './../../_models/customer';
import { Component, OnInit, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-confirm-modal',
templateUrl: './confirm-modal.component.html',
styleUrls: ['./confirm-modal.component.css']
})
export class ConfirmModalComponent implements OnInit {
@Input() data: CustomerId;
constructor(public activeModal: NgbActiveModal) { }
ngOnInit() {
}
}confirm-modal.component.ts(最終版)
確認13行修飾字元為public
11行變數名稱將用於HTML檔
<div class="modal-header">
<h4 class="modal-title" id="modal-title">刪除資料</h4>
<button type="button" class="close" aria-describedby="modal-title" (click)="activeModal.dismiss('Cross')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p><strong>確定刪除<span class="text-primary">{{ data.name }}</span> 資料?</strong></p>
<p>所有該使用者的資料將永久刪除
<span class="text-danger">刪除動作無法回復.</span>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="activeModal.dismiss('Cancel')">取消</button>
<button type="button" class="btn btn-danger" (click)="activeModal.close('DELETE')">確認</button>
</div>confirm-modal.component.html(最終版)
查詢一筆資料
☑ 撰寫CRUD服務 (service/customer.service.ts)
☞ 查詢一筆資料
☑ 撰寫個別頁面(使用CRUD服務)
☞ 完成個別客戶頁面(customer-detail)
☞ 修改「客戶列表」(customer-list)
export class CustomerService {
...
/**
* 讀取單一客戶
* @param id: 客戶id
*/
getCustomer(id: string): Observable<Customer> {
return this.customerRef.doc<Customer>(`customer/${id}`).valueChanges();
}
...
}
「取得一筆資料」服務功能
customer-detail?
如何使用getCustomer()?
頁面內容版型?
如何回到customer-list?
步驟一:修改資料模型檔,定義自己的欄位
_models/customer.ts
import { NgbDate } from '@ng-bootstrap/ng-bootstrap/datepicker/ngb-date';
export interface Customer {
name: string; // 姓名
date_init_contact: NgbDate; // 初次接觸日期
cell_phone: string; // 手機
background_info?: string; // 背景資訊
customer_id: string; // 客戶編號
}
// 增加firestore特有的"亂數id"欄位
export interface CustomerId extends Customer { id: string; }export interface Diary {
date: string; // 日期
date_field: {
day: number,
month: number,
year: number
};
residence: string; // 留宿地點
text: string; // 中文翻譯
text_en: string; // 英文
}
// document id(用於修改/刪除)
export interface DiaryID extends Diary { id: string; }_models/diary.ts
id欄位要保留
步驟二:至Firebase後台建立新的集合
集合名稱: diaries
步驟三:修改服務檔(以模型檔Diary, 集合名稱diaries為例)
...[略]...
export class DiaryService {
customerRef: AngularFirestoreCollection<Customer> ; // 資料庫集合參照點
customers: Observable<CustomerId[]>; // 所有客戶的動態資料流(供訂閱)
constructor(private db: AngularFirestore) {
// 設定資料庫集合參照點
this.customerRef = this.db.collection<Customer>('customers');
}
// 新增一筆會員資料
addCustomer(customer: Customer): Promise<any> {
}
// 刪除一筆會員資料
removeCustomer(id: string): Promise<any> {
}
// 修改一筆會員資料
updateCustomer(id: string, data: Customer): Promise<any> {
}
// 取得所有會員資料
getCustomers(): Observable<CustomerId[]> {
}
}
1. 建立ng g service diary
2. 將所有Customer/customer改為Diary/diary
步驟四:重新設計首頁
更換圖示、文字、超連結
<div class="container">
<div class="card-columns text-center my-5">
<div class="card p-3">
<i class="fas fa-users fa-5x"></i>
<div class="card-body">
<h4 class="card-title">
<a class="btn btn-lg btn-primary text-light" [routerLink]="['/customer']">會員管理</a>
</h4>
<p class="card-text">會員資料管理</p>
</div>
</div>
...略...
</div>
</div>修改連結時,app-routing.module.ts必須同步修改
<i class="fas fa-anchor fa-5x"></i><a class="btn btn-lg btn-primary text-light" [routerLink]="['/diary']">日記管理</a> 步驟五:修改查詢全部(以Customer-list為例)
步驟六:修改新增表單(以Customer-form為例)
HTML表單是更改最多的地方
因為可能有不同的欄位(數量 or 類型)
步驟七:修改「修改功能」(以Customer-form為例)
步驟八:修改「刪除功能」(以Confirm-modal為例)