欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

Angular Material 完全攻略系列 day 19 至 day 26 有大用 有大大用 有大大大用

[Angular Material完全攻略] Day 19  - 设计一个部落格(4) - Dialog

昨天我们轻松地介绍两个与进度有关的元件,今天让我们稍微精实一点,来介绍一下写程式多于写HTML的Dialog,不管在不在SPA架构,Dialog都是经典且极为重要的元件,也因此我们会比较多的时间来介绍,好让读着们能完全的掌控Angular Material中的Dialog使用方式!

关于Material Design中的Dialog

Material Design的Dialogs设计指南中,Dialog的作用是用来提醒使用者需要进行的一些特定工作,同时可能包含了重要的提示讯息,或是需要做一些决定等等。因此我们会有非常多机会在里面放是表单元件,或是特性讯息等等。

Dialog的主要几个常见用途如下:

  • 产生提示:用来立即的中断使用者目前的行为,并告知使用者目前的状况或所需要知道的资讯等等。

  • 简易的选单:提供一些基本选项让使用者选取。

  • 确认用:需要使用者明确的进行一个确认性的选择。

Dialog可以说是很基础的元件,也可以说是让画面呈现变得更加有立体感的关键,例如我们过去介绍的Datepicker、Select、Menu等等,都可以说是Dialog的一种应用结果。

开始使用Angular Material的Dialog

要使用Dialog,当然我们必须加入MatDialogModule,接着我们就可以来设计一个简单的Dialog元件。

先让一个Dialog可以显示吧!

Dialog不像是其他Angular Material元件,只要单纯的使用即可,需要一些比较复杂的动作,但其实也不是说多困难,让我们一步一步来说明:

  1. 我们先单纯建立一个元件AddPostDialogComponent元件,不改变任何内容

    ng g c dashboard/blog/add-post-dialog        

  2. 接着用一个按钮,希望这个按钮按下后可以显示Dialog

    <button mat-raised-button color="primary" (click)="showAddPostDialog()">新增文章</button>        

  3. 在对应的component.ts中,注入MatDialog这个Service

    import { MatDialog } from '@angular/material';
    export class BlogComponent {
      constructor(public dialog: MatDialog) {}
    }
    

           

           

           

           

       

  4. 使用这个MatDialog的实体,打开Dialog

    showAddPostDialog() {
      this.dialog.open(AddPostDialogComponent);
    }
    

           

           

           

           

       

  5. 由于这种方式是动态产生元件的,因此我们需要在所属Module中的entryComponents中加入要产生的component

    @NgModule({
      ...
      entryComponents: [AddPostDialogComponent]
    })
    export class DashboardModule {}
    

           

           

           

           

       

虽然看起来步骤比较多,但其实只有两个重点:

  1. 建立要当作Dialog的component

  2. 使用注入的MatDialog实体把它打开

这种步骤理解并习惯了就很快啰。接着就让我们直接来看看结果吧!

Dialog

有没有很感动啊!一个基本的Dialog就浮现在我们面前啦!我们这时候可以透过点击灰底(backdrop)的部分来关闭dialog。

在dialog中这个灰底的部分称为backdrop,我找不到比较好的翻译,因此之后依旧会直接使用backdrop来称呼它。    

使用mat-dialog-xxx丰富Dialog内容

MatDialogModule中,定义了几个重要的directives,这些directives可以帮助我们丰富dialog里面的内容,同时还能够减少一些不必要的程式码,让我们简单来介绍一下:

mat-dialog-title

代表的是一个dialog的标题部分,尽管因为dialog的内容高度太长而造成卷动,依然会固定在整个dialog的最上方。

mat-dialog-content

代表一个dialog的内文部分,当内容长度超过dialog可以容纳的高度时,就会变成可以卷动的模式。

mat-dialog-actions

用来放置行动按钮的区块,呈现位置刚好与mat-dialog-title相反,会固定在画面的最下方,我们会在这里放置一些如确认取消的按钮。

mat-dialog-close

只允许在button上使用的directive,这个directive会使得button变成一个可以关闭目前dialog的按钮。

实际使用看看

接下来就让我们把一个简单的新增文章页面加入,并透过刚刚介绍的directives让整个dialog更加完整吧!

<h2 mat-dialog-title>
  新增部落格文章
</h2>

<mat-dialog-content class="post-form">
  <mat-form-field>
    <input matInput placeholder="標題" />
  </mat-form-field>
  <mat-form-field>
    <textarea matInput placeholder="文章內容" rows="3"></textarea>
  </mat-form-field>
  <p>條款01</p>
  <p>條款02</p>
  ...
</mat-dialog-content>

<mat-dialog-actions>
  <button mat-button color="primary">發表</button>
  <button mat-button mat-dialog-close color="warn">取消</button>
</mat-dialog-actions>






再来看看结果:

Dialog content

一个标准的dialog就诞生啦!除了依照title、content和actions切割空间之外,按下取消的按钮就能够关闭dialog,另外当高度超过可以自动延展的范围(Angular Material中的dialog设定为65vh)时,就会变成可以卷动的状态。

读者有兴趣可以实际试玩看看这个dialog的效果,可以看到以下几个亮点:

  1. dialog显示时,预设会focus到第一个表单控制项

  2. 当使用tab / shift + tab切换focus状态时,永远不会跳出dialog的范围,只会在dialog内移动

  3. 不只按下取消按钮可以关闭dialog,按下ESC键也可以。

以上特色我们在未来介绍Angular CDK时,都可以透过Angular CDK来帮助我们在自己设计的元件中达到一样的功能!在这边就先不多做说明啰。

###关于MatDialog Service

在一开始介绍如何打开一个dialog时,我们注入了MatDialog这个Service,接下来我们来详细介绍一下这个Service的属性及方法:

MatDialog的属性

MatDialog有3个属性:

  • afterAllClosedObservable<void>,会在所有画面上的dialog都被关闭时,才会触发的一个Observable,从这样的说明应该可以发现:没错,Dialog是可以开多个的只需要在任何时候使用MatDialog.open()方法即可

  • afterOpenObservable<MatDialogRef<any>>,每当一个dialog开启时,就会触发一次,并告知目前开启的dialog

  • openDialogsMatDialogRef<any>[],单纯的纪录目前所有开启中的dialog。

简单的范例如下:

this.dialog.afterAllClosed.subscribe(() => {
  console.log('目前已經沒有dialog了');
});

this.dialog.afterOpen.subscribe((dialogRef: MatDialogRef<any>) => {
  console.log(`新的dialog已開啟:${dialogRef.id}`);
  console.log(`目前已開啟 ${this.dialog.openDialogs.length} 個dialog了`);
});






开启多的dialog的程式就不多做说明了,只需要呼叫注入的MatDialogopen()方法,就会自然而然地开一个
新的dialog。

我们直接来看看log显示的结果:

Dialog properties

MatDialog的方法

MatDialog有3个方法,可以让我们自由自在地控制dialog:

  • closeAll:顾名思义,就是关闭所有的dialog

    Close all dialog        

  • getDialogById:每个dialog都有他自己的id,当然我们也可以自行指定id,不管是用哪种方式,只要有id,我们就能在任何时候使用getDialogById(id),来取得某个dialog,如果无法取得的话,会回传undefined

  • open:最重要的一个方法,包含2个参数

    • componentOrTemplateRef:ComponentType<T> | TemplateRef<T>:必填,要显示的dialog,我们可以传入componet或是一个templateRef。

      <ng-template #dialogByTemplate>
        ...
      </ng-template>
      

                 

                 

                 

                 

                 

      在程式中可以直接将这个<ng-template>当作dialog显示,可以减少设计太多的component,反而难以管理。            

      @ViewChildren('dialogByTemplate') dialogByTemplate;
      open() {
          this.dialog.open(this.dialogByTemplate);
      }
      

                 

                 

                 

                 

             

    • config?:MatDialogConfig<D>:非必填,用来设定一些显示的细节。我们稍后会直接针对这个MatDialogConfig型别做说明。

自行设定MatDialogConfig

我们可以透过MatDialogConfig型别设定一些dialog打开时的细节,由于属性众多,以下挑几个个人觉得重要的来介绍:

data

超重要,我们不可能永远只是单纯地打开一个dialog,一定会有需要传入一些资讯的时候,这时我们就可以使用data参数来传入一些资讯,如下:

doPost() {
  this.dialog.open(AddPostConfirmDialogComponent, {
    data: {
      title: this.title
    }
  });
}






而在dialog内,我们可以使用@Inject(MAT_DIALOG_DATA)来注入需要的资讯

@Component()
export class AddPostConfirmDialogComponent implements OnInit {
  get title(){
    return this.data.title;
  }
  constructor(@Inject(MAT_DIALOG_DATA) private data: any) {
      console.log(data.title);
  }
}






成果如下:

Inject data to dialog

autoFocus

当dialog打开时,是否要自动focus在第一个控制项

id

我们可以为每个dialog自定一个唯一的id

高度与宽度

我们可以使用heightwidthminHeightminWidthmaxHeightmaxWidth来设定dialog的尺寸资讯,除了heightwidth一定要用字串表示外,其他属性可以给予数值,当给予数值而非字串时,预设的单位为px

hasBackdrop

是否要使用一个灰色的底来隔绝dialog与下面的画面,也就是backdrop,如果设定为false则依然可以和dialog后面的元件互动。

showAddPostDialog() {
  this.dialog.open(AddPostDialogComponent, {
    hasBackdrop: false
  });
}






结果如下:

backdrop

backdropClass

可以设定backdrop的样式,不太常使用,若对灰色底不满意时,可以进行调整,预设样式如下:

background: rgba(0,0,0,.6);






position

可以设定topbottomleftright,来决定dialog显示的位置。

disableClose

预设情况下我们可以使用ESC键关闭dialog,透过设定disableClosetrue,可以取消这个功能,但要注意可能影响使用者的使用经验。

使用MatDialogRef

所有的dialog开启后,都会产生一个对应的MatDialogRef<T>,其中的T代表实际产生的component或templateRef,取得这个DialogRef的方式很多,主要有

  1. 使用MatDialogopen()时,回传的值,例如以下程式范例,可以透过取得开启对应component的MatDialogRef,来处理原来元件的事件:

      doPost() {
        const confirmDialogRef = this.dialog.open(AddPostConfirmDialogComponent, {
          data: {
            title: this.title
          }
        });
        // doConfirm是AddPostConfirmDialogComponent中的事件(EventEmitter)
        // 透過componentInstance取得AddPostConfirmDialogComponent產生的實體
        confirmDialogRef.componentInstance.doConfirm.subscribe(() => {
          console.log('開啟的dialog按下確認按鈕了');
        });
      }
    

           

           

           

           

       

  2. 使用MatDialoggetDialogById取得

  3. 在我们要当作dialog的component中,注入取得

    @Component()
    export class AddPostDialogComponent {
      constructor(private dialogRef: MatDialogRef<AddPostDialogComponent>)
    
      move() {
        this.dialogRef.updatePosition({
          top: '0',
          left: '0'
        });
      }
    }
    

           

           

           

           

           

    结果如下:        

    DialogRef        

MatDialogRef许多方法都跟前面介绍过的类似,就不多作介绍,有兴趣的可以再去仔细看看dialog的API内容

本日小结

今天我们用了非常多的文字及程式码来介绍dialog这个功能,要建立并产生一个dialog放到画面上本身并不是一件困难的事情,但更多时候我们需要的是dialog的细节设定,这时候就必须仰赖程式码的帮助了。

所以今天的内容会比较多偏向程式码的方面,来说明所有相关的component、directive和service的属性方法等等,并透过这些来调整dialog的显示。

Dialog是前端非常经典的议题,也是SPA架构下不可或缺的一个环节,在Material Design中更是应用范围极广,如果能好好善用dialog,在设计互动性较高的介面时,也能够更加灵活,让使用者经验大幅提高哩!

本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-19-dialog

分支:day-19-dialog

相关资源

thomas550728                
iT邦新手5级‧ 2019-04-04 23:14:30                

你好:
想请教一个问题,是否可在dialog打开后,尚可点击gialog外的按钮,比如是header的button,因为我想做一个截截图功能,万一使用者觉得打开dialog有问题,可以透过系统内建的截图功能直接回报问题,感谢你                    

                         
黄升煌Mike iT邦新手4级‧ 2019-04-05 13:04:00                            

dialog 有一个hasBackdrop 可以设定,设定后就可以按dialog 之下的元素,若希望按其他元素不要被关掉,可以设定disableClose                                

可以参考dialog可以用的属性
https://material.angular.io/components/dialog/api#MatDialogConfig                                

                         
thomas550728 iT邦新手5级‧ 2019-04-10 20:55:33                            

谢谢!但这样有一个缺点,万一使用者按到转换另一个页面时,该dialog 还是停留着,有点唐突                                

                         
黄升煌Mike iT邦新手4级‧ 2019-04-11 10:09:04                            

这也很好解决,在页面元件的ngOnDestory()时关掉就好了                                

登入发表回应            
    
           
0            
           
fm119                
iT邦新手5级‧ 2019-09-05 17:54:08                

如何改变在同一页面上开启的多个Dialog 的z-index?                    

例如有2个Dialog, 分别为A和B. 最初A置于B后面, A的一部分的内容被B遮挡. 当点击A后, A移到B的前面.                    

能否实现此功能? 我查看了angular material 的doc都没有提到z-index.                    

看更多先前的回应...                
                         
fm119 iT邦新手5级‧ 2019-09-06 11:12:36                            

我发现使用addPanelClass/removePanelClass 只能改到内层的Panel, Dialog的前后次序仍被外层的z-index限制.                                

https://ithelp.ithome.com.tw/upload/images/20190906/20120697ROOOWoADso.jpg                                 

                         
fm119 iT邦新手5级‧ 2019-09-08 15:20:54                            

在网上找到了一个方法, 取得dialog 的overlay-container                                

const overlayWrapper = this.dialogRef['_overlayRef'].hostElement;  
    overlayWrapper.classList.add('front-window');
                           
                         
黄升煌Mike iT邦新手4级‧ 2019-09-12 11:22:31                            

感谢提供,这是个虽然不美但可以用的作法,希望之后Angular Material 可以考虑把overlayRef 转成公开属性XDD                               

来自   https://ithelp.ithome.com.tw/articles/10196190



[Angular Material完全攻略] Day 20 - 设计一个部落格(5) - Chip、Tooltip、Snackbar

今天是Angular Material部落格篇的最后一天,我们要一口气介绍三个元件,分别是Chip、Tooltip和Snackbar,其中Chip很适合用来当作类似标签的功能;而Tooltip和Snackbar则是用在不同的地方,作为提示时使用。

关于Material Design中的Chip

Material Design的Chips设计指南中,Chip主要用来把复杂的实体分成多个小区块显示,像是联络人清单等等的资讯,就很适合用Chips存放。

Chip是可以被选择的,当被选择时,我们应该要能提供更多关于这个Chip的资讯;当然,既然可选择,应该也是要能够提供直觉的删除Chip的方法。

开始使用Angular Material的Chip

我们可以在加入MatChipsModule后,使用<mat-chip-list><mat-chip>的组合,即可完成一个基本的清单。

使用mat-chip-list和mat-chip

这种类型的组合我们已经练习过很多次了,不多说,直接上程式:

<h4>文章標籤</h4>
<mat-chip-list>
  <mat-chip>JavaScript</mat-chip>
  <mat-chip>Material Design</mat-chip>
  <mat-chip>Angular Material</mat-chip>
</mat-chip-list>






结果如下:

芯片

简单到不行吧!接下来我们再看看还有什么其他的玩法吧!

替mat-chip加上选取状态

非常容易,只需要加上selected="true"即可

<mat-chip-list>
  <mat-chip selected="true">JavaScript</mat-chip>
  <mat-chip>Material Design</mat-chip>
  <mat-chip>Angular Material</mat-chip>
</mat-chip-list>






结果如下:

精选芯片

可以看到JavaScript选项因为加上selected="true"之后,变成了被选取的颜色了!当然这个被选取的颜色,也是可以调整的。

变更选取的mat-chip颜色

<mat-chip-list>
  <mat-chip color="primary" selected="true">JavaScript</mat-chip>
  <mat-chip color="accent" selected="true">Material Design</mat-chip>
  <mat-chip color="warn" selected="true">Angular Material</mat-chip>
</mat-chip-list>






结果如下:

有色芯片

这里有个比较容易搞混的部分是,设定color并不会改变整个chip的颜色,只会改变被选取时的颜色,目前是没有办法在不被选取的状态下透过color属性改变颜色的,只能直接调整CSS。

设计自己的Chip样式

预设的<mat-chip>已经有了基本Material Design风格的样式,而Angular Material提供了一个<mat-basic-chip>,只提供基本的mat-basic-chip的CSS class,让我们可以自订chip样式:

例如一个CSS如下

.mat-basic-chip {
  background: lime;
  margin: 0 0 0 8px;
  padding: 7px 12px;
  border-radius: 5px;
}






接着画面上加入<mat-basic-chip>

<mat-basic-chip>HTML</mat-basic-chip>






成果如下:

定制基本芯片

自己使用Chip就可以自己做啦!

可被删除的Chip

Chip是可以被删除的,我们可以在<mat-chip>里面加上一个包含matChipRemove的元件,通常是<mat-icon>,之后就可以搭配remove事件,来处理删除的程式逻辑。

如下:

<mat-chip-list>
  <mat-chip *ngFor="let tag of tags" (remove)="removeTag(tag)">
    {{ tag }}
    <mat-icon matChipRemove>cancel</mat-icon>
  </mat-chip>
</mat-chip-list>






接着在程式加上移除的逻辑:

@Component({
  ...
})
export class AddPostDialogComponent implements OnInit {
  tags = ['JavaScript', 'Material Design', 'Angular Material'];
  removeTag(tagName) {
    this.tags = this.tags.filter(tag => tag !== tagName);
  }






成果如下:

删除筹码清单

我们也可以为<mat-chip>设定removable属性,来决定是否允许删除,例如以下程式代表设定成remove事件不会发生,自然也无法删除啰

<mat-chip removable="false">
  ...
</mat-chip-list>






搭配mat-form-field使用

<mat-chip-list><mat-chip>很棒的一点是,他同时可以跟表单元件一起使用,让我们能直接输入文字,并立即转为Chip,我们直接来看看程式码:

这次我们反过来,先看TypeScript再来看HTML

TypeScript部分:

import { ENTER, COMMA } from '@angular/cdk/keycodes';
@Component({
  ...
})
export class AddPostDialogComponent implements OnInit {
  tags = ['JavaScript', 'Material Design', 'Angular Material'];
  separatorKeysCodes = [ENTER, COMMA];
  
  addTag($event: MatChipInputEvent) {
    if (($event.value || '').trim()) {
      const value = $event.value.trim();
      if (this.tags.indexOf(value) === -1) {
        this.tags.push(value);
      }
    }

    $event.input.value = '';
  }
}






上面程式我们宣告了一个separatorKeysCodes变数,来指令当按下哪些键时,会传入新的MatChipInputEvent资料,以这里的设定来说是ENTER键和逗點(,)这两个键盘keyCode都在@angular/cdk/keycodes先定义好了,节省我们寻找键盘keyCode的时间。

另外我们也写了一个addTag()方法,接收当画面上输入资料遇到separatorKeysCodes时,传入的事件,这个事件有两个属性

  • input:输入的来源,基本上就是DOM

  • value:输入的资料

程式中我们确定输入的资料若不是空的且没有重复,就把输入的内容加入清单中,然后重设输入的资料。

接着看看HTML中有哪些东西:

<mat-form-field>
  <mat-chip-list #chipList>
    <mat-chip *ngFor="let tag of tags" (remove)="removeTag(tag)">
      {{ tag }}
      <mat-icon matChipRemove>cancel</mat-icon>
    </mat-chip>
  </mat-chip-list>
  <input placeholder="文章標籤" 
         [matChipInputFor]="chipList" 
         [matChipInputAddOnBlur]="true" 
         [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
         (matChipInputTokenEnd)="addTag($event)" />
</mat-form-field>






我们把原来的<mat-chip-list>包装在<mat-form-field>之中,同时也加上一个<input>,也因此我们可以直接使用placeholder当做整个输入控制项的标签,呈现更具有一致性!

<input>中,关于chip有几个重要的属性可以设定

  • [matChipInputFor]:实际上要放置的<mat-chip-list>

  • [matChipInputAddOnBlur]:是否要在blur时加入chip

  • [matChipInputSeparatorKeyCodes]:当按下指定的键盘按键时,视为要新增chip

  • (matChipInputTokenEnd):当真正要加入一个chip时的逻辑程式。

最后来看看成果:

筹码形式

看起来就很强大啊!

关于Material Design中的Tooltip

Material Design的Tooltips设计指南中,Tooltip主要是用来作为提示讯息使用,会在使用者与某个UI发生互动如滑鼠移过去或focus等行为时显示;用来给予使用这一些提示文字。

开始使用Angular Material的Tooltip

要使用Tooltip,我们需要加入MatTooltipModule,接着就可以直接来使用matTooltip这个directive。

使用matTooltip

我们可以在任何元件上加入matTooltip这个directive来决定tooltip的文字,如下

<mat-form-field>
  <input matInput placeholder="標題" [(ngModel)]="title" matTooltip="替你的文章下一個漂亮的標題吧!" />
</mat-form-field>






成果如下:

工具提示

设定tooltip的方向

我们可以透过matTooltipPosition属性来决定tooltip显示的位置,matTooltipPosition可以设定成如下:

位置            显示位置            
above            显示在目标之上            
below            显示在目标之下            
left            显示在目标左边            
right            显示在目标右边            
before            LTR模式在左边,RTL模式在右边            
after            LTR模式在右边,RTL模式在左边            

例如我们改为matTooltipPosition="right",就会改为显示在目标的右边

<mat-form-field>
  <input matInput placeholder="標題" [(ngModel)]="title" 
         matTooltip="替你的文章下一個漂亮的標題吧!" 
         matTooltipPosition="right"/>
</mat-form-field>






成果如下:

工具提示位置

在程式中决定tooltip的显示

我们可以直接使用matTooltip提供的show()hide()方法来动态打开或关闭tooltip

以下程式按下"打开"按钮,会直接显示tooltip;按下"关闭"按钮则会关闭tooltip:

<button mat-button (click)="titleTooltip.show()">打開提示</button>
<button mat-button (click)="titleTooltip.hide()">關閉提示</button>
<mat-form-field>
  <input matInput placeholder="標題" [(ngModel)]="title" 
         #titleTooltip="matTooltip" 
         matTooltip="替你的文章下一個漂亮的標題吧!" 
         matTooltipPosition="right"/>
</mat-form-field>






成果如下:

程序提示

决定tooltip显示和隐藏的延长时间

我们可以使用matTooltipShowDelay属性来决定要延迟多久显示tooltip,另外也可以使用matTooltipHideDelay来决定延迟多久隐藏tooltip:

<mat-form-field>
  <input matInput placeholder="標題" [(ngModel)]="title" 
         #titleTooltip="matTooltip" 
         matTooltip="替你的文章下一個漂亮的標題吧!" 
         matTooltipPosition="right" 
         matTooltipShowDelay="500" 
         matTooltipHideDelay="500" />
  </mat-form-field>






结果如下:

当滑鼠移过去时,会延迟500毫秒才显示tooltip,同样的当滑鼠离开时,也会延迟500毫秒才关闭tooltip。

延迟显示工具提示

关于Material Design中的Snackbar

Material Design的Snackbars & toasts设计指南中,Snackbar是在画面最下方提供一个文字讯息,让使用者知道目前系统大致的状态。这种功能在Android中也叫toast

开始使用Angular Material的Snackbar

SnackBar主要由一个MatSnackBarservice来控制显示,要使用这个service,必须要先加入MatSnackBarModule

直接使用MatSnackBar service

不多说,直接上程式:

@Component({
  ...
})
export class AddPostConfirmDialogComponent implements OnInit {
  constructor(..., private snackBar: MatSnackBar) {}

  confirm() {
    ...
    this.snackBar.open('已新增部落格文章', '我知道了');
  }
}






成果如下:

小吃店

我们只用了MatSnackBaropen()方法,第一个参数代表提示的讯息,第二个参数代表用来关闭讯息的按钮文字。另外还有第三个参数config?: MatSnackBarConfig,用来做比较细部的微调,稍后我们再来说明。

使用Component当作SnackBar

我们也可以使用MatSnackBaropenFromComponent方法,把一个component当作是SnackBar要显示的对象,概念上跟昨天介绍过的Dialog非常像,下面程式我们自订了一个新的Component,HTML内容如下:

<button mat-icon-button (click)="closeSnackBar()">
  <mat-icon align="right">cancel</mat-icon>
</button>
<h4>已新增部落格文章</h4>
<p>溫馨小提示:每30分鐘記得起來運動一下喔!</p>






接着在程式内,使用MatSnackBardismiss()方法,将SnackBar关闭

@Component({
   ...
})
export class AfterPostNotifyComponent implements OnInit {
  constructor(private snackBar: MatSnackBar) {}
  closeSnackBar() {
    this.snackBar.dismiss();
  }
}






成果如下:

带组件的小吃店

记得自订的component要加入entryComponents中。    

使用config: MatSnackBarConfig来调整显示细节

MatSnackBaropen()openFromComponent()的最后一个参数都是config: MatSnackBarConfig,我们可以透过这个参数来做一些调整,完成的参数可以上SnackBar的API文件去看,以下列出一些个人觉得重要的参数:

持续时间

代表多少ms后,会自动关闭SnackBar,如此一来我们就不需要手动去关掉SnackBar了。

数据

在使用自订component当作SnackBar时非常重要,概念同dialog,我们可以透过注入MAT_SNACK_BAR_DATA来传入我们要的资讯给自订的component。

在component中注入的方法:

@Component({
  ...
})
export class AfterPostNotifyComponent implements OnInit {
  get title() {
    return this.snackBarData.title;
  }
  constructor(private snackBar: MatSnackBar, @Inject(MAT_SNACK_BAR_DATA) private snackBarData: any) {}

  ngOnInit() {}

  closeSnackBar() {
    this.snackBar.dismiss();
  }
}






在使用openFromComponent()时只需要加入这个data参数即可:

this.snackBar.openFromComponent(AfterPostNotifyComponent, {
  data: { title: this.title }
});






成果如下:

将数据注入小吃店

有了昨天dialog的概念后,这里应该很容易理解才对吧!

horizontalPosition和verticalPosition

设定SnackBar的水平和垂直的定位方式。

水平定位方式horizontalPosition有几个选项:

位置            显示位置            
center            显示在萤幕中间            
left            显示在萤幕左边            
right            显示在萤幕右边            
before            LTR模式在左边,RTL模式在右边            
after            LTR模式在右边,RTL模式在左边            

垂直定位方式verticalPosition的选项如下:

位置            显示位置            
top            显示在萤幕之上            
bottom            显示在萤幕之下            

预设会是在中间正下方,如果我们希望改成左下方可以自行设定:

成果如下:

小吃店位置

有没有觉得再调整一下就跟某个社群网站很像啦XD

本日小结

今天我们介绍了一个可以当作标签使用的<mat-chip>,这个元件在需要显示多笔资料时非常好用,也能搭配<mat-form-field>,在表单中能够有更一致的呈现方式。

另外我们也介绍了两个用来作为提示用的好功能,分别是tooltip和snackbar

Tooltip可以当作不同元件的提示讯息,当某个元件可能会有不明确的状况需要提醒,又不希望提示讯息影响到画面呈现时,可以使用这种方式来在不破坏画面的前提给予明确的讯息

Snackbar则适合当作整个网站程式的状态提示,不会特别针对某个元件,比起使用alert这种方式,Snackbar则更具有设计质感,也不会破坏画面的操作,更棒的是我们还能自己设计元件来客制化SnackBar的内容,真的是非常的方便!

部落格篇到这边告一个段落,不知不觉也用掉了20天,介绍了25个Angular Material的实用功能,明天开始我们将进入收件夹篇,把最后的5个功能介绍完;到时候我们就能大声的跟人家说我们把Angular Material基本的30种功能 玩弄在鼓掌之间 理解得很不错啦!

本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-20-chip-tooltip-snackbar

分支:day-20-chip-tooltip-snackbar

相关资源

赫斯基林        
iT邦新手5级‧ 2019-05-15 14:27:47        

刚刚踩雷了,赶快留言给日后的人参考XD            

<mat-chip-list>
  <mat-chip *ngFor="let tag of tags" (remove)="removeTag(tag)">
    {{ tag }}
    <mat-icon matChipRemove>cancel</mat-icon>
  </mat-chip>
</mat-chip-list>
           

(remove)语法已经变成(remove d )了
原本还想说哪里写错没反应
ref: https://github.com/angular/components/issues/11290            

来自   https://ithelp.ithome.com.tw/articles/10196270



[Angular Material完全攻略] Day 21 - 收件夹页面(1) - Expansion Panels

今天开始我们又要进入一个新的页面-「收件夹」啰!先从比较轻松的Expansion Panel开始,我们会使用这个元件在画面左边建立一个类似收件夹大纲/分类的清单;透过Expansion Panel我们可以展开/收合不同类型的资料,在浏览及管理上都非常方便,就让我们继续看下去吧!

关于Material Design中的Expansion panels

Material Design的Expansion panels设计指南中,Expansion panels可以用来将一系列的动作或资讯分门别类放在不同的panels中,可以透过展开与收合某个panel来显示/隐藏资讯,收合时我们可以看到这panel的基本资料,展开后则可以看到或编辑详细的资讯。这样的概念有点像之前介绍过的Steppers,但又没有那么重的“步骤”的概念。

Material Design - Expansion Panel     

开始使用Angular Material的Expansion panels

Expansion panels的相关功能都放在MatExpansionModule之中,加入后我们就可以开始使用<mat-expansion-panel>来建立一个基本的expansion panel。

使用mat-expansion-panel

<mat-expansion-panel>使用上非常简单,唯一要注意的是在<mat-expansion-panel>中一定要加上<mat-expansion-panel-header>,代表整个panel的标题,否则会无法呈现资料哩!

<mat-expansion-panel>
  <mat-expansion-panel-header>
    收件夾
  </mat-expansion-panel-header>
  <!-- 收件夾內容 -->
  ...
</mat-expansion-panel>
<mat-expansion-panel>
  <mat-expansion-panel-header>
    聯絡人列表   
  </mat-expansion-panel-header>
  <!-- 聯絡人列表內容 -->
  ...
</mat-expansion-panel>






结果如下:

Expansion Panel Basic

很简单吧!只要点一下panel的标题,就可以立即展开/收起每个panel啰。

使用mat-panel-title与mat-panel-description未header显示更多资讯

在标题<mat-expansion-panel-header>里面,我们也可以用<mat-panel-title><mat-panel-description>分别显示标题的主文字与补充描述,<mat-panel-description>会在标题旁边加上灰色的文字补充说明;例如:

<mat-expansion-panel>
  <mat-expansion-panel-header>
    <mat-panel-title>
      <mat-icon>inbox</mat-icon>
          收件夾
    </mat-panel-title>
    <mat-panel-description>
      3 封未讀
    </mat-panel-description>
  </mat-expansion-panel-header>
  <!-- 收件夾內容 -->
  ...
</mat-expansion-panel>

<mat-expansion-panel>
  <mat-expansion-panel-header>
    <mat-panel-title>
      <mat-icon>person</mat-icon>
      聯絡人列表
    </mat-panel-title>
    <mat-panel-description>
      2 人在線上
    </mat-panel-description>
  </mat-expansion-panel-header>
  <!-- 聯絡人列表內容 -->
  ...
</mat-expansion-panel>






成果如下:

Panel with description

使用mat-action-row管理更多动作按钮

我们也能够在<mat-expansion-panel>中加上<mat-action-row>,这会在panel下方隔出一块空间,方便我们在这里加上一些管理按钮:

<mat-expansion-panel>
  <mat-expansion-panel-header>
    ...
  </mat-expansion-panel-header>
  <mat-action-row>
    <button mat-button color="primary">管理我的聯絡人</button>
  </mat-action-row>      
</mat-expansion-panel>






成果如下:

Panel with action row

隐藏panel header的展开/收起按钮

每个panel的header部分,右方都会有一个展开/收合的按钮,如下:

Hide toggle before

我们可以透过设定hideToggle属性,把这个按钮藏起来,毕竟我们其实点击标题就可以执行展开/收合的动作了。

<mat-expansion-panel hideToggle="true">
  ...
</mat-expansion-panel>






成果如下:

Hide toggle after

使用expanded属性让panel预设展开

在使用<mat-expansion-panel>时,预设是把内容都收起来的,我们可以透过expanded属性,动态的决定<mat-expansion-panel>是否要展开( true),还是收合( false)

<mat-expansion-panel expanded="true">
  ...
</mat-expansion-panel>






设定disabled属性

我们也可以为<mat-expansion-panel>设定disabled属性,若设定disabled="true",则会维持原来的状态,无法点击:

<mat-expansion-panel hideToggle="true" expanded="true" disabled="true">
  ...
</mat-expansion-panel>>






成果如下:

Disabled Panel

手风琴效果

目前我们的<mat-expansion-panel>每个都是独立的,我们可以把多个<mat-expansion-panel>包装近一个<mat-accordion>中,变成一个手风琴效果,这个<mat-according>有一个multi属性,预设为false,代表一次只会显示一个panel,当某个panel被打开时,其他panel就会自动被收起。

<!-- mat-accotdion的multi預設及為false -->
<mat-accordion multi="false">
  <mat-expansion-panel>
    Panel1
  </mat-expansion-panel> 
  <mat-expansion-panel>
    Panel2
  </mat-expansion-panel> 
</mat-accordion>






成果如下:

Accordion

一个手风琴效果就完成啦!

在手风琴效果中去除panel之间的空隙

使用手风琴效果时,panel之间会产生一个空隙,方便我们分辨每块的panel,如下图:

Accordion default display mode

这个空隙可以透过设定displayMode="flat"拿掉(预设值为displayMode="default"):

<mat-accordion multi="false" displayMode="flat">
  ...
</mat-accordion>






成果如下:

Accordion float display mode

本日小结

今天我们学习了Expansion Panel这个特效满满的元件,来分类管理不同的资讯,方便我们随时展开或收合特定的资料群组,避免空间混乱。同时我们也学会了使用手风琴效果,将多个expansion panels当作一个群组来管理。

遇到特定的情境,expansion panel这种配置方式还蛮实用的哩!

本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-21-expansion-panel-tab

分支:day-21-expansion-panel-tab

相关资源


来自   https://ithelp.ithome.com.tw/articles/10196478



[Angular Material完全攻略] Day 22 - 收件夹页面(2) - Tabs

昨天我们介绍了Expansaion Panels,替收件夹的左边加上了基本的分类资讯,今天让我们来使用Angular Material的Tab元件,来把右边也填入些东西!

Tab元件可以说是许许多多UI都会用到的功能,使用上虽然简单,但也有许多不同的呈现模式,可以说是最简单也最复杂的元件之一,接下来就立刻来看看有些什么变化吧!

关于Material Design中的Tabs

Material Design的Tabs设计指南中,Tab提供了一个简易的方式来切换不同的画面;Tab使用页签的方式管理不同的画面,并且一次只会显示一个画面。

Material Design - Tabs     

开始使用Angular Material的Tabs

要使用Tabs元件,需要加入MatTabsModule,之后我们可以使用<mat-tab-group><mat-tab>来建立tabs。

使用mat-tab-group与mat-tab

跟许多群组型的元件都差不多,我们可以使用<mat-tab-group>作为一个最外层的tab容器,并在里面放置数个<mat-tab>

<mat-tab-group>
  <mat-tab label="郵件列表">
    郵件列表清單
  </mat-tab>
  <mat-tab label="系統設定">
    系統設定表單
  </mat-tab>
</mat-tab-group>






成果如下:

Basic Tabs

使用selectedIndex改变选取的tab

我们可以透过selectedIndex,来设定要选取第几个tab,这个selectedIndex是two way binding的,所以我们也能在手动切换tab时,得知目前的被选取的tab index:

<button mat-button (click)="tabIndex = tabIndex - 1">上一頁</button>
<button mat-button (click)="tabIndex = tabIndex + 1">下一頁</button>
<p>目前的selectedIndex: {{ tabIndex }}</p>
<mat-tab-group [(selectedIndex)]="tabIndex">
  <mat-tab label="郵件列表">
    郵件列表清單
  </mat-tab>
  <mat-tab label="系統設定">
    系統設定表單
  </mat-tab>
  <mat-tab label="其他">
    其他畫面
  </mat-tab>
</mat-tab-group>






成果如下:

SelectedIndex

mat-tab-group的相关事件

Angular Material的tabs是可以透过键盘切换的,我们可以使用左右方向鍵focus到不同的tab,再使用SPACEENTER确认切换动作,这时候我们可以使用focusChange事件来得知focus状态的变更,另外也有selectedIndexChangeselectedTabChange事件:

<mat-tab-group [(selectedIndex)]="tabIndex" 
               (focusChange)="tabFocusChange($event)"
               (selectedIndexChange)="tabSelectedIndexChange($event)"
               (selectedTabChange)="tabSelectedTabChange($event)">
  ...
</mat-tab-group>






TypeScript程式如下:

@Component({ ... })
export class InboxComponent {
  tabFocusChange($event: MatTabChangeEvent) {
    console.log(`focus變更,indx:${$event.index}`);
  }

  tabSelectedIndexChange($event: number) {
    console.log(`selectedIndex變更,index:${$event}`);
  }

  tabSelectedTabChange($event: MatTabChangeEvent) {
    console.log(`selectedTab變更,index:${$event.index}`);
  }
}






成果如下:

Tab Events     

复杂的tab label显示

刚才我们tab显示的label都是使用<mat-tab label="xxx">的方式,这样只能设定纯文字,如果希望更复杂的设定,可以使用<ng-template mat-tab-label>的方式,来设定tab的label:

<mat-tab-group>
  <mat-tab>
    <ng-template mat-tab-label>
      <mat-icon>inbox</mat-icon>
      郵件列表
    </ng-template>
    郵件列表清單
  </mat-tab>
  <mat-tab>
    <ng-template mat-tab-label>
      <mat-icon>settings</mat-icon>
      系統設定
    </ng-template>
    系統設定表單
  </mat-tab>
</mat-tab-group>






成果如下:

Tab label

mta-tab-group固定宽度时,设定stretch模式

当我们为<mat-tab-group>设定一个固定宽度时,可以加上mat-stretch-tabs这个directive,此时所有tab页签就会平均分配宽度,例<mat-tab-group>宽度设为300时,每个<mat-tab>的label就会占据100(300/3)的宽度:

<mat-tab-group mat-stretch-tabs>
  <mat-tab label="tab1"></mat-tab>
  <mat-tab label="tab2"></mat-tab>
  <mat-tab label="tab3"></mat-tab>
</mat-tab-group>






成果如下:

Stretch Tab     

mta-tab-group固定宽度时,tab很多的显示模式

当tab很多,但<mat-tab-group>宽度不够时该怎么办呢?我们不用做任何设定,Angular Material都帮我们设计好了,tab标签不会因为过多而换行,破坏版面,而是会显示一个可以左右移动的按钮,方便我们切换tab:

Many tabs

设定mat-tab-group的颜色

mat-tab-goup的颜色也是可以设定的,我们可以使用backgroundColor来设定背景颜色,使用color来设定focus的tab底部颜色,例如:

<mat-tab-group backgroundColor="primary" color="accent" ...>
  ...
</mat-tab-group>






成果如下:

Colored Tab

使用headerPosition改变tab的位置

预设的tabs都是显示在上方,但我们也能够设定headerPosition="below",让tab呈现在画面下方:

<mat-tab-group headerPosition="below" ...>
  ...
</mat-tab-group>






成果如下:

HeaderPosition Below

设定disabled状态

当舞们不希望某个<mat-tab>能够被选取时,可以为它加上disabled,变成不可选取的状态:

<mat-tab-group>
  <mat-tab>
    ...
  </mat-tab>
  <mat-tab disabled="true">
    ...
  </mat-tab>
</mat-tab-group>






成果如下:

Disabled Tab

设定disabled的tab就会变成灰色的字,而且连focus都无法做到啦!

本日小结

今天我们介绍了Tab元件,Tab在许许多多的UI呈现上都占据了很重要的角色,使用的频率非常非常的高,尽管在Angular Material中,要建立一个基本的tab并不困难,但tab本身却有非常多的变化,来满足各种不同的tab呈现需求,如颜色、方向等等,都是很常见需要调整的情境,好好的把tab学起来,就不怕面临各种不同的需求啰!

本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-22-tabs

分支:day-22-tabs

目前我们已经把收件夹的基本画面都组好了,看起来如下:

Inbox Preview     

明天我们将使用table元件,来把邮件清单给补起来,同时学习table多采多姿的组合技,敬请期待!

相关资源


来自   https://ithelp.ithome.com.tw/articles/10196575




[Angular Material完全攻略] Day 23 - 收件夹页面(3) - Table (基础功能)

今天我们要来介绍Angular Material中最复杂的元件之一:表格Table透过组合table、sort header和paginator这三个功能,我们会完成一个大部分情境都适用的data table。

Data table可以说是许多软体都会被使用到的功能,尤其是管理各种资料的后台程式,更是使用data table的大宗来源,而在商务应用上后台软体的开发需求也是源源不绝,因此data table可以说是前端应用最大的一个议题也不为过!

也因此在Angular Material中要设计data table自然有非常多弹性可以调整的地方,尤其是我们会一次组合3种元件,来完成data table的功能,让状况更加的复杂,因此我们会将data table这个主题拆成2篇介绍,今天我们会先完成一个大部分情境都适用的data table,明天则会针对一些细节的部分做进阶的介绍;准备好了吗?开始啰!

关于Material Design中的Data Tables

Material Design的Data tables设计指南中,data table用来呈现多笔的资料列,在许多系统中的会使用到,我们能透过data table呈现资料,也能够进行资料的管理。

Data table基本上就是表格的呈现,只是比起传统HTML的表格,应该具备更多的功能,如分页、排序等等。

开始使用Angular Material的Data Table

基本上大多数的data table,都需要3个主要部分:资料显示的主体、允许排序的标题及分页在Angular Material中这三个功能分别放在MatTableModuleMatSortModuleMatPaginatorModule,我们今天会一口气把这三个功能都介绍过,来完成一个基本功能完整的data table,因此我们可以先把这3个module都加到我们的前端专案中。

使用mat-table

我们先从最基本的显示资料主体开始,使用到<mat-table>这个元件;在Angular Material中,使用<mat-table>与一般table的使用方式会略有不同,因此让我们一步一步的来完成

1. 准备data source

我们先把资料来源准备好

@Component({ })
export class InboxComponent implements OnInit {
  emailsDataSource = new MatTableDataSource<any>();

  constructor(private httpClient: HttpClient) {}

  ngOnInit() {
    this.httpClient.get<any>('https://api.github.com/search/issues?q=repo:angular/material2&page=1').subscribe(data => {
      this.emailsDataSource.data = data.items;
    });
  }
}






这里我们先把Angular Material的GitHub repository中的issues当作是我们的email资料来源,同时我们建立一个emailDataSource当作资料的来源,他的型别是MatTableDataSource<T>,其中的data: T属性是用来放置主要呈现资料的属性,其他属性我们之后的内容慢慢理解。

资料来源APIhttps://api.github.com/search/issues?q=repo:angular/material2&page=1    

接着在画面上可以使用一个<mat-table>当作data table的主体,同时使用dataSource属性设定资料的来源,来源必须是MatTableDataSource<T>,也就是我们在程式中的emailDataSource

<mat-table [dataSource]="emailsDataSource">
</mat-table>






2. 定义表格的栏位及资料呈现

接着在<mat-table>里面,我们可以使用<ng-container matColumnDef="xxxx">来定义一个表格的栏位(column),matColumnDef="xxxx"代表这个栏位的名称。

而在栏位里面需要提供两个资讯:

  • <mat-header-cell *matHeaderCellDef>:代表资料在标题列cell内容。

  • <mat-cell *matCellDef="let xxx">:代表实际呈现资料的cell。

在这里我们定义了4个column,其中3个负责呈现资料,最后一个则是用来管理资料用的:

<mat-table [dataSource]="emailsDataSource">
  <ng-container matColumnDef="user">
    <mat-header-cell *matHeaderCellDef>寄件人</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.user.login }}</mat-cell>
  </ng-container>
  <ng-container matColumnDef="title">
    <mat-header-cell *matHeaderCellDef>標題</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.title }}</mat-cell>
  </ng-container>
  <ng-container matColumnDef="created_at">
    <mat-header-cell *matHeaderCellDef>日期</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.created_at }}</mat-cell>
  </ng-container>
  <ng-container matColumnDef="management">
    <mat-header-cell *matHeaderCellDef>
      <u>管理</u>
    </mat-header-cell>
    <mat-cell *matCellDef="let row">
      <button mat-button color="primary" (click)="reply(row)">回覆</button>
      <button mat-button color="warn" (click)="delete(row)">刪除</button>
    </mat-cell>
  </ng-container>
</mat-table>






3. 显示标题资料列

我们可以使用<mat-header-row *matHeaderRowDef="[]">语法,将资料的标题列显示出来,其中*matHeaderRowDef="[]"放置的是每个标题栏位的名称,也就是我们上个步骤的matColumnDef="xxx"的名称,设定后会把对应名称column里的<mat-header-cell>找出来并呈现在画面上:

<mat-table [dataSource]="emailsDataSource">
  <ng-container matColumnDef="user">
    <mat-header-cell *matHeaderCellDef>寄件人</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.user.login }}</mat-cell>
  </ng-container>
  ...

  <mat-header-row *matHeaderRowDef="['user', 'title', 'created_at', 'management']"></mat-header-row>
</mat-table>






这时候我们已经可以看到标题列的呈现啰:

Table Header     

4. 显示资料列

有了标题列之后,我们再来把资料列本身显示出来,透过<mat-row *matRowDef="">的语法,我们可以决定要显示的栏位资料,使用方式如下:

<mat-row *matRowDef="let emailRow; columns: ['user', 'title', 'created_at', 'management']"></mat-row>






在这边我们拆成两个部分

  • let emailRow:代表每一列的资料列名称,可以想像成类似<ng-container *ngFor="let emailRow of dataSource.data"></ng-container>的概念。

  • columns:代表实际上要呈现的资料栏位名称。

这时候画面就可以正确呈现data table资料啦!

Table Rows     

图解程式码

用文字其实稍微有点难描述,毕竟这跟我们一般使用<table><tr><td>的习惯不太相同,所以笔者把程式码截图后,搭配图示解说,可以把以下图片与上面的步骤做对应,应该会比较好理解:

Data Table Explain     

以上就是基本的data table资料的呈现方式,刚开始可能会不太适应,但用习惯之后,你会发现这种设计其实更加直觉,维护起来也会更加容易哩!

虽然是以表格的方式呈现,但在Angular Material的data table已经把资料拆成一块一块的,再透过flexbox排版组合,因此已经不像原生的HTML的<table>了,也同时代表我们能够有弹性的调整data table的呈现方式啰。    

客制化cell样式

预设情况下,每个栏位的宽度都会被平均分配,但这不一定是我们需要的,已上述例子来说,「寄件人」其实不用那么宽,我们可以把空间省下来,至于该如何做呢?其实在每个matColumnDef区块内,都会依照我们给的名称,加上mat-column-xxxx的class,例如寄件人栏位我们定义为matColumnDef="user",此时在里面的<mat-header-cell><mat-cell>都会加上一个mat-column-user的class,所以我们只需要定义这个class就可以啦!

.mat-column-user {
  max-width: 100px;
}

.mat-header-cell.mat-column-user {
  color: red;
}

.mat-cell.mat-column-user {
  font-weight: bold;
  text-decoration: underline dashed #000;
  cursor: pointer;
}






上述CSS中我们将所有包含.mat-column-user的cell最大宽度都设为100px,同时我们也设定了标题cell改用红色文字,以及内容cell的样式调整,结果是否如我们预期呢?

成果如下:

Styled Cell

果然完全照着我们的预期显示,完全不用担心跟原来习惯不同后会不会产生难以做细部调整的问题,真是太棒了!

加入分页功能

接下来我们要加入另一个data table应该具有的重要功能,分页

使用前端资料进行分页

我们可以使用<mat-paginator>元件,立刻产生一个基本的分页画面,并把这个<mat-paginator>传给我们的MatTableDataSource,这时候就可以把分页功能和data table绑在一起啦!

<mat-paginator #paginator 
               [length]="totalCount" 
               [pageIndex]="0" 
               [pageSize]="10"
               [pageSizeOptions]="[5, 10, 15]">
</mat-paginator>






<mat-paginator>的几个重要属性说明如下:

  • length:资料的总笔数,有这个笔数才能够搭配其他参数算出总共有几页等资讯

  • pageIndex:目前的页码,从0开始,预设值是0

  • pageSize:每页要呈现几笔资料,预设值是50

  • pageSizeOptions:允许切换的每页资料笔数

接着我们调整一下程式的部分:

@Component({ })
export class EmailListComponent implements OnInit {
  @ViewChild('paginator') paginator: MatPaginator;
  
  ...

  ngOnInit() {
    this.httpClient.get<any>('https://api.github.com/search/issues?q=repo:angular/material2&page=1').subscribe(data => {
      this.totalCount = data.items.length;
      this.emailsDataSource.data = data.items;
      this.emailsDataSource.paginator = this.paginator;
    });
  }






这里主要是设定总笔数跟data source所使用的paginator。成果如下:

Pagination

使用后端资料进行分页

刚刚我们已经完成一个基本的分页了,不过目前这个分页有点问题,因为分页的对象是已经捞到前端的资料,因此捞出30笔,就只能针对这30笔做分页,但在实务上我们常常是需要将分页资讯传递给后端,由后端依照分页资讯捞取资料后,再传给前端显示,这应该怎么做呢?

这时候我们就不再需要把<mat-paginator>指定给data srouce,而是接收<mat-paginator>page: Observable<PageEvent>变动,当使用者切换分页资讯时,再依照分页资讯传递给后端重新捞取资料,而使用者切换分页时会得到一个PageEvent,这个PageEvent有3个参数:

  • pageIndex:页码

  • pageSize:每页笔数

  • length:目前资料总笔数(通常是用不到)

接着就让我们来调整一下程式码,让分页时可以到后端读取资料吧!

@Component({ })
export class EmailListComponent implements OnInit {
  @ViewChild('paginator') paginator: MatPaginator;
  
  ...
  
  ngOnInit() {
    this.getIssues(0, 10);

    // 分頁切換時,重新取得資料
    this.paginator.page.subscribe((page: PageEvent) => {
      this.getIssues(page.pageIndex, page.pageSize);
    });
  }

  getIssues(pageIndex, pageSize) {
    this.httpClient
      .get<any>(`https://api.github.com/search/issues?q=repo:angular/material2&page=${pageIndex + 1}&per_page=${pageSize}`)
      .subscribe(data => {
        this.totalCount = data.total_count;
        this.emailsDataSource.data = data.items;
        // 從後端取得資料時,就不用指定data srouce的paginator了
        // this.emailsDataSource.paginator = this.paginator;
      });
  }






成果如下:

Pagination Data From Backend

我们打开开发人员工具(F12),可以看到每次分页切换时,就会自动往后端查询资料,然后再更新到画面上啰。

加入排序功能

有了分页后,我们再来加入排序的功能。

使用前端资料进行排序

要替栏位加上排序功能很简单,首先在<mat-table>中加入matSort,接着在要排序栏位的<mat-header-cell>加入mat-sort-header这个directive即可。

<mat-table [dataSource]="emailsDataSource" matSort #sortTable="matSort">
  ...
  <ng-container matColumnDef="created_at">
    <mat-header-cell *matHeaderCellDef mat-sort-header>日期</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.created_at }}</mat-cell>
  </ng-container>
  ...
</mat-table>






跟前端资料分页时一样,要把这个matSort的来源交给data source

@Component({ })
export class EmailListComponent implements OnInit {
  @ViewChild('sortTable') sortTable: MatSort;
  
  ...

  getIssues(pageIndex, pageSize) {
    this.httpClient
      .get<any>(`https://api.github.com/search/issues?q=repo:angular/material2&page=${pageIndex + 1}&per_page=${pageSize}`)
      .subscribe(data => {
        this.totalCount = data.total_count;
        this.emailsDataSource.data = data.items;
        // 設定使用前端資料排序
        this.emailsDataSource.sort = this.sortTable;
        // 從後端取得資料時,就不用指定data srouce的paginator了
        // this.emailsDataSource.paginator = this.paginator;
      });
  }






结果如下:

Data Table Sort

使用后端资料进行排序

当然,我们也一样可以透过把资料传递给后端的方式来进行排序,只需要在加入matSort的来源(也就是<mat-table>)加入一个matSortChange事件即可,当使用者按下栏位排序时,会传入一个Sort型别的变数,包含两个栏位:

  • active:选择要排序的栏位。

  • direction:包含ascdesc空字串,代表要如何进行排序。

<mat-table [dataSource]="emailsDataSource" matSort #sortTable="matSort" (matSortChange)="changeSort($event)">
</mat-table>






接着我们针对matSortChange来处理排序,并把程式做一些比较大的调整:

@Component({ })
export class EmailListComponent implements OnInit {
  @ViewChild('paginator') paginator: MatPaginator;
  @ViewChild('sortTable') sortTable: MatSort;

  currentPage: PageEvent;
  currentSort: Sort;
  
  ...

  ngOnInit() {
    this.currentPage = {
      pageIndex: 0,
      pageSize: 10,
      length: null
    };
    this.currentSort = {
      active: '',
      direction: ''
    };
    this.getIssuees();

    // 分頁切換時,重新取得資料
    this.paginator.page.subscribe((page: PageEvent) => {
      this.currentPage = page;
      this.getIssuees();
    });
  }

  changeSort(sortInfo: Sort) {
    // 因為API排序欄位是created,因此在這邊做調整
    if (sortInfo.active === 'created_at') {
      sortInfo.active = 'created';
    }
    this.currentSort = sortInfo;
    this.getIssuees();
  }

  getIssues() {
    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;
        // 從後端進行排序時,不用指定sort
        // this.emailsDataSource.sort = this.sortTable;
        // 從後端取得資料時,就不用指定data srouce的paginator了
        // this.emailsDataSource.paginator = this.paginator;
      });
  }
}






程式码看起来有点多,但主要的部分就是把分页和排序都串在一起,然后再一次跟后端取得资料。

成果如下:

Sort From Backend     

同样的打开开发人员工具(F12),可已看到每次按排序时,就会自动向后端抓资料啰。

本日小结

在资料呈现上,data table可以说是最实用的一种呈现方式,传统使用<table>呈现资料有许多的限制,但若要自己使用CSS排版来重新设计一个data table也不是一件容易的事情,好在Angular Material帮我们设计了一个<mat-table>来实现各种data table所需要的功能。

今天我们把一个data table最关键的三个部分:显示资料、分页及排序都介绍过了,刚开始也许会对于这样的设计方式不太习惯,但多做几次后就不难发现这种设计方式会更加直觉,管理上也更加容易。

不管是从前端整理资料,还是把排序、分页等资讯都交给后端处理,在Angular Material的data table都有对应的方法,以期漂亮的UI,在设计上可以说是没死角!

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

分支:day-23-table

明天我们会在介绍两个进阶的data table功能,以及针对排序、分页功能做一些进阶的说明。

相关资源

WT                
iT邦新手5级‧ 2019-02-17 23:20:42                

您好:
请问您,如果是要使用"前端资料"的方法,不管是分页、排序或搜寻,
如果table中有按钮可以跳转页面(例如进入明细),
那我要如何在返回时还能保留原来table中分页、排序、搜寻的状态呢?                    

                         
黄升煌Mike iT邦新手4级‧ 2019-02-18 08:04:42                            

您好,建议可以将搜寻结果或搜寻条件暂存起来,如localstorage 或记忆体内,在跳转回来时,检查是否有暂存的内容,有的话就显示,没有的话就重新查询:)                                

                         
WT iT邦新手5级‧ 2019-02-18 11:37:51                            

谢谢MIKE哥的回覆!!
另外请问您,用您的方法,我返回时pageIndex和pageSize都能取回正确的设定,但如果我加上了filter的条件,然后在ngOnInit()中加入
this.memberDataSource .filter = localstorage.getItem('filter')
显示会帮我直接产生搜寻结果,但是资料的总笔数却是原始笔数而不是搜寻后的笔数,不知道我该在哪修改?
谢谢您! !!                                

                         
WT iT邦新手5级‧ 2019-02-19 13:12:53                            

可以用this.length = this.memberDataSource.filteredData.length;
已解决,感谢您!                                

登入发表回应            
    
           
0            
           
ekekvivi                
iT邦新手5级‧ 2019-05-08 11:42:40                

您好:
依照您的方法实作
但是他一直显示                    

ERROR TypeError: Cannot read property 'page' of undefined                    

                         
黄升煌Mike iT邦新手4级‧ 2019-05-09 11:42:38                            

看起来是抓不到分分页的元件,但正常是可以的,你可以把程式贴到stackblitz 上吗?比较好确认程式哪里有问题                                

登入发表回应            
    
           
0            
           
WT                
iT邦新手5级‧ 2019-06-27 12:09:22                

Mike哥您好:
请问您,如下程式码
lineDataSource = new MatTableDataSource();
....
this.lineDataSource.data = this.onLineList;                    

我持续刷新this.lineDataSource.data的内容(大约一秒刷新一次),
那页面中如果有卷轴的话,我不管卷轴往上滚还是往下滚,页面中都会往下再多滚一次。
请问您有遇过这个问题吗?                    

来自  https://ithelp.ithome.com.tw/articles/10196731



[Angular Material完全攻略] Day 24 - 收件夹页面(4) - Table (进阶功能)

昨天我们把一个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,也能打造出具有互动感的元件,准备好迎接新的挑战吧!

相关资源

thomas550728                
iT邦新手5级‧ 2018-03-31 12:05:39                

您好:
跟你请教一个问题,该如何让Data Table在选择之后有光棒,另外如设定成可多选
再次感谢您的文章,让来开发UI上的便利                    

                         
黄升煌Mike iT邦新手4级‧ 2018-03-31 23:07:10                            

您好:                                

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; 
}
                           
                         
thomas550728 iT邦新手5级‧ 2018-04-02 14:08:13                            

厉害,果然是神人,让我对angular material的应用有更多的想像
thx                                

                         
黄升煌Mike iT邦新手4级‧ 2018-06-27 15:14:36                            

顺带一提,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">
日期
                   

                         
黄升煌Mike iT邦新手4级‧ 2019-02-21 09:44:09                            

您好,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)
               
看更多先前的回应...                
                         
thomas550728 iT邦新手5级‧ 2019-08-02 18:22:21                            

感谢版主:
回报一下测试的结果,使用你提供的方法会把大于filter的资料全部显示出来,但如果先把BG_BUG_ID转成string,则就会只要包含输入内容的资料,
let tt = data.BG_BUG_ID .toString();
if (tt.indexOf(Filter) != -1)
return true;
else
return false;
}
再次谢谢出问题点                                

                         
黄升煌Mike iT邦新手4级‧ 2019-08-04 21:40:04                            

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

                         
thomas550728 iT邦新手5级‧ 2019-08-08 11:11:42                            

不好意思,可以连续针对二个栏位,进行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有作用?
还是我的写法有问题?
感谢                                

来自    https://ithelp.ithome.com.tw/articles/10196827


[Angular Material完全攻略] Day 25 - Angular CDK(1) - 基础介绍

我们即将要迈入新的篇章-Angular CDK,我们今天先不来写程式,而是大致的把目前(5.0.0)Angular CDK的架构做一个整体的介绍,让读者们能先在心中有个蓝图,在未来学习Angular CDK应该会更有感觉!

CDK是component development kit的简写,顾名思义就是「用来开发元件的工具」,因此不难想像Angular CDK就是用来帮助我们开发各种元件的好用工具。建议读者可以先看过2017年在奥兰多的Angular Mix大会上的一段关于Angular CDK介绍的影片:

image.png

https://www.youtube.com/watch?v=kYDLlfpTLEA

当然,如果你不想看英文或没有时间看影片,也可以直接往下看,以下内容将会以上段影片及投影片为主要参考,来介绍Angular CDK。

简介Angular CDK

故事的起源

故事是这样的,Angular Material团队在开发这套library时,对于品质有许多的坚持,这是我们都已经知道的事情;而在开发时,团队发现一件铁一般的事实:「许多元件都有部分的功能是共用的!」如下图:

Angular CDK Slide 1     

就算不是如同Angular Material开发团队般拥有顶尖的人才,只要是有一定经验的开发人员,就算不公开成人人可用的library,将这些共用的部分抽取出来,绝对是一件再正常不过的事情;而抽出来的部分,就是Angular CDK

Angular CDK Slide 2     

Why CDK?

但是我们为什么需要使用Angular CDK呢?毕竟就算不使用Angular Material,我们还有很多现成的元件库可以使用不是吗?更不用说不是每个人都需要Material Design了!如下图,已经有许多知名的Angular元件库可以使用:

Angular CDK Slide 3     

但是,尽管有很多现成好用的元件库,在专案中我们几乎不可能避免要依照需求设计自己的元件库

Angular CDK Slide 4     

也因此,我们需要一套library,它不需要华丽的元件,但要能够提供各种模组,来帮助我们依照需求来打造各式各样的元件

Angular CDK Slide 5     

基于以上原因,Angular Material团队便将他们开发过程中共用的部分,提炼出一个共用的类别库,来解决上面提到的问题,这也就是Angular CDK啦!

Angular CDK目前拥有的功能

在Angular Material的文件中,上方有一个CDK的连结,点进去可以看到目前Angular CDK主要分成两大类,分别是Common Behaviors和Components

Angular CDK Document     

Common Behaviors内的功能

Common Behaviors主要是一些常见的互动需求,这里面的内容通常不会直接影响画面或元件的呈现,但却与它们的行为息息相关。

Common Behaviors     

目前包含了:

  • Accessibility:包含了一系列的工具,让元件的操作更加容易,也更容易让萤幕阅读器的功能理解。

  • Observables:主要是替基于web平台提供的observers提供一层包装,让使用上更加容易。

  • Layout:打造响应式网页(RWD)     ( 响应式网页设计(RWD)   Responsive Web Design ) 必备的一套工具,用来判断目前浏览器配置的变化,以回应不同的呈现需求。

  • Overlay:提供一些方法来在萤幕上呈现一个操作画面(panel),是dialog类型元件的核心。

  • Portal:提供我们在呈现template或component上更加弹性的功能;对于需要动态载入的功能非常有用。

  • Bidirectionality:主要用来处理RTL和LTR变化。

  • Scrolling:针对卷轴卷动时的互动,提供了一些处理方法。

Components内的功能

Components内主要就是在设计些常用元件时的辅助directive,替我们的元件直接加上某个功能。

CDK Components     

目前包含:

  • Table:方便我们建立一个data table。

  • Stepper:方便我们建立一个精灵功能。

在Angular Material中我们的Table和Stepper就是建立在这两个Angular CDK的directive在进行扩充,因此这两个功能我们几乎可以说是已经学习过了,因此之后的文章不会介绍,不过直接上文件去看,在比对一下Angular Material本身的元件介绍,要上手也是很容易的!

本日小结

今天我们简单的对Angular CDK做了介绍,也把目前Angular CDK所能够提供的功能部分大致说明了一下;读着们可以想想同样的类似的功能,若自己要写程式实作需要花多少时间,就能理解若有人帮我们都设计好,只要直接使用是多么方便的一件事情!

在之后介绍每个功能时,也可以一样的想想这样的功能,不使用CDK我们要写出多少程式码,相信会对于Angular CDK有更加的印象深刻。

明天开始,我们就来介绍这些强大的CDK吧!

顺便置入一下笔者前阵子在线上Angular读书会介绍Angular CDK的影片,虽然有些内容不太正确,但不会影响对Angular CDK强大这个事实的理解!

线上读书会- Angular CDK

https://www.youtube.com/watch?v=ZxY3QoGkLhQ

相关资源

来自   https://ithelp.ithome.com.tw/articles/10196968



[Angular Material完全攻略] Day 26 - Angular CDK(2) - Accessibility


今天我们要来介绍第一个Angular CDK的分类功能-Accessibility。Accessibility(简称A11y)主要是放置一些方便与使用者互动的功能,以及让我们在使用萤幕阅读器时更加方便的工具。我们将介绍里面几个有趣的功能!

开始使用Angular CDK的Accessibility

记得要先从@angular/cdk/a11y中加入A11yModule

import { A11yModule } from '@angular/cdk/a11y';

@NgModule({
  exports: [
    A11yModule
  ]
})
export class SharedMaterialModule {}






ListKeyManager

ListKeyManager是用来管理一组元件,让这些元件可以跟键盘互动,对于跟键盘互动,大部分最容易想到的就是如TabEscEnter方向鍵这些操作,但不是每个元件都支援这样的键盘互动功能,而我们则可以透过使用ListKeyManager,来让我们的元件轻易地达到跟键盘互动的成果!

使用ListKeyManager的基本要求

ListKeyManager是一个基础类别,目前他有两个衍伸:

  • FocusKeyManager:用于切换不同元件间在浏览器里的focus状态。这些元件需要实作以下interface:

    interface FocusableOption extends ListKeyManagerOption {
      focus(): void;
    }
    

           

           

           

           

       

  • ActiveDescendantKeyManager:用来切换active的样式,主要是在使用萤幕阅读程式时使用。

    interface Highlightable extends ListKeyManagerOption {
      setActiveStyles(): void;
      setInactiveStyles(): void;
    }
    

           

           

           

           

       

而这两个interface都继承自一个ListKeyManagerOption,它的内容如下:

interface ListKeyManagerOption {
  disabled?: boolean;
  getLabel?(): string;
}






也就是说,当我们希望自订的元件可以被ListKeyManager使用时,至少需要实作对应interface的功能,不过,ListKeyManagerOption的内容不是必须的。

举例来说,当我们要使用FocusKeyManager时,所有包含在内的元件都必须要有个focus()方法,但ListKeyManagerOption的内容则不是必要的。

而一般来说,要使用ListKeyManager功能有3个步骤:

  1. 使用@ViewChildren查出画面上需要包含在内的元件

  2. 建立一个新的ListKeyManager,并把上一步查出来的清单当参数传入

  3. 使用相关的键盘事件及设定状态的方法,来达到互动效果。

接下来我们使用FocusKeyManager来做示范,看看该如何使用吧!

使用FocusKeyManager

我们将以之前建立的问卷调查页面作为范本,让一些表单元件可以用不一样的方式来切换focus状态!

建立一个通用的directive并实作focus()方法

由于需要使用@ViewChildren来取得一系列包含focus()方法的元件,最简单的方式是建立一个directive,并实作ListKeyManagerOptionfocus()方法:

@Directive({
  selector: '[appSurveyInput]'
})
export class SurveyInputDirective implements FocusableOption {
  constructor(private element: ElementRef) {}

  focus() {
    this.element.nativeElement.focus();
  }
}






将要加入FocusKeyManager的元件加上自订的directive

接着我们把要互动的元件都加上刚刚建立的directive,如下:

<mat-form-field floatLabel="auto" [hideRequiredMarker]="true" hintLabel="最多輸入5個字">
  <input name="name" matInput formControlName="name" maxlength="5" required appSurveyInput>
</mat-form-field>
...






使用@ViewChildren查出元件并加入FocusManager

接着我们使用@ViewChildren找出前面加入的SurveyInputDirective,并取得一个QueryList,然后在ngAfterViewInit中把这个QueryList加入FocusManager

@Component({ })
export class SurveyComponent implements AfterViewInit {
  @ViewChildren(SurveyInputDirective) surveyInputs: QueryList<SurveyInputDirective>;
  keyManager: FocusKeyManager<SurveyInputDirective>;
  
  ngAfterViewInit() {
    this.keyManager = new FocusKeyManager(this.surveyInputs).withWrap();
    this.keyManager.setActiveItem(0);
  }
}






我们使用了new FocusKeyManager(this.surveyInputs).withWrap()建立新的FocusKeyManager,并加上withWrap(),这会让我们的focus状态成为一个循环,也就是当目前focus的是最后一个元件时,使用程式设定要跳到下一个元件时会回到第一个元件!

接着我们就可以透过keyManager来操作所有包含SurveyInputDirective的focus状态啦!例如上面程式我们使用了setActiveItem(0),代表在一开始产生元件时,就会立刻将第一个元件focus起来。

成果如下:

重点经理

在我们重新整理后,预设就会将第一个包含SurveyInputDirective的元件给focus起来!

为什么要放到ngAfterViewInit这是因为在ngOnInit时,并没有任何画面,所以相关的元件都还没产生,这时候使用@ViewChildren查出来的QueryList一定会是空的,因此要在ngAfterViewInit,所有元件都产生后,才加入。    

与键盘互动

接着让我们试试更复杂的玩法,透过键盘事件切换focus状态。

import { UP_ARROW, DOWN_ARROW } from '@angular/cdk/keycodes';

@Component({ })
export class SurveyComponent implements OnInit, AfterViewInit {
  ...
  
  @HostListener('keydown', ['$event'])
  keydown($event: KeyboardEvent) {
    // 監聽鍵盤事件並依照按鍵設定按鈕focus狀態
    if ($event.keyCode === UP_ARROW) {
      this.keyManager.setPreviousItemActive();
    } else if ($event.keyCode === DOWN_ARROW) {
      this.keyManager.setNextItemActive();
    }
  }

  ...
}






上面程式我们监听了keydown事件,并依照输入的keyCode来决定要使用setPreviousItemActive()回到上一个,还是使用setNextItemActive()往下一个focus,而UP_ARROW (向上箭头键)DOWN_ARROW   (向下箭头键)则是Angular CDK预先设定好的几种常用的键盘keyCode。内建的keyCodes可以参考Angular CDK的原始码

上面程式执行结果如下:

使用Keydown进行重点键管理

虽然用图示可能看不太出来,但这里我们试用上下键来接换元件的focus状态的,同时因为刚才有加入withWrap()的关系,所以当setNextItemActive()却没有下一笔元件可以focus时,就会切换到第一笔啰!

这样一个切换来切换去的复杂功能,透过Angular CDK的FocusKeyManager,只要十几行就搞定,实在太强啦!

CdkFocusTrap

接下来我们来聊聊CdkFocusTrap,在之前介绍dialog时,我们曾经看到过dialog内的表单,在使用Tab / Shift + Tab切换时,是不会跳到dialog之外的,这样的功能,我们可以透过FocusTrap提供的一系列directives,来达到目标,我们会使用到以下几个directives:

  • cdkTrapFocus:用来形成一个FocusTrap区间,一般情况下,使用Tab将无法跳出这里。

  • cdkFocusRegionStart:FocusTrap的范围起点。

  • cdkFocusRegionEnd:FocusTrap的范围终点。

  • cdkFocusInitial:区间出现时,一开始会focus的来源。

这里有几项我们需要注意

  1. CdkFocusTrap主要是用在非静态的区间,也就是由程式逻辑判断产生的,当需要产生时,就会预设进入FocusTrap的范围内

  2. 我们依然可以用在静态的区间,但会产生一个问题,当Tab进这个区间时,会从cdkFocusRegionEnd开始,而非cdkFocusRegionStartcdkFocusInitial这是目前已知的问题。

来看看程式码吧:

<h2>FocusTrap</h2>
<input value="cdkTrapFocus外的Input">
<div cdkTrapFocus>
  <input value="1" cdkFocusInitial>
  <input value="2" cdkFocusRegionStart>
  <input value="3">
  <input value="4" cdkFocusRegionEnd>
</div>






从directive的规划来看,我们focus的顺序应该会是外面 -> 1 -> 2 -> 3 -> 4 -> 2 -> 3 -> 4 …

结果如下:

FocusTrap-沐浴

很怪异吧!结果竟然是外面 -> 4 -> 2 -> 3 -> 4 -> 2 -> 3 -> 4 …,这就是我们前面提到的,FocusTrap目前主要是用在非静态的区间,当他是静态的时候,Tab焦点进来时会自动跑到cdkFocusRegionEnd,需要特别小心!

我们来改一下程式码,让这段FocusTrap变成动态出现的!

<h2>FocusTrap</h2>
<button (click)="displayFocusTrap = !displayFocusTrap">顯示輸入項</button>
<input value="cdkTrapFocus外的Input">
<div cdkTrapFocus *ngIf="displayFocusTrap">
  <input value="1" cdkFocusInitial>
  <input value="2" cdkFocusRegionStart>
  <input value="3">
  <input value="4" cdkFocusRegionEnd>
</div>






结果如下:

FocusTrap-bath2

当动态产生时,预期value="1"的input会被立刻focus,但看起来是没有,结果又回到原来的问题了,怎么会这样呢?再追查Angular CDK的程式及范例后,发现了一个文件范例程式没有说明 (晕倒)的cdkTrapFocusAutoCapture属性,原来需要加上这个才会正常:

<h2>FocusTrap</h2>
<button (click)="displayFocusTrap = !displayFocusTrap">顯示輸入項</button>
<input value="cdkTrapFocus外的Input">
<!-- 多了cdkTrapFocusAutoCapture -->
<div cdkTrapFocus *ngIf="displayFocusTrap" cdkTrapFocusAutoCapture="true">
  <input value="1" cdkFocusInitial>
  <input value="2" cdkFocusRegionStart>
  <input value="3">
  <input value="4" cdkFocusRegionEnd>
</div>






结果如下:

FocusTrap-好

虽然文件还不太齐全,但只要好好地善用这些directives,真的可以帮助我们节省很多程式码哩!

接下来我们来介绍一些其他的功能,有兴趣的读者也可以自己玩玩看啰。

FocusTrapFactory

我们可以使用cdkFocusTrap来产生一个focus区间,而FocusTrapFactory则是真正背后在做事的service,包含我们一直提到的dialog,也是使用FocusTrapFactory来产生focus区间的!以下简单撷取MatDialogContainer的部分程式,来看看FocusTrapFactory该如何使用:

export class MatDialogContainer {
  constructor(
    private _elementRef: ElementRef,
    private _focusTrapFactory: FocusTrapFactory){ }
    
  private _trapFocus() {
    if (!this._focusTrap) {
      this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
    }
  }
  
  _onAnimationDone(event: AnimationEvent) {
    if (event.toState === 'enter') {
      this._trapFocus();
    } else if (event.toState === 'exit') {
      this._restoreFocus();
    }
  }
}






很好理解吧!只要使用create方法,将要包起来的范围丢进去,就自动形成一个focus区间啦!

FocusMonitor

FocusMonitor是一个用来检查元件被focus的service,我们可以透过这个service来监看某个元件的focus状态,以及他被focus的来源是什么(touch、mouse、keyboard、program),简单的程式如下:

  startMonitor() {
    console.log('start monitoring element');
    this.focusMonitor.monitor(this.container.nativeElement, this.renderer2, false).subscribe(mode => {
      console.log('element focused by ${mode}');
    });
  }

  stopMonitor() {
    this.focusMonitor.stopMonitoring(this.container.nativeElement);
    console.log('stop monitoring element');
  }






cdkMonitorElementFocus / cdkMonitorSubtreeFocus

刚刚介绍的FocusMonitor会需要写比较多程式,如果我们只需要针对不同的focus状态来改变样式的时候,可以使用cdkMonitorElementFocuscdkMonitorSubtreeFocus这两种directive,差别是:

  • cdkMonitorElementFocus:只有自己被focus时有用

  • cdkMonitorSubtreeFocus:当里面的元素被focus时也有用

<div class="demo-focusable" tabindex="0" cdkMonitorElementFocus>
  <p>div with element focus monitored</p>
  <button>1</button><button>2</button>
</div>

<div class="demo-focusable" tabindex="0" cdkMonitorSubtreeFocus>
  <p>div with subtree focus monitored</p>
  <button>1</button><button>2</button>
</div>






当加上directive的元素被focus时,会自动加上一个CSS class:cdk-focused,方便我们针对focus状态设计样式。

另外,针对不同的focus来源,也会加上以下CSS class:

  • cdk-mouse-focused

  • cdk-keyboard-focused

  • cdk-program-focused

  • cdk-touch-focused

如此一来我们就可以根据不同的状况来设计样式,是不是整个超有弹性的啊!

互动检查器

InteractivityChecker是一个用来检查元件互动性的service,透过这个service,我们可以得知某个元件是否支援浏览器的某种互动,包含以下检查:

  • isDisabled:是否可以加上disabled属性

  • isFocusable:是否可以focus

  • isTabbable:使用tab是否可以碰到目标

  • isVisible:是否在画面上可以被看到

程式范例如下:

export class AppComponent implements OnInit, AfterContentInit {
  @ViewChild('button') button: ElementRef;

  constructor(private interactivityChecker: InteractivityChecker) {}

  ngOnInit() {
    console.log(this.interactivityChecker.isDisabled(this.button.nativeElement));
    console.log(this.interactivityChecker.isFocusable(this.button.nativeElement));
    console.log(this.interactivityChecker.isTabbable(this.button.nativeElement));
    console.log(this.interactivityChecker.isVisible(this.button.nativeElement));
  }
}






在需要与不同元件互动时,透过InteractivityChecker先检查一下,就不怕互动错啦!

现场播音员

这是在萤幕阅读器上使用的一个service,可以让萤幕朗读器(Windows)或Voice Over(Mac)念出指定的文字:

@Component({ })
export class SomeComponent {

  constructor(liveAnnouncer: LiveAnnouncer) {
    liveAnnouncer.announce("Hey Google");
  }
}






针对需要支援相关无障碍功能的网站来说,也是个不错的工具哩!

本日小结

今天我们针对Angular CDK的A11y功能做了一个大致的介绍,里面包含了不少帮助我们与画面互动的功能,很多的比例是针对focus状态的处理;同时我们也看到A11y提供许多互动性的检查,甚至针对萤幕朗读器或Voice Over这类无障碍网页的解决方案也提供了相关的service。这可是其他元件library很少注重的项目!只能说Angular CDK太贴心啦!

本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-26-cdk-accessibility

分支:day-26-cdk-accessibility

相关资源

来自   https://ithelp.ithome.com.tw/articles/10197071



普通分类: