iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 24
0
Modern Web

用Angular打造完整後台系列 第 24

day24 SearchModule(一)

  • 分享至 

  • xImage
  •  

簡述

列表中非常常見的搜尋列
如下圖所示

search1
search2
search3

功能

搜尋列的組成大約分幾種:

  • 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

實作

(一) SearchModule概念

其實後台最多的就是列表,而列表中通常都有資料篩選的選項。

那非常多的Component上都會用到,
筆者就會建議把能夠複用的都包起來,
要用到的時候只要給設定值就好。

資料篩選的選項,
目前分成三種 inputselectdate
也將會有三種子組件相對應,
以及把這三種子組件包起來成SearchModule
來做搜尋的各種設定。

search.module.ts

請先安裝
ng-pick-datetime
ng-pick-datetime-moment
moment

參考至 https://github.com/DanielYKPan/date-time-picker

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物件裡面的屬性。

以下面的圖來看
search1
search2
search3

大概會有以下的搜尋條件

項目 變數名稱 型別
會員編號 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;
}

startendMoment 型別,要轉換為時間戳需使用valueOf()

--

關聯式搜尋:

在列表中搜尋時如果後端沒有處理,
要前端自己去關聯其他資料表,
這時候參考json-server的關聯條件:

向上關聯:(單個)
GET /comments/1?_expand=post

向下關聯:(多個)
GET /posts?_embed=comments

注意複數s,上面範例有兩個資料表postscomments
參考至 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;
    }
  }
}

大概可以分為幾個部分

  • 前面的部分是內部屬性使用getset關鍵字,
    可以對內部屬性設置存值跟取值。

想了解更多請參閱ES6的JavaScript。

  • 驗證各搜尋項目的有效值

一個列表內可能有多種搜尋項目,
每個搜尋項目的驗證是獨立的,
但不是每一個搜尋項目都會是有效的。

private validObjs: IValid[] = [];

validObjs 就是裝同一列表中所擁有的搜尋項目的有效值。

如下圖
search1

需要驗證
等級(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,則不能搜尋!

searcherror

button-search則是判斷Search物件的check屬性
當為 true 的時候才能按

--

  • 日期函式

最後面的setSearchDate()就是單純的日期函式,
可能會用在日期搜尋的初始值,
或是日期的快捷鍵等等。


範例碼

此為完整專案範例碼,連線方式為json-server。

https://stackblitz.com/edit/ngcms-json-server

Start

一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json到本地端,
並下指令:

json-server db.json

json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690


上一篇
day23 OrderModule
下一篇
day25 SearchModule(二):應用
系列文
用Angular打造完整後台30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言