iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
2
Modern Web

Angular Material完全攻略系列 第 24

[Angular Material完全攻略] Day 24 - 收件夾頁面(4) - Table (進階功能)

  • 分享至 

  • xImage
  •  

昨天我們把一個data table的基礎功能-「顯示資料、分頁、排序」都大致說明了一遍,今天我們來講一些進階的data table用法,以及分頁和排序元件的補充說明;Angular Material中的分頁和排序功能都很強,而且也不會和<mat-table>綁死,在任何地方可以應用。

就讓我們繼續往下看吧!

開始使用Angular Material的Data Table(進階篇)

篩選data table資料-filter

要篩選data table的資料,在前後端都不算困難,我們來看看該如何實作。

直接篩選前端資料

要篩選前端已經撈出來的資料並不困難,只需要為提供的data source設定filter屬性即可。

我們先在畫面上加入一個輸入框:

<mat-form-field>
  <input matInput #filter placeholder="搜尋">
</mat-form-field>

之後在程式中針對input變化時設定data source的filter:

export class EmailListComponent implements OnInit {
  @ViewChild('filter') filter: ElementRef;
  
  ngOnInit() {
    Observable.fromEvent(this.filter.nativeElement, 'keyup')
      .debounceTime(300)
      .distinctUntilChanged()
      .subscribe(() => {
        this.emailsDataSource.filter = (this.filter.nativeElement as HTMLInputElement).value;
      });
  }
}

成果如下:

Filter Basic

自訂篩選前端資料的邏輯

剛才我們使用filter讓data source自己來過濾資料,不過這樣會有點問題,最主要是filter會篩選所有的欄位,只要有某個欄位包含filter內容,就會取出結果,但這未必是我們要的,以上面的例子來說,只看得到標題,卻過濾出一些標題不包含filter的資訊,依情境而定有可能會造成誤解。

Angular Material的MatTableDataSource中有一個filterPredicate方法,是用來對filter篩選資料用的,我們可以複寫這個方法,來針對需要的欄位篩選就好:

this.emailsDataSource.filterPredicate = (data: any, filter: string): boolean => {
  return data.title.indexOf(filter) !== -1;
};

這個方法有兩個傳入參數,分別是每列的資料物件(data),以及要篩選的內容(filter),我們可以回傳true代表這筆資料要顯示,反之則不顯示。

再來看看結果:

filterPredicate

這時候就能夠只針對標題的欄位篩選囉。

篩選後端資料

關於後端篩選,就更加簡單了,不需要再使用data source的filter,只需要把篩選資料送給API就好:

@Component({
  selector: 'app-email-list',
  templateUrl: './email-list.component.html',
  styleUrls: ['./email-list.component.css']
})
export class EmailListComponent implements OnInit {
  ...
  @ViewChild('filter') filter: ElementRef;
  currentFilterData: string;

  ngOnInit() {
    ...

    Observable.fromEvent(this.filter.nativeElement, 'keyup')
      .debounceTime(300)
      .distinctUntilChanged()
      .subscribe((filterData: string) => {
        // 準備要提供給API的filter資料
        this.currentFilterData = filterData;
        this.getIssuees();
        // 後端篩選就不需要指定filter了
        // this.emailsDataSource.filter = (this.filter.nativeElement as HTMLInputElement).value;
      });

    // 後端篩選就用不到filterPredicate了
    // this.emailsDataSource.filterPredicate = (data: any, filter: string): boolean => {
    //   return data.title.indexOf(filter) !== -1;
    // };
  }

  getIssuees() {
    const baseUrl = 'https://api.github.com/search/issues?q=repo:angular/material2';
    let targetUrl = `${baseUrl}&page=${this.currentPage.pageIndex + 1}&per_page=${this.currentPage.pageSize}`;
    if (this.currentSort.direction) {
      targetUrl = `${targetUrl}&sort=${this.currentSort.active}&order=${this.currentSort.direction}`;
    }
    this.httpClient.get<any>(targetUrl).subscribe(data => {
      this.totalCount = data.total_count;
      this.emailsDataSource.data = data.items;
    });
  }
}

成果如下:

Filter from backend

篩選、分頁到排序,3個願望通通都滿足啦!

關於matSort和mat-sort-header的補充

在任何地方使用matSort和mat-sort-header

在之前的範例中,我們都是針對<mat-table>來進行排序,但其實matSortmat-sort-header在任何地方都使用,只要在外層元素加上matSort,當內部的mat-sort-header點擊時,就會發生matSortChange事件。因此以下程式是完全可以正常運作的!

以下我們使用mat-sort-header="xxxx",代表排序時的欄位名稱是title

<table matSort (matSortChange)="sort($event)">
  <thead>
    <tr>
      <th mat-sort-header="title">標題</th>
    </tr>
  </thead>
</table>

只要任何地方有排序的需求,不管是不是<mat-table>只要加上matSortmat-sort-header就搞定啦!

mat-sort-header的參數補充

使用mat-sort-header會在內容旁邊加上一個箭頭符號,方便我們判別目前的排序狀態,這個符號也是可以調整的,有以下幾個屬性可以設定:

  • arrowPosition:箭頭符號要放在文字的前面(before)還是後面(after)
  • disableClear:是否允許取消排序,預設為false,會以asc -> desc -> 輪流切換;若設成true則只會在ascdesc之間切換。
  • start:當按下排序時預設先顯示的排序狀態,預設為asc,可以選擇ascdesc

以下程式會將欄位的排序規則改為,(1)把箭頭放在前面、(2)無法取消排序和(3)預設先以desc排序:

<mat-header-cell *matHeaderCellDef 
                 mat-sort-header 
                 arrowPosition="before" 
                 disableClear="true" 
                 start="desc">
  日期
</mat-header-cell>

結果如下:

Sort Header Properties

可以看到大致上都如我們預期的顯示,唯一的問題是arrowPosition="before"後,整個內容被推到右邊了,這是因為flex設定的關係,從開發人員工具可以看到header cell的加上了一個mat-sort-header-position-before樣式如下:

Sort Header Positio Before

不過這不是大問題,自行CSS調整一下即可:

.mat-sort-header-position-before {
  justify-content: flex-end;
}

成果如下:

Custom css for header cell

看起來就正常多啦!

關於mat-paginator的補充

單獨使用mat-paginator

<mat-paginator>一樣不需要跟<mat-table>綁死,我們可以在任何需要呈現多筆資料的地方使用<mat-paginator>,只要設定好至確的參數及可正常顯示,並且得知分頁切換時的事件。

<mat-paginator #paginator 
               [length]="totalCount" 
               [pageIndex]="0" 
               [pageSize]="10"
               [pageSizeOptions]="[5, 10, 15]"
               (page)="pageChange($event)">
</mat-paginator>

相關的內容在上一篇文章都已經有詳細的說明了,在這邊就不多做說明囉。

設定mat-paginator的提示文字

<mat-paginator>中的文字內容都是英文的,包含上一頁及下一頁按鈕,當滑鼠移過去時會呈現一個tooltip,如下:

Default Paginator Intl

當然這個文字我們也可以進行調整,只要在MatPaginatorIntl這個service裡面設定即可:

@Component({ })
export class EmailListComponent implements OnInit {

  constructor(private matPaginatorIntl: MatPaginatorIntl) {}

  ngOnInit() {
    // 設定顯示筆數資訊文字
    this.matPaginatorIntl.getRangeLabel = (page: number, pageSize: number, length: number): string => {
      if (length === 0 || pageSize === 0) {
        return `第 0 筆、共 ${length} 筆`;
      }

      length = Math.max(length, 0);
      const startIndex = page * pageSize;
      const endIndex = startIndex < length ? Math.min(startIndex + pageSize, length) : startIndex + pageSize;

      return `第 ${startIndex + 1} - ${endIndex} 筆、共 ${length} 筆`;
    };

    // 設定其他顯示資訊文字
    this.matPaginatorIntl.itemsPerPageLabel = '每頁筆數:';
    this.matPaginatorIntl.nextPageLabel = '下一頁';
    this.matPaginatorIntl.previousPageLabel = '上一頁';
  }
}

成果如下:

Custom Paginator Intl

所有的文字都變成中文呈現啦!

本日小結

今天我們介紹了另一個常在data table中常出現的功能,篩選資料(filter),一樣的,不管是直接處理前端的資料還是後端,都不會造成問題;在前端篩選時我們甚至幾乎不用寫什麼程式即可完成篩選,當然我們也能透過自訂filterPredicate來決定篩選邏輯;在後端資料篩選時則沒有什麼特別,就是傳遞資料給API而已。

另外我們也補充了一些關於分頁及排序元件的使用方法,這些方法都是筆者個人認為比較重要的部分,但實際上則有更多可以調整的部分;例如排序功能其實也有一個MatSortHeaderIntl可以設定文字,但主要是在螢幕閱讀器的部分使用,比較難用畫面呈現,因此也就沒有特別介紹了。

關於排序、分頁還有許多可以設定的屬性、API等等,可以直接到官方的文件上去看,也有詳細的說明。

本日的程式碼GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-24-data-table-advanced

分支:day-24-table-advanced

我們終於把所有Angular Material目前版本(5.0.0)所有的30個主要功能和常見的使用情境都介紹過啦!有沒有覺得非常充實,也對於Angular Material的設計和互動感及考量的全面性感覺到非常佩服啊!?如果有任何問題不明白,或是覺得需要補充的內容,都歡迎你直接在下方留言喔!

明天開始我們要邁入從Angular Material衍伸出來的另一大主題-Angular CDK,讓你不必非得要綁死Angular Material,也能打造出具有互動感的元件,準備好迎接新的挑戰吧!!

相關資源


上一篇
[Angular Material完全攻略] Day 23 - 收件夾頁面(3) - Table (基礎功能)
下一篇
[Angular Material完全攻略] Day 25 - Angular CDK(1) - 基礎介紹
系列文
Angular Material完全攻略34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
thomas550728
iT邦新手 5 級 ‧ 2018-03-31 12:05:39

您好:
跟你請教一個問題,該如何讓 Data Table 在選擇之後有光棒,另外如設定成可多選
再次感謝您的文章,讓來開發 UI 上的便利

您好:

Angular的Table僅負責將資料呈現出來,不負責其他複雜的呈現效果,但這些效果在Angular中要實作並不是件很困難的事情,只要去綁定click, mouseover, mouseout等事件,並改變mat-row的樣式即可,我做了以下範例程式給您參考:

https://stackblitz.com/edit/angular-selected-material-table?file=app%2Ftable-basic-example.html

程式碼重點:

  • 用來選擇資料
<ng-container matColumnDef="checked">
  <mat-header-cell *matHeaderCellDef>Check</mat-header-cell>
  <mat-cell *matCellDef="let element"> 
    <mat-checkbox [(ngModel)]="element.checked"></mat-checkbox>
  </mat-cell>
</ng-container>
  • mat-row的樣式變更
<mat-row *matRowDef="let row; columns: displayedColumns;" [ngClass]="{hovered: row.hovered, highlighted: row.highlighted}" (click)="row.highlighted = !row.highlighted" (mouseover)="row.hovered = true" (mouseout)="row.hovered = false"></mat-row>
  • 實際上的CSS樣式設定
.mat-row.hovered {
  background: #eee;
}

.mat-row.highlighted {
  background: #999;
}

/*
  This is a work around for checkbox bug in material row
  https://github.com/angular/material2/issues/8600
*/
mat-cell.mat-cell, mat-header-cell.mat-header-cell {
  overflow: visible; 
}

厲害,果然是神人,讓我對 angular material 的應用有更多的想像
thx

順帶一提,Angular Material 6已經正確支援這個功能囉!
https://material.angular.io/components/table/overview#selection

基本原理是一樣的,但提供了完整的資料結構來儲存

/images/emoticon/emoticon12.gif

0
WT
iT邦新手 5 級 ‧ 2019-02-20 16:30:50

您好:
請問以下這段 disableClear="true" 是否能全域設定??
<mat-header-cell *matHeaderCellDef mat-sort-header disableClear="true">
日期

您好, disableClear 是 matSortHeader 的一個 @Input,所以應該是沒有特別可以全域設定的地方,但您可以自己撰寫一個新的 directive 繼承 matSortHeader,然後覆蓋掉 disableClear 的設定即可

WT iT邦新手 5 級 ‧ 2019-02-21 11:45:44 檢舉

好的,了解! 謝謝您!

0
thomas550728
iT邦新手 5 級 ‧ 2019-07-31 15:31:07

你好:
如果資料欄位為數字,就不能 filter 了嗎?
程式碼及錯誤程式碼如下:
observableFromEvent(this.filter.nativeElement, 'keyup').pipe(
debounceTime(300),
distinctUntilChanged())
.subscribe(() => {
this.defectsDataSource.filter = (this.filter.nativeElement as HTMLInputElement).value;
});
this.defectsDataSource.filterPredicate = (data: any, filter: string): boolean => {

          return data.BG_BUG_ID.indexOf(filter) !== -1;
        }
        
        ![https://ithelp.ithome.com.tw/upload/images/20190731/20108814x9m0ssMky5.png](https://ithelp.ithome.com.tw/upload/images/20190731/20108814x9m0ssMky5.png)
看更多先前的回應...收起先前的回應...

數字沒有 indexOf 所以要調整你過濾的條件喔!
例如 BG_BUG_ID > parseInt(filter, 0)
請在依照自己的狀況調整

感謝版主:
回報一下測試的結果,使用你提供的方法會把大於 filter 的資料全部顯示出來,但如果先把 BG_BUG_ID 轉成 string,則就會只要包含輸入內容的資料,
let tt = data.BG_BUG_ID.toString();
if (tt.indexOf(Filter) != -1)
return true;
else
return false;
}
再次謝謝出問題點

BG_BUG_ID === parseInt(filter, 0) 如何?

不好意思,可以連續針對二個欄位,進行 filter 嗎?
observableFromEvent(this.filter01.nativeElement, 'keyup').pipe(
debounceTime(300),
distinctUntilChanged())
.subscribe(() => {
this.defectsDataSource.filter = (this.filter01.nativeElement as HTMLInputElement).value;

          });

          this.defectsDataSource.filterPredicate = (data: any, Filter01: string): boolean => {
           
            let tt = data.BG_BUG_ID.toString();
            if (tt.indexOf(Filter01) != -1)
              return true;
            else
              return false;
          }


          observableFromEvent(this.filter02.nativeElement, 'keyup').pipe(
          debounceTime(300),
          distinctUntilChanged())
          .subscribe(() => {
            this.defectsDataSource.filter = (this.filter02.nativeElement as HTMLInputElement).value;

          });
          this.defectsDataSource.filterPredicate = (data: any, Filter02: string): boolean => {
           
            if ((data.BG_SUMMARY.indexOf(Filter02) != -1))
               return true;
            else
               return false;
          }
          

試過好只會對最一個 filterPredicate 有作用?
還是我的寫法有問題?
感謝

我要留言

立即登入留言