iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 25
1
Modern Web

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

day25 SearchModule(二):應用

簡述

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

search1
search2
search3

功能

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

  • input 輸入編號或是帳號。
  • select 下拉式選單。
  • data 時間工具。

檔案結構

-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

實作

在上篇已經有敘述Search的基底,
接下來開始實作相對應的子組件。

(一) search-input

search-input.component.ts:

@Component(...)
export class SearchInputComponent {
  @Input() label: string;        //搜尋項目名稱
  @Input() selectLabel: string;  //要搜尋的變數
  @Input() textholder: string;
  @Input() searchObj: Search;    //頁面元素tab物件中的searchObj
  @Output() searchSend: EventEmitter<Search> = new EventEmitter<Search>();
  isNumber = false;         //此搜尋項目本質是否為數字
  searchValue: string;

  constructor() {}

  ngOnInit() {
    this.init();
  }

  init() {
    if (!!this.selectLabel) {
      switch (this.selectLabel) {
        case "id":
          this.isNumber = true;
          this.searchValue = this.searchObj.idSel;
          break;
        case "name":
          this.searchValue = this.searchObj.name;
          break;
      }
    }
  }

  setSearch() {
    switch (this.selectLabel) {
      case "id":
        this.searchObj.idSel = this.searchValue;
        break;
      case "name":
        this.searchObj.name = this.searchValue;
        break;
    }
  }

  resetSearchValue() {
    this.searchValue = "";
  }

  validSearch(event) {
    let obj = { type: this.selectLabel, valid: event.valid };
    this.searchObj.setValidObjs(obj);
    this.getSearch();
  }

  getSearch(reset = false) {
    if (reset) {
      this.resetSearchValue();
      let obj = { type: this.selectLabel, valid: true };
      this.searchObj.setValidObjs(obj);
    }
    this.setSearch();
    this.searchSend.emit(this.searchObj);
  }
}

--

search-input.component.html:

<div class="item-wrapper search">
  <div>
    <span>{{ label | translate }}</span>
  </div>
  <div *ngIf="!isNumber">
    <input
      type="text"
      [(ngModel)]="searchValue"
      [placeholder]="textholder | translate"
      (change)="getSearch()"
    />
    <mat-icon *ngIf="searchValue != ''" (click)="getSearch(true)" 
    class="del-search">
      cancel
    </mat-icon>
  </div>
  <div *ngIf="isNumber">
    <input
      type="text"
      name="idSel"
      #idSel="ngModel"
      [(ngModel)]="searchValue"
      [placeholder]="textholder | translate"
      (change)="validSearch(idSel)"
      [fnValidator]="'numberOnlyValidator'"
    />
    <mat-icon *ngIf="searchValue != ''" (click)="getSearch(true)" 
    class="del-search">
      cancel
    </mat-icon>
    <validation-messages [control]="idSel"></validation-messages>
  </div>
</div>

實際運用時:

searchchild1

在會員列表中需要搜尋會員編號跟會員帳號:

<!--customer/list/list.component.html-->
 <public-search-input
[label]="'customer_id'"
[textholder]="'import_customer_id'"
[selectLabel]="'id'"
[searchObj]="tab.searchObj"
></public-search-input>

<public-search-input
[label]="'customer_name'"
[textholder]="'import_customer_account'"
[selectLabel]="'name'"
[searchObj]="tab.searchObj"
></public-search-input>

(二) search-select

search-select.component.ts

@Component({
  selector: "public-search-select",
  templateUrl: "./search-select.component.html",
  styleUrls: ["./search-select.component.css"]
})
export class SearchSelectComponent {
  @Input() label: string;       //搜尋項目名稱
  @Input() selectLabel: string; //要搜尋的變數
  @Input() searchObj: Search;   //頁面元素tab物件中的searchObj
  @Input() selects: { id: number; name: number }[]; //要顯示的options
  @Output() searchSend: EventEmitter<Search> = new EventEmitter<Search>();
  searchValue: string = "";

  constructor() {}

  ngOnInit() {
    this.init();
  }

  init() {
    if (!!this.selectLabel) {
      switch (this.selectLabel) {
        case "levelId":
          this.searchValue = this.searchObj.levelIdSel;
          break;
        case "typeId":
          this.searchValue = this.searchObj.typeIdSel;
          break;
        case "status":
          this.searchValue = this.searchObj.statusSel;
          break;
      }
    }
  }

  setSearch() {
    switch (this.selectLabel) {
      case "levelId":
        this.searchObj.levelIdSel = this.searchValue;
        break;
      case "typeId":
        this.searchObj.typeIdSel = this.searchValue;
        break;
      case "status":
        this.searchObj.statusSel = this.searchValue;
        break;
    }
  }

  getSearch() {
    this.setSearch();
    this.searchSend.emit(this.searchObj);
  }
}

--

search-select.component.html

<div class="item-wrapper search">
  <div>
    <span>{{ label | translate }}</span>
  </div>
  <div *ngIf="!!selects && !!selects.length">

    <select name="se_status" id="se_status" 
    [(ngModel)]="searchValue" (change)="getSearch()">

      <option value="" selected>{{ "all" | translate }}</option>
      <option *ngFor="let s of selects" [value]="s.id">
        {{ s.name | translate }}
      </option>
    </select>

  </div>
</div>

實際運用時:

searchchild2

在會員列表中需要搜尋會員等級:

<!--customer/list/list.component.html-->
<public-search-select
*ngIf="!!levels && !!levels.length"
[label]="'level_type'"
[selectLabel]="'levelId'"
[searchObj]="tab.searchObj"
[selects]="levels"
></public-search-select>

(三) search-date

search-date.component.ts

interface IDateData {
  valid: boolean;
  value: Moment;
}

@Component({...})
export class SearchDateComponent {
  @Input() searchObj: Search;   //頁面元素tab物件中的searchObj
  @Input() toggleMb: boolean;   //是否為手機尺寸,排版用
  @Output() searchSend: EventEmitter<Search> = new EventEmitter<Search>();
  startObj = <IDateData>{ valid: true, value: null };
  endObj = <IDateData>{ valid: true, value: null };

  constructor() {}

  ngOnInit() {
    this.init();
  }

  init() {
    this.startObj.value = this.searchObj.start;
    this.endObj.value = this.searchObj.end;
  }

  setDate(type: string, event) {
    if (type === "start") {
      this.startObj.valid = event.valid;
      this.setSearch(type, this.startObj);
    } else {
      this.endObj.valid = event.valid;
      this.setSearch(type, this.endObj);
    }
    this.getSearch();
  }

  setSearch(type: string, obj: IDateData) {
    if (obj.valid) {
      this.searchObj[type] = obj.value;
    }
  }

  getSearch() {
    this.searchObj.setValidObjs(
        <IValid>{ type: "start", valid: this.startObj.valid }
    );
    this.searchObj.setValidObjs(
        <IValid>{ type: "end", valid: this.endObj.valid }
    );
    this.searchSend.emit(this.searchObj);
  }
}

--

search-date.component.html

<div *ngIf="!toggleMb" class="flex">
  <div class="item-wrapper search">
    <div>
      <span>{{ "search_date" | translate }}</span>
    </div>
    <div>
      <input
        [placeholder]="'import_date' | translate"
        [(ngModel)]="startObj.value"
        [owlDateTimeTrigger]="start"
        [owlDateTime]="start"
        #s="ngModel"
        (dateTimeInput)="setDate('start', s)"
        required
      />
      <owl-date-time #start></owl-date-time>
      <validation-messages [control]="s" class="tips">
      </validation-messages>
    </div>
  </div>

  <div class="item-wrapper search">
    <div>
      <span>~</span>
    </div>
    <div>
      <input
        [placeholder]="'import_date' | translate"
        [(ngModel)]="endObj.value"
        [owlDateTimeTrigger]="end"
        [owlDateTime]="end"
        #e="ngModel"
        [min]="s.value"
        (dateTimeInput)="setDate('end', e)"
        required
      />
      <owl-date-time #end></owl-date-time>
      <validation-messages [control]="e" class="tips">
      </validation-messages>
    </div>
  </div>
</div>

<ng-container *ngIf="toggleMb">
  <div class="item-wrapper search">
    <div>
      <span>{{ "start" | translate }}</span>
    </div>
    <div>
      <input
        [placeholder]="'import_date' | translate"
        [(ngModel)]="startObj.value"
        [owlDateTimeTrigger]="start"
        [owlDateTime]="start"
        #s="ngModel"
        (dateTimeInput)="setDate('start', s)"
        required
      />
      <owl-date-time [pickerMode]="'dialog'" #start></owl-date-time>
      <validation-messages [control]="s"></validation-messages>
    </div>
  </div>
  <div class="item-wrapper search">
    <div>
      <span>{{ "end" | translate }}</span>
    </div>
    <div>
      <input
        [placeholder]="'import_date' | translate"
        [(ngModel)]="endObj.value"
        [owlDateTimeTrigger]="end"
        [owlDateTime]="end"
        #e="ngModel"
        [min]="s.value"
        (dateTimeInput)="setDate('end', e)"
        required
      />
      <owl-date-time [pickerMode]="'dialog'" #end></owl-date-time>
      <validation-messages [control]="e"></validation-messages>
    </div>
  </div>
</ng-container>

實際運用時:

searchchild3

在訂單列表中需要搜尋下單時間:

<public-search-date
    [searchObj]="tab.searchObj"
    [toggleMb]="isMobileToggle"
></public-search-date>

(四) 總結

最後我們要設定 ListComponent.ts 中的Search設定,
所有ListComponent都是一樣的程序。

舉個例子:

list.component.ts

//order/list/list.component.ts
...
import { Search } from "../../../modules/search/search";
import * as _moment from "moment";
import { TabService } from 'src/app/modules/tab/tab.service';
const moment = (_moment as any).default ? (_moment as any).default : _moment;

@Component(...))
export class OrderListComponent implements OnInit {
  tab: ITabBase;
  ...

  constructor(...) {}

  /*get resolve */
  ngOnInit() {...}

  /*初始 */
  init() {
    if (!this.tab.pageObj) {
      this.tab.pageObj = <IPage>{
        pageIndex: 0,
        pageSize: PAGESIZE,
        length: 0
      };
    }
    if (!this.tab.searchObj) {
      this.tab.searchObj = new Search();
      this.tab.searchObj.setSearchDate("month");
      this.tab.searchObj.expand = "customer";
    } else {
      this.tab.searchObj = Object.assign(
                              new Search(), 
                              this.tab.searchObj
                            );
      this.tab.searchObj.start = new moment(this.tab.searchObj.start);
      this.tab.searchObj.end = new moment(this.tab.searchObj.end);
    }
    this.setDatas(true);
  }

  /*換分頁 */
  onSetPage(pageObj: IPage) {
    ...
  }

  /*搜尋 */
  onSearch() {
    this.tab.pageObj.pageIndex = 0;
    this.setDatas();
  }

  /*裝畫面資料 */
  setDatas(isDataInit = false) {
    this.isLoadingToggle = true;
    let url = this.dataService.setUrl("orders", this.setFilters());
    this.dataService.getData(url, this.tab.pageObj)
    .subscribe((data: IData) => {
      ...
    });
  }

  /*儲存storage */
  setLoadingDatas(isDataInit = false) {
    ...
  }

  /*裝Search條件 */
  setFilters(): IFilter[] {
    let f: IFilter[] = [];
    let s = this.tab.searchObj.getSearch();
    let keys = Object.keys(s);
    keys.forEach((item: string) => {
      f.push({
        key: item,
        val: s[item]
      });
    });
    return f;
  }

  ...
}

--

list.component.html

<div class="container">
  <div class="search-box">
    <div class="flex" *ngIf="!!tab.searchObj">
      <public-search-select
        [label]="'status'"
        [selectLabel]="'status'"
        [searchObj]="tab.searchObj"
        [selects]="ORDERSTATUS"
      ></public-search-select>

      <public-search-date
        [searchObj]="tab.searchObj"
        [toggleMb]="isMobileToggle"
      ></public-search-date>

      <div class="flex">
        <button
          (click)="onSearch()"
          class="button search-btn pb radius-20"
          [disabled]="!tab.searchObj.check"
          [ngClass]="{ disable: !tab.searchObj.check }"
        >
          <mat-icon svgIcon="search"></mat-icon>
        </button>
      </div>

      <div class="search-action">
        <button class="button small-icons radius-5 pr-10 pink" 
        (click)="openDialog('insert')">
          <mat-icon>add</mat-icon>
          <span>{{ "insert" | translate }}</span>
        </button>
      </div>
    </div>
  </div>
  ...
</div>

範例碼

此為完整專案範例碼,連線方式為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


上一篇
day24 SearchModule(一)
下一篇
day26 IndexModule
系列文
用Angular打造完整後台30

尚未有邦友留言

立即登入留言