DAY26 — 展現成果,建立 firestore 動態與複雜的查詢

Jason Z
jason-read-code
Published in
10 min readMar 28, 2022

建立動態查詢

還記得之幾篇一開始如何使用條件查詢 firestore 的資料嗎?

this.firestore.collection("fruits",ref=>ref
.where("price", ">=", 200)
.orderBy("price", "asc"))

這樣的查詢翻譯成白話文就是,我要查詢水果,價格要大於等於200,且且結果要升冪排列。但是這樣有一個缺點,有沒有注意到200這個數字是寫死的,如果想要改成低於300,且結果降冪排列的時候,就要重新寫一個 function ,並且是寫死的,無法動態查詢。

因此官方文件給了一個動態查詢的例子

const size$ = new Subject<string>();
const queryObservable = size$.pipe(
switchMap(size =>
afs.collection('items', ref => ref.where('size', '==', size)).valueChanges()
)
);
// subscribe to changes
queryObservable.subscribe(queriedItems => {
console.log(queriedItems);
});
// trigger the query
size$.next('large');
// re-trigger the query!!!
size$.next('small');

說起來複雜不複雜,簡單也不簡單,如果要看懂上面的程式碼,就必須對 rxjs 有一定的了解,如果對 rxjs 了解的,就大約可以了解上面的行為流程

  1. 將每一個查詢的參數都包裝成 Subject
  2. 如果參數改變的話,就使用 subject的 next 去觸發新的資料流
  3. 而訂閱參數的可被觀察對象在收到新的資料流後,使用 switchMap 轉換資料流,轉換成查詢 firestore 的資料流,並且以最新的參數作為變數去查詢。

如此一來,就完成動態的查詢方法。

再回到 side project 也是使用同樣的原理來做動態查詢

頁面

頁面照樣利用套件的元件,所做的只是將資料綁定上去

<div class="container-fluid">
<div class="row">
<div class="col col12">
<nb-tabset fullWidth (changeTab)="changeMode($event)">
<nb-tab tabTitle="看看自己"> </nb-tab>
<nb-tab tabTitle="看看大家">
<div class="col col-12">
<nb-select
placeholder="選擇挑戰者"
(selectedChange)="changeUser($event)"
[(selected)]="selectedUserId"
>
<nb-option
*ngFor="let user of userList$ | async"
[value]="user.userId"
>{{ user.name }}</nb-option
>
</nb-select>
<input
nbInput
placeholder="選擇日期"
[nbDatepicker]="dateTimePicker"
[(ngModel)]="selectedDate"
(ngModelChange)="changeDate()"
/>
<nb-datepicker #dateTimePicker format="yyyy-MM-dd"></nb-datepicker>
<button nbButton hero status="danger" (click)="clearFilter()">清除</button>
</div>
</nb-tab>
</nb-tabset>
</div>
</div>
<div class="row justify-content-start">
<div
class="col col-md-4 col-12"
*ngFor="let checkin of checkinList$ | async"
>
<challenge90days-checkin-card
[checkin]="checkin"
></challenge90days-checkin-card>
</div>
</div>
</div>

樣式

無,淋漓盡致地使用,一個自訂樣式都不寫。

邏輯

import { Component, OnInit } from '@angular/core';
import {
AngularFirestore,
AngularFirestoreCollection,
} from '@angular/fire/firestore';
import { Observable, Subject } from 'rxjs';
import { UserService } from '../../../../services/user.service';
import {
debounceTime,
distinctUntilChanged,
switchMap,
} from 'rxjs/operators';
import { Checkin, UserInfo } from '@challenge90days/api-interfaces';
import { ActivatedRoute } from '@angular/router';
import { DateService } from '../../../../services/date.service';
@Component({
selector: 'challenge90days-myself',
templateUrl: './list.component.html',
styleUrls: ['./list.component.scss'],
})
export class ListComponent implements OnInit {
userId: string;
userCollection: AngularFirestoreCollection<any>;
userList$: Observable<UserInfo[]>;
checkinList$: Observable<Checkin[]>;
checkinListQuery$ = new Subject<unknown>();
selectedDate: Date;
selectedUserId: string;
mode = false;
constructor(
private firestore: AngularFirestore,
private userService: UserService,
private dateService: DateService,
private activatedRoute: ActivatedRoute
) {
this.userId = this.userService.userId$.value;
}
ngOnInit(): void {
console.log(this.activatedRoute.snapshot.params);
this.getUserId();
this.getUserList();
}
getUserId(): void {
this.userService.userId$.subscribe((userId) => {
console.log(userId);
this.getCheckinListData();
});
}
getUserList(): void {
this.userList$ = this.firestore.collection<UserInfo>('user').valueChanges();
}
getCheckinListData(): void {
this.checkinList$ = this.checkinListQuery$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(() =>
this.firestore
.collection<Checkin>('checkin', (ref) => {
if (this.mode) {
let finalQuery = ref
.where('type', '==', 1)
.orderBy('time', 'desc')
if (this.selectedUserId) {
console.log(this.selectedUserId);
finalQuery = finalQuery.where(
'userId',
'==',
this.selectedUserId
);
}
if (this.selectedDate) {
const { startOfDay, endOfDay } = this.dateService.getDayRange(
this.selectedDate
);
finalQuery = finalQuery
.where('time', '>', startOfDay)
.where('time', '<', endOfDay);
}
return finalQuery;
} else {
return ref
.where('userId', '==', this.userId)
.where('type', '==', 1)
.limit(65)
.orderBy('time', 'desc');
}
})
.valueChanges()
)
);
}
changeMode(tab: any): void {
this.mode = tab.tabTitle === '看看大家';
this.checkinListQuery$.next(this.mode);
}
changeUser(userId: string) {
this.checkinListQuery$.next(userId);
}
changeDate() {
this.checkinListQuery$.next(this.selectedDate);
}
clearFilter() {
this.selectedDate = null;
this.selectedUserId = null;
this.checkinListQuery$.next();
}
}

getCheckinListData ,就是使用同樣的原理去查詢,只不過條件比較多一點,不是只有單一條件,所以看起來比較複雜,因為查詢的條件有:

  • 查詢自己或查詢他人
  • 查詢的日期
  • 查詢他人的特定使用者

有三個查詢條件綜合查詢起來,所以看起來會比較頭昏眼花一點,不過只要搞懂 rxjs 的資料流的概念之後,就會一點也不複雜囉!

--

--

Jason Z
jason-read-code

哲學系畢業的前端工程師,大部分時間都在搞鐵路系統,喜歡寫程式外,更喜歡鐵道,欣賞路上每個平凡的風景