列表中非常常見的搜尋列
如下圖所示
搜尋列的組成大約分幾種:
input
輸入編號或是帳號。select
下拉式選單。date
時間工具。-src
|-app
|-cms
|-...
|-modules
|-search
|-search-input
|-search-input.component.css
|-search-input.component.html
|-search-input.component.ts
|-search-select
|-search-select.component.css
|-search-select.component.html
|-search-select.component.ts
|-search-date
|-search-date.component.css
|-search-date.component.html
|-search-date.component.ts
|-search.module.ts
|-search.ts
其實後台最多的就是列表,而列表中通常都有資料篩選的選項。
那非常多的Component上都會用到,
筆者就會建議把能夠複用的都包起來,
要用到的時候只要給設定值就好。
資料篩選的選項,
目前分成三種 input、select、date,
也將會有三種子組件相對應,
以及把這三種子組件包起來成SearchModule
,
來做搜尋的各種設定。
search.module.ts
:請先安裝
ng-pick-datetime、
ng-pick-datetime-moment、
moment。
Demo所使用的時間套件為
ng-pick-datetime
相關,
因 Angular Materia 的時間套件只能年月日而已。
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared/shared.module";
import {
DateTimeAdapter,
OWL_DATE_TIME_FORMATS,
OWL_DATE_TIME_LOCALE,
OwlDateTimeModule,
OwlNativeDateTimeModule
} from "ng-pick-datetime";
import { MomentDateTimeAdapter, OWL_MOMENT_DATE_TIME_FORMATS } from "ng-pick-datetime-moment";
import { SearchSelectComponent } from "./search-select/search-select.component";
import { SearchInputComponent } from "./search-input/search-input.component";
import { SearchDateComponent } from "./search-date/search-date.component";
@NgModule({
providers: [
{
provide: DateTimeAdapter,
useClass: MomentDateTimeAdapter,
deps: [OWL_DATE_TIME_LOCALE]
},
{
provide: OWL_DATE_TIME_FORMATS,
useValue: OWL_MOMENT_DATE_TIME_FORMATS
}
],
imports: [SharedModule, OwlDateTimeModule, OwlNativeDateTimeModule],
declarations: [
SearchSelectComponent,
SearchInputComponent,
SearchDateComponent
],
exports: [
OwlDateTimeModule,
OwlNativeDateTimeModule,
SearchSelectComponent,
SearchInputComponent,
SearchDateComponent
]
})
export class SearchModule {}
那接下來我們就要開始設定Search物件裡面的屬性。
以下面的圖來看
大概會有以下的搜尋條件
項目 | 變數名稱 | 型別 |
---|---|---|
會員編號 | id | number |
會員等級 | levelId | number |
會員名稱 | name | string |
商品類型 | typeId | number |
商品狀態 | status | number |
訂單狀態 | status | number |
訂單開始日期 | start | Moment |
訂單結束日期 | end | Moment |
--
接下來還會有幾種狀況:
舉例來說:
如果要搜尋某個會員的 登入紀錄,
那麼進入 登入紀錄 時的Component就會帶入會員編號id
。
但如果是在畫面上有個搜尋input
要輸入會員編號id
,
這時候會有型別的差異,
在html的輸入任何值都是string
。
所以會放在html做搜尋的項目,
只要是number
屬性的,建議會再做一層:
項目 | 變數名稱 | 型別 |
---|---|---|
會員編號 | id | number |
會員編號 |
idSel |
string |
會員等級 | levelId | number |
會員等級 |
levelIdSel |
string |
會員名稱 | name | string |
商品類型 | typeId | number |
商品類型 |
typeIdSel |
string |
商品狀態 | status | number |
訂單狀態 | status | number |
訂單開始日期 | start | Moment |
訂單結束日期 | end | Moment |
--
雖然有會員狀態、管理者狀態、商品狀態、訂單狀態,
都是各個不同的Model,
但因為統一做Search,在這裡就跟各個Model無關,
也就是說變數名稱相同就統一起來。
項目 | 變數名稱 | 型別 |
---|---|---|
會員編號 | id | number |
會員編號 |
idSel |
string |
會員等級 | levelId | number |
會員等級 |
levelIdSel |
string |
會員名稱 | name | string |
商品類型 | typeId | number |
商品類型 |
typeIdSel |
string |
狀態 | status | number |
狀態 | statusSel | string |
訂單開始日期 | start | Moment |
訂單結束日期 | end | Moment |
--
通常一個Search物件,
我們會把寫入的條件賦值給Search物件的屬性。
let s = new Search();
setStatus(idSel:string){
s.idSel = idSel;
}
然後使用者把所有的搜尋條件都賦值後,
我們要把所有被賦值的變數一一過濾以及轉型。
getSearch(){
let obj = <Search>{};
if(!!s.idSel){
obj.id = +s.idSel
}
...
return obj;
}
為什麼要過濾跟轉型?
因為要送去後端的參數不一定跟Search物件的屬性相同名稱。
舉例來說,json-server 的判斷條件:
_gte : 大于等于
_lte : 小于等于
_ne : 不等于
_like : 包含
於是我們要搜尋日期區間:
http://localhost:3000/orders/?inserted_gte=1569859200000&&inserted_lte=1572537600000
inserted
在Model建置為 建立時間。
Model請參照day06 json-server 模擬與 Model 建置(二)
。
所以在過濾取值的時候應該這麼做:
getSearch(s:Search){
let obj = <Search>{};
if(!!s.start && !!s.end){
obj["inserted_gte"] = s.start.valueOf()
obj["inserted_lte"] = s.end.valueOf()
}
...
return obj;
}
start
、end
為 Moment 型別,要轉換為時間戳需使用valueOf()
。
--
在列表中搜尋時如果後端沒有處理,
要前端自己去關聯其他資料表,
這時候參考json-server的關聯條件:
向上關聯:(單個)
GET /comments/1?_expand=post
向下關聯:(多個)
GET /posts?_embed=comments
注意複數s,上面範例有兩個資料表
posts
、comments
參考至 https://www.cnblogs.com/fly_dragon/p/9150732.html
所以我們可以在Search物件裡這麼做:
getSearch(s:Search){
let obj = <Search>{};
if (!!this._expand) {
obj["_expand"] = s.expand;
}
if (!!this._embed) {
obj["_embed"] = s._embed;
}
...
return obj;
}
search.ts
import * as _moment from "moment";
import { Moment } from "moment";
const moment = (_moment as any).default ? (_moment as any).default : _moment;
class Base {
public static getGetters(): string[] {
return Object.keys(this.prototype).filter(name => {
return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["get"] === "function";
});
}
public static getSetters(): string[] {
return Object.keys(this.prototype).filter(name => {
return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["set"] === "function";
});
}
}
export interface IValid {
type: string;
valid: boolean;
}
export class Search extends Base {
private validObjs: IValid[] = [];
constructor(
private _id: number = 0,
private _idSel: string = "",
private _levelId: number = 0,
private _levelIdSel: string = "",
private _typeId: number = 0,
private _typeIdSel: string = "",
private _name: string = "",
private _status: number = 0,
private _statusSel: string = "",
private _start: Moment = null,
private _end: Moment = null,
private _expand:string = "",
private _embed:string = "",
private _check: boolean = true
) {
super();
}
set id(_id) {
this._id = _id;
}
set idSel(_idSel) {
this._idSel = _idSel;
}
set levelId(_levelId) {
this._levelId = _levelId;
}
set levelIdSel(_levelIdSel) {
this._levelIdSel = _levelIdSel;
}
set typeId(_typeId) {
this._typeId = _typeId;
}
set typeIdSel(_typeIdSel) {
this._typeIdSel = _typeIdSel;
}
set name(_name) {
this._name = _name;
}
set status(_status) {
this._status = _status;
}
set statusSel(_statusSel) {
this._statusSel = _statusSel;
}
set start(_start) {
this._start = _start;
}
set end(_end) {
this._end = _end;
}
set expand(_expand) {
this._expand = _expand;
}
set embed(_embed) {
this._embed = _embed;
}
set check(_check) {
this._check = _check;
}
/**GET */
get id(): number {
return this._id;
}
get idSel(): string {
return this._idSel;
}
get levelId(): number {
return this._levelId;
}
get levelIdSel(): string {
return this._levelIdSel;
}
get typeId(): number {
return this._typeId;
}
get typeIdSel(): string {
return this._typeIdSel;
}
get name(): string {
return this._name;
}
get status(): number {
return this._status;
}
get statusSel(): string {
return this._statusSel;
}
get check(): boolean {
return this._check;
}
get start(): Moment {
return this._start;
}
get end(): Moment {
return this._end;
}
get expand(): string {
return this._expand;
}
get embed(): string {
return this._embed;
}
setValidObjs(obj: IValid) {
let index = this.validObjs
.map((validObj: IValid) => {
return validObj.type;
})
.indexOf(obj.type);
if (index === -1) {
this.validObjs.push(obj);
} else {
this.validObjs[index] = obj;
}
this.setCheck();
}
setCheck() {
this._check = true;
this.validObjs.forEach((validObj: IValid) => {
if (!validObj.valid) {
this._check = false;
return;
}
});
}
getSearch(): Search { //過濾以及轉型
let obj = <Search>{};
if (!!this._id) {
obj["id"] = this._id;
}
if (!!this._idSel) {
obj["id"] = +this._idSel;
}
if (!!this._levelId) {
obj["levelId"] = this._levelId;
}
if (!!this._levelIdSel) {
obj["levelId"] = +this._levelIdSel;
}
if (!!this._typeId) {
obj["typeId"] = this._typeId;
}
if (!!this._typeIdSel) {
obj["typeId"] = +this._typeIdSel;
}
if (!!this._name) {
obj["name"] = this._name;
}
if (!!this._status) {
obj["status"] = this._status;
}
if (!!this._statusSel) {
obj["status"] = +this._statusSel;
}
if (!!this._start) {
obj["inserted_gte"] = this._start.valueOf();
}
if (!!this._end) {
obj["inserted_lte"] = this._end.valueOf();
}
if (!!this._expand) {
obj["_expand"] = this._expand;
}
if (!!this._embed) {
obj["_embed"] = this._embed;
}
return obj;
}
setSearchDate(indexDateTab: string) {
switch (indexDateTab) {
case "now":
this._start = moment().subtract(1, "hours");
this._end = moment()
.startOf("date")
.add(1, "days");
break;
case "today":
this._start = moment().startOf("date");
this._end = moment()
.startOf("date")
.add(1, "days");
break;
case "yesterday":
this._start = moment()
.startOf("date")
.subtract(1, "days");
this._end = moment().startOf("date");
break;
case "month":
this._start = moment().startOf("month");
this._end = moment()
.startOf("month")
.add(1, "months");
break;
}
}
}
大概可以分為幾個部分
get
、set
關鍵字,想了解更多請參閱
ES6
的JavaScript。
一個列表內可能有多種搜尋項目,
每個搜尋項目的驗證是獨立的,
但不是每一個搜尋項目都會是有效的。
private validObjs: IValid[] = [];
validObjs
就是裝同一列表中所擁有的搜尋項目的有效值。
如下圖
需要驗證
等級(levelId
)
會員編號(id
)
會員名稱(name
)
如果 等級 有效,則:
let o:IValid = {
type: 'levelId';
valid: true;
}
setValidObjs(o)
setValidObjs()
就是把每個 IValid 物件裝進validObjs裡。setValidObjs()
會先判斷 IValid 物件是否已存在(以type區分)。
以上述例子來說,不管使用者怎麼輸入條件,
最終validObjs
結果只會有三個:
console.log(validObjs)
/*
[
{type: 'levelId', valid: true},
{type: 'id', valid: false},
{type: 'name', valid: true}
]
*/
最後就是setCheck()
要來check validObjs,
只要其中有一個是false
,則不能搜尋!
button-search則是判斷Search物件的
check
屬性
當為 true 的時候才能按
--
最後面的setSearchDate()
就是單純的日期函式,
可能會用在日期搜尋的初始值,
或是日期的快捷鍵等等。
此為完整專案範例碼,連線方式為json-server。
https://stackblitz.com/edit/ngcms-json-server
一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json
到本地端,
並下指令:
json-server db.json
json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690