欢迎各位兄弟 发布技术文章
这里的技术是共享的
昨天我们轻松地介绍两个与进度有关的元件,今天让我们稍微精实一点,来介绍一下写程式多于写HTML的Dialog,不管在不在SPA架构,Dialog都是经典且极为重要的元件,也因此我们会比较多的时间来介绍,好让读着们能完全的掌控Angular Material中的Dialog使用方式!
在Material Design的Dialogs设计指南中,Dialog的作用是用来提醒使用者需要进行的一些特定工作,同时可能包含了重要的提示讯息,或是需要做一些决定等等。因此我们会有非常多机会在里面放是表单元件,或是特性讯息等等。
Dialog的主要几个常见用途如下:
产生提示:用来立即的中断使用者目前的行为,并告知使用者目前的状况或所需要知道的资讯等等。
简易的选单:提供一些基本选项让使用者选取。
确认用:需要使用者明确的进行一个确认性的选择。
Dialog可以说是很基础的元件,也可以说是让画面呈现变得更加有立体感的关键,例如我们过去介绍的Datepicker、Select、Menu等等,都可以说是Dialog的一种应用结果。
要使用Dialog,当然我们必须加入MatDialogModule
,接着我们就可以来设计一个简单的Dialog元件。
Dialog不像是其他Angular Material元件,只要单纯的使用即可,需要一些比较复杂的动作,但其实也不是说多困难,让我们一步一步来说明:
我们先单纯建立一个元件AddPostDialogComponent
元件,不改变任何内容
ng g c dashboard/blog/add-post-dialog
接着用一个按钮,希望这个按钮按下后可以显示Dialog
<button mat-raised-button color="primary" (click)="showAddPostDialog()">新增文章</button>
在对应的component.ts中,注入MatDialog
这个Service
import { MatDialog } from '@angular/material';
export class BlogComponent {
constructor(public dialog: MatDialog) {}
}
使用这个MatDialog
的实体,打开Dialog
showAddPostDialog() {
this.dialog.open(AddPostDialogComponent);
}
由于这种方式是动态产生元件的,因此我们需要在所属Module中的entryComponents
中加入要产生的component
@NgModule({
...
entryComponents: [AddPostDialogComponent]
})
export class DashboardModule {}
虽然看起来步骤比较多,但其实只有两个重点:
建立要当作Dialog的component
使用注入的MatDialog
实体把它打开
这种步骤理解并习惯了就很快啰。接着就让我们直接来看看结果吧!
有没有很感动啊!一个基本的Dialog就浮现在我们面前啦!我们这时候可以透过点击灰底(backdrop)的部分来关闭dialog。
在dialog中这个灰底的部分称为
backdrop
,我找不到比较好的翻译,因此之后依旧会直接使用backdrop来称呼它。
在MatDialogModule
中,定义了几个重要的directives,这些directives可以帮助我们丰富dialog里面的内容,同时还能够减少一些不必要的程式码,让我们简单来介绍一下:
代表的是一个dialog的标题部分,尽管因为dialog的内容高度太长而造成卷动,依然会固定在整个dialog的最上方。
代表一个dialog的内文部分,当内容长度超过dialog可以容纳的高度时,就会变成可以卷动的模式。
用来放置行动按钮的区块,呈现位置刚好与mat-dialog-title
相反,会固定在画面的最下方,我们会在这里放置一些如确认、取消的按钮。
只允许在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就诞生啦!除了依照title、content和actions切割空间之外,按下取消的按钮就能够关闭dialog,另外当高度超过可以自动延展的范围(Angular Material中的dialog设定为65vh
)时,就会变成可以卷动的状态。
读者有兴趣可以实际试玩看看这个dialog的效果,可以看到以下几个亮点:
dialog显示时,预设会focus到第一个表单控制项。
当使用tab / shift + tab
切换focus状态时,永远不会跳出dialog的范围,只会在dialog内移动。
不只按下取消按钮可以关闭dialog,按下ESC
键也可以。
以上特色我们在未来介绍Angular CDK时,都可以透过Angular CDK来帮助我们在自己设计的元件中达到一样的功能!在这边就先不多做说明啰。
###关于MatDialog Service
在一开始介绍如何打开一个dialog时,我们注入了MatDialog
这个Service,接下来我们来详细介绍一下这个Service的属性及方法:
MatDialog有3个属性:
afterAllClosed
:Observable<void>
,会在所有画面上的dialog都被关闭时,才会触发的一个Observable,从这样的说明应该可以发现:没错,Dialog是可以开多个的!只需要在任何时候使用MatDialog.open()
方法即可
afterOpen
:Observable<MatDialogRef<any>>
,每当一个dialog开启时,就会触发一次,并告知目前开启的dialog
openDialogs
:MatDialogRef<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的程式就不多做说明了,只需要呼叫注入的MatDialog
的open()
方法,就会自然而然地开一个
新的dialog。
我们直接来看看log显示的结果:
MatDialog有3个方法,可以让我们自由自在地控制dialog:
closeAll
:顾名思义,就是关闭所有的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
型别设定一些dialog打开时的细节,由于属性众多,以下挑几个个人觉得重要的来介绍:
超重要,我们不可能永远只是单纯地打开一个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);
}
}
成果如下:
当dialog打开时,是否要自动focus在第一个控制项
我们可以为每个dialog自定一个唯一的id
我们可以使用height
、width
、minHeight
、minWidth
、maxHeight
和maxWidth
来设定dialog的尺寸资讯,除了height
和width
一定要用字串表示外,其他属性可以给予数值,当给予数值而非字串时,预设的单位为px
。
是否要使用一个灰色的底来隔绝dialog与下面的画面,也就是backdrop,如果设定为false
则依然可以和dialog后面的元件互动。
showAddPostDialog() {
this.dialog.open(AddPostDialogComponent, {
hasBackdrop: false
});
}
结果如下:
可以设定backdrop的样式,不太常使用,若对灰色底不满意时,可以进行调整,预设样式如下:
background: rgba(0,0,0,.6);
可以设定top
、bottom
、left
和right
,来决定dialog显示的位置。
预设情况下我们可以使用ESC
键关闭dialog,透过设定disableClose
为true
,可以取消这个功能,但要注意可能影响使用者的使用经验。
所有的dialog开启后,都会产生一个对应的MatDialogRef<T>
,其中的T
代表实际产生的component或templateRef,取得这个DialogRef的方式很多,主要有
使用MatDialog
的open()
时,回传的值,例如以下程式范例,可以透过取得开启对应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按下確認按鈕了');
});
}
使用MatDialog
的getDialogById
取得
在我们要当作dialog的component中,注入取得
@Component()
export class AddPostDialogComponent {
constructor(private dialogRef: MatDialogRef<AddPostDialogComponent>)
move() {
this.dialogRef.updatePosition({
top: '0',
left: '0'
});
}
}
结果如下:
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
你好:
想请教一个问题,是否可在dialog打开后,尚可点击gialog外的按钮,比如是header的button,因为我想做一个截截图功能,万一使用者觉得打开dialog有问题,可以透过系统内建的截图功能直接回报问题,感谢你
dialog 有一个hasBackdrop 可以设定,设定后就可以按dialog 之下的元素,若希望按其他元素不要被关掉,可以设定disableClose
可以参考dialog可以用的属性
https://material.angular.io/components/dialog/api#MatDialogConfig
谢谢!但这样有一个缺点,万一使用者按到转换另一个页面时,该dialog 还是停留着,有点唐突
这也很好解决,在页面元件的ngOnDestory()
时关掉就好了
如何改变在同一页面上开启的多个Dialog 的z-index?
例如有2个Dialog, 分别为A和B. 最初A置于B后面, A的一部分的内容被B遮挡. 当点击A后, A移到B的前面.
能否实现此功能? 我查看了angular material 的doc都没有提到z-index.
我发现使用addPanelClass/removePanelClass 只能改到内层的Panel, Dialog的前后次序仍被外层的z-index限制.
在网上找到了一个方法, 取得dialog 的overlay-container
const overlayWrapper = this.dialogRef['_overlayRef'].hostElement;
overlayWrapper.classList.add('front-window');
感谢提供,这是个虽然不美但可以用的作法,希望之后Angular Material 可以考虑把overlayRef 转成公开属性XDD
来自 https://ithelp.ithome.com.tw/articles/10196190
今天是Angular Material部落格篇的最后一天,我们要一口气介绍三个元件,分别是Chip、Tooltip和Snackbar,其中Chip很适合用来当作类似标签的功能;而Tooltip和Snackbar则是用在不同的地方,作为提示时使用。
在Material Design的Chips设计指南中,Chip主要用来把复杂的实体分成多个小区块显示,像是联络人清单等等的资讯,就很适合用Chips存放。
Chip是可以被选择的,当被选择时,我们应该要能提供更多关于这个Chip的资讯;当然,既然可选择,应该也是要能够提供直觉的删除Chip的方法。
我们可以在加入MatChipsModule
后,使用<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>
结果如下:
简单到不行吧!接下来我们再看看还有什么其他的玩法吧!!
非常容易,只需要加上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-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。
预设的<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是可以被删除的,我们可以在<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-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的Tooltips设计指南中,Tooltip主要是用来作为提示讯息使用,会在使用者与某个UI发生互动如滑鼠移过去或focus等行为时显示;用来给予使用这一些提示文字。
要使用Tooltip,我们需要加入MatTooltipModule
,接着就可以直接来使用matTooltip
这个directive。
我们可以在任何元件上加入matTooltip
这个directive来决定tooltip的文字,如下
<mat-form-field>
<input matInput placeholder="標題" [(ngModel)]="title" matTooltip="替你的文章下一個漂亮的標題吧!" />
</mat-form-field>
成果如下:
我们可以透过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>
成果如下:
我们可以直接使用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>
成果如下:
我们可以使用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的Snackbars & toasts设计指南中,Snackbar是在画面最下方提供一个文字讯息,让使用者知道目前系统大致的状态。这种功能在Android中也叫toast。
SnackBar主要由一个MatSnackBar
service来控制显示,要使用这个service,必须要先加入MatSnackBarModule
。
不多说,直接上程式:
@Component({
...
})
export class AddPostConfirmDialogComponent implements OnInit {
constructor(..., private snackBar: MatSnackBar) {}
confirm() {
...
this.snackBar.open('已新增部落格文章', '我知道了');
}
}
成果如下:
我们只用了MatSnackBar
的open()
方法,第一个参数代表提示的讯息,第二个参数代表用来关闭讯息的按钮文字。另外还有第三个参数config?: MatSnackBarConfig
,用来做比较细部的微调,稍后我们再来说明。
我们也可以使用MatSnackBar
的openFromComponent
方法,把一个component当作是SnackBar要显示的对象,概念上跟昨天介绍过的Dialog非常像,下面程式我们自订了一个新的Component,HTML内容如下:
<button mat-icon-button (click)="closeSnackBar()">
<mat-icon align="right">cancel</mat-icon>
</button>
<h4>已新增部落格文章</h4>
<p>溫馨小提示:每30分鐘記得起來運動一下喔!</p>
接着在程式内,使用MatSnackBar
的dismiss()
方法,将SnackBar关闭
@Component({
...
})
export class AfterPostNotifyComponent implements OnInit {
constructor(private snackBar: MatSnackBar) {}
closeSnackBar() {
this.snackBar.dismiss();
}
}
成果如下:
记得自订的component要加入entryComponents中。
MatSnackBar
的open()
或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的概念后,这里应该很容易理解才对吧!
设定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
刚刚踩雷了,赶快留言给日后的人参考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
今天开始我们又要进入一个新的页面-「收件夹」啰!先从比较轻松的Expansion Panel开始,我们会使用这个元件在画面左边建立一个类似收件夹大纲/分类的清单;透过Expansion Panel我们可以展开/收合不同类型的资料,在浏览及管理上都非常方便,就让我们继续看下去吧!
在Material Design的Expansion panels设计指南中,Expansion panels可以用来将一系列的动作或资讯分门别类放在不同的panels中,可以透过展开与收合某个panel来显示/隐藏资讯,收合时我们可以看到这panel的基本资料,展开后则可以看到或编辑详细的资讯。这样的概念有点像之前介绍过的Steppers,但又没有那么重的“步骤”的概念。
Expansion panels的相关功能都放在MatExpansionModule
之中,加入后我们就可以开始使用<mat-expansion-panel>
来建立一个基本的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>
结果如下:
很简单吧!只要点一下panel的标题,就可以立即展开/收起每个panel啰。
在标题<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>
成果如下:
我们也能够在<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的header部分,右方都会有一个展开/收合的按钮,如下:
我们可以透过设定hideToggle属性,把这个按钮藏起来,毕竟我们其实点击标题就可以执行展开/收合的动作了。
<mat-expansion-panel hideToggle="true">
...
</mat-expansion-panel>
成果如下:
在使用<mat-expansion-panel>
时,预设是把内容都收起来的,我们可以透过expanded
属性,动态的决定<mat-expansion-panel>
是否要展开( true
),还是收合( false
)
<mat-expansion-panel expanded="true">
...
</mat-expansion-panel>
我们也可以为<mat-expansion-panel>
设定disabled
属性,若设定disabled="true"
,则会维持原来的状态,无法点击:
<mat-expansion-panel hideToggle="true" expanded="true" disabled="true">
...
</mat-expansion-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>
成果如下:
一个手风琴效果就完成啦!
使用手风琴效果时,panel之间会产生一个空隙,方便我们分辨每块的panel,如下图:
这个空隙可以透过设定displayMode="flat"
拿掉(预设值为displayMode="default"
):
<mat-accordion multi="false" displayMode="flat">
...
</mat-accordion>
成果如下:
今天我们学习了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
昨天我们介绍了Expansaion Panels,替收件夹的左边加上了基本的分类资讯,今天让我们来使用Angular Material的Tab元件,来把右边也填入些东西!
Tab元件可以说是许许多多UI都会用到的功能,使用上虽然简单,但也有许多不同的呈现模式,可以说是最简单也最复杂的元件之一,接下来就立刻来看看有些什么变化吧!
在Material Design的Tabs设计指南中,Tab提供了一个简易的方式来切换不同的画面;Tab使用页签的方式管理不同的画面,并且一次只会显示一个画面。
要使用Tabs元件,需要加入MatTabsModule
,之后我们可以使用<mat-tab-group>
与<mat-tab>
来建立tabs。
跟许多群组型的元件都差不多,我们可以使用<mat-tab-group>
作为一个最外层的tab容器,并在里面放置数个<mat-tab>
:
<mat-tab-group>
<mat-tab label="郵件列表">
郵件列表清單
</mat-tab>
<mat-tab label="系統設定">
系統設定表單
</mat-tab>
</mat-tab-group>
成果如下:
我们可以透过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>
成果如下:
Angular Material的tabs是可以透过键盘切换的,我们可以使用左右方向鍵
focus到不同的tab,再使用SPACE
或ENTER
确认切换动作,这时候我们可以使用focusChange
事件来得知focus状态的变更,另外也有selectedIndexChange
及selectedTabChange
事件:
<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显示的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>
成果如下:
当我们为<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>
成果如下:
当tab很多,但<mat-tab-group>
宽度不够时该怎么办呢?我们不用做任何设定,Angular Material都帮我们设计好了,tab标签不会因为过多而换行,破坏版面,而是会显示一个可以左右移动的按钮,方便我们切换tab:
mat-tab-goup
的颜色也是可以设定的,我们可以使用backgroundColor
来设定背景颜色,使用color
来设定focus的tab底部颜色,例如:
<mat-tab-group backgroundColor="primary" color="accent" ...>
...
</mat-tab-group>
成果如下:
预设的tabs都是显示在上方,但我们也能够设定headerPosition="below"
,让tab呈现在画面下方:
<mat-tab-group headerPosition="below" ...>
...
</mat-tab-group>
成果如下:
当舞们不希望某个<mat-tab>
能够被选取时,可以为它加上disabled
,变成不可选取的状态:
<mat-tab-group>
<mat-tab>
...
</mat-tab>
<mat-tab disabled="true">
...
</mat-tab>
</mat-tab-group>
成果如下:
设定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
目前我们已经把收件夹的基本画面都组好了,看起来如下:
明天我们将使用table元件,来把邮件清单给补起来,同时学习table多采多姿的组合技,敬请期待!
来自 https://ithelp.ithome.com.tw/articles/10196575
今天我们要来介绍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设计指南中,data table用来呈现多笔的资料列,在许多系统中的会使用到,我们能透过data table呈现资料,也能够进行资料的管理。
Data table基本上就是表格的呈现,只是比起传统HTML的表格,应该具备更多的功能,如分页、排序等等。
基本上大多数的data table,都需要3个主要部分:资料显示的主体、允许排序的标题及分页。在Angular Material中这三个功能分别放在MatTableModule
、MatSortModule
及MatPaginatorModule
,我们今天会一口气把这三个功能都介绍过,来完成一个基本功能完整的data table,因此我们可以先把这3个module都加到我们的前端专案中。
我们先从最基本的显示资料主体开始,使用到<mat-table>
这个元件;在Angular Material中,使用<mat-table>
与一般table的使用方式会略有不同,因此让我们一步一步的来完成
我们先把资料来源准备好
@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
属性是用来放置主要呈现资料的属性,其他属性我们之后的内容慢慢理解。
资料来源API:https://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>
接着在<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>
我们可以使用<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>
这时候我们已经可以看到标题列的呈现啰:
有了标题列之后,我们再来把资料列本身显示出来,透过<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>
、<tr>
和<td>
的习惯不太相同,所以笔者把程式码截图后,搭配图示解说,可以把以下图片与上面的步骤做对应,应该会比较好理解:
以上就是基本的data table资料的呈现方式,刚开始可能会不太适应,但用习惯之后,你会发现这种设计其实更加直觉,维护起来也会更加容易哩!
虽然是以表格的方式呈现,但在Angular Material的data table已经把资料拆成一块一块的,再透过flexbox排版组合,因此已经不像原生的HTML的
<table>
了,也同时代表我们能够有弹性的调整data table的呈现方式啰。
预设情况下,每个栏位的宽度都会被平均分配,但这不一定是我们需要的,已上述例子来说,「寄件人」其实不用那么宽,我们可以把空间省下来,至于该如何做呢?其实在每个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的样式调整,结果是否如我们预期呢?
成果如下:
果然完全照着我们的预期显示,完全不用担心跟原来习惯不同后会不会产生难以做细部调整的问题,真是太棒了!
接下来我们要加入另一个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。成果如下:
刚刚我们已经完成一个基本的分页了,不过目前这个分页有点问题,因为分页的对象是已经捞到前端的资料,因此捞出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;
});
}
成果如下:
我们打开开发人员工具(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;
});
}
结果如下:
当然,我们也一样可以透过把资料传递给后端的方式来进行排序,只需要在加入matSort
的来源(也就是<mat-table>
)加入一个matSortChange
事件即可,当使用者按下栏位排序时,会传入一个Sort
型别的变数,包含两个栏位:
active:选择要排序的栏位。
direction:包含asc
,desc
和空字串
,代表要如何进行排序。
<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;
});
}
}
程式码看起来有点多,但主要的部分就是把分页和排序都串在一起,然后再一次跟后端取得资料。
成果如下:
同样的打开开发人员工具(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功能,以及针对排序、分页功能做一些进阶的说明。
您好:
请问您,如果是要使用"前端资料"的方法,不管是分页、排序或搜寻,
如果table中有按钮可以跳转页面(例如进入明细),
那我要如何在返回时还能保留原来table中分页、排序、搜寻的状态呢?
您好,建议可以将搜寻结果或搜寻条件暂存起来,如localstorage 或记忆体内,在跳转回来时,检查是否有暂存的内容,有的话就显示,没有的话就重新查询:)
谢谢MIKE哥的回覆!!
另外请问您,用您的方法,我返回时pageIndex和pageSize都能取回正确的设定,但如果我加上了filter的条件,然后在ngOnInit()中加入
this.memberDataSource .filter = localstorage.getItem('filter')
显示会帮我直接产生搜寻结果,但是资料的总笔数却是原始笔数而不是搜寻后的笔数,不知道我该在哪修改?
谢谢您! !!
可以用this.length = this.memberDataSource.filteredData.length;
已解决,感谢您!
您好:
依照您的方法实作
但是他一直显示
ERROR TypeError: Cannot read property 'page' of undefined
看起来是抓不到分分页的元件,但正常是可以的,你可以把程式贴到stackblitz 上吗?比较好确认程式哪里有问题
Mike哥您好:
请问您,如下程式码
lineDataSource = new MatTableDataSource();
....
this.lineDataSource.data = this.onLineList;
我持续刷新this.lineDataSource.data的内容(大约一秒刷新一次),
那页面中如果有卷轴的话,我不管卷轴往上滚还是往下滚,页面中都会往下再多滚一次。
请问您有遇过这个问题吗?
来自 https://ithelp.ithome.com.tw/articles/10196731
昨天我们把一个data table的基础功能-「显示资料、分页、排序」都大致说明了一遍,今天我们来讲一些进阶的data table用法,以及分页和排序元件的补充说明;Angular Material中的分页和排序功能都很强,而且也不会和<mat-table>
绑死,在任何地方可以应用。
就让我们继续往下看吧!
要筛选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
让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代表这笔资料要显示,反之则不显示。
再来看看结果:
这时候就能够只针对标题的栏位筛选啰。
关于后端筛选,就更加简单了,不需要再使用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;
});
}
}
成果如下:
从筛选、分页到排序,3个愿望通通都满足啦!
在之前的范例中,我们都是针对<mat-table>
来进行排序,但其实matSort
和mat-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>
只要加上matSort
和mat-sort-header
就搞定啦!
使用mat-sort-header
会在内容旁边加上一个箭头符号,方便我们判别目前的排序状态,这个符号也是可以调整的,有以下几个属性可以设定:
arrowPosition
:箭头符号要放在文字的前面 ( before
)还是后面 ( after
)
disableClear
:是否允许取消排序,预设为false
,会以asc
-> desc
-> 無
轮流切换;若设成true
则只会在asc
和desc
之间切换。
start
:当按下排序时预设先显示的排序状态,预设为asc
,可以选择asc
或desc
。
以下程式会将栏位的排序规则改为,(1)把箭头放在前面、(2)无法取消排序和(3)预设先以desc
排序:
<mat-header-cell *matHeaderCellDef
mat-sort-header
arrowPosition="before"
disableClear="true"
start="desc">
日期
</mat-header-cell>
结果如下:
可以看到大致上都如我们预期的显示,唯一的问题是arrowPosition="before"
后,整个内容被推到右边了,这是因为flex设定的关系,从开发人员工具可以看到header cell的加上了一个mat-sort-header-position-before
样式如下:
不过这不是大问题,自行CSS调整一下即可:
.mat-sort-header-position-before {
justify-content: flex-end;
}
成果如下:
看起来就正常多啦!
<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>
中的文字内容都是英文的,包含上一页及下一页按钮,当滑鼠移过去时会呈现一个tooltip,如下:
当然这个文字我们也可以进行调整,只要在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 = '上一頁';
}
}
成果如下:
所有的文字都变成中文呈现啦!
今天我们介绍了另一个常在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,也能打造出具有互动感的元件,准备好迎接新的挑战吧!!
您好:
跟你请教一个问题,该如何让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
基本原理是一样的,但提供了完整的资料结构来储存
您好:
请问以下这段disableClear="true"是否能全域设定??
<mat-header-cell *matHeaderCellDef mat-sort-header disableClear="true">
日期
您好,disableClear
是matSortHeader的一个@Input
,所以应该是没有特别可以全域设定的地方,但您可以自己撰写一个新的directive继承matSortHeader,然后覆盖掉disableClear
的设定即可
好的,了解! 谢谢您!
你好:
如果资料栏位为数字,就不能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)
感谢版主:
回报一下测试的结果,使用你提供的方法会把大于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有作用?
还是我的写法有问题?
感谢
来自 https://ithelp.ithome.com.tw/articles/10196827
我们即将要迈入新的篇章-Angular CDK,我们今天先不来写程式,而是大致的把目前(5.0.0)Angular CDK的架构做一个整体的介绍,让读者们能先在心中有个蓝图,在未来学习Angular CDK应该会更有感觉!
CDK是component development kit的简写,顾名思义就是「用来开发元件的工具」,因此不难想像Angular CDK就是用来帮助我们开发各种元件的好用工具。建议读者可以先看过2017年在奥兰多的Angular Mix大会上的一段关于Angular CDK介绍的影片:
https://www.youtube.com/watch?v=kYDLlfpTLEA
当然,如果你不想看英文或没有时间看影片,也可以直接往下看,以下内容将会以上段影片及投影片为主要参考,来介绍Angular CDK。
故事是这样的,Angular Material团队在开发这套library时,对于品质有许多的坚持,这是我们都已经知道的事情;而在开发时,团队发现一件铁一般的事实:「许多元件都有部分的功能是共用的!」如下图:
就算不是如同Angular Material开发团队般拥有顶尖的人才,只要是有一定经验的开发人员,就算不公开成人人可用的library,将这些共用的部分抽取出来,绝对是一件再正常不过的事情;而抽出来的部分,就是Angular CDK。
但是我们为什么需要使用Angular CDK呢?毕竟就算不使用Angular Material,我们还有很多现成的元件库可以使用不是吗?更不用说不是每个人都需要Material Design了!如下图,已经有许多知名的Angular元件库可以使用:
但是,尽管有很多现成好用的元件库,在专案中我们几乎不可能避免要依照需求设计自己的元件库
也因此,我们需要一套library,它不需要华丽的元件,但要能够提供各种模组,来帮助我们依照需求来打造各式各样的元件。
基于以上原因,Angular Material团队便将他们开发过程中共用的部分,提炼出一个共用的类别库,来解决上面提到的问题,这也就是Angular CDK啦!
在Angular Material的文件中,上方有一个CDK的连结,点进去可以看到目前Angular CDK主要分成两大类,分别是Common Behaviors和Components:
Common Behaviors主要是一些常见的互动需求,这里面的内容通常不会直接影响画面或元件的呈现,但却与它们的行为息息相关。
目前包含了:
Accessibility:包含了一系列的工具,让元件的操作更加容易,也更容易让萤幕阅读器的功能理解。
Observables:主要是替基于web平台提供的observers提供一层包装,让使用上更加容易。
Layout:打造响应式网页(RWD) ( 响应式网页设计(RWD) Responsive Web Design ) 必备的一套工具,用来判断目前浏览器配置的变化,以回应不同的呈现需求。
Overlay:提供一些方法来在萤幕上呈现一个操作画面(panel),是dialog类型元件的核心。
Portal:提供我们在呈现template或component上更加弹性的功能;对于需要动态载入的功能非常有用。
Bidirectionality:主要用来处理RTL和LTR变化。
Scrolling:针对卷轴卷动时的互动,提供了一些处理方法。
Components内主要就是在设计些常用元件时的辅助directive,替我们的元件直接加上某个功能。
目前包含:
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强大这个事实的理解!
https://www.youtube.com/watch?v=ZxY3QoGkLhQ
来自 https://ithelp.ithome.com.tw/articles/10196968
今天我们要来介绍第一个Angular CDK的分类功能-Accessibility。Accessibility(简称A11y)主要是放置一些方便与使用者互动的功能,以及让我们在使用萤幕阅读器时更加方便的工具。我们将介绍里面几个有趣的功能!
记得要先从@angular/cdk/a11y
中加入A11yModule
import { A11yModule } from '@angular/cdk/a11y';
@NgModule({
exports: [
A11yModule
]
})
export class SharedMaterialModule {}
ListKeyManager是用来管理一组元件,让这些元件可以跟键盘互动,对于跟键盘互动,大部分最容易想到的就是如Tab
、Esc
、Enter
和方向鍵
这些操作,但不是每个元件都支援这样的键盘互动功能,而我们则可以透过使用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个步骤:
使用@ViewChildren
查出画面上需要包含在内的元件
建立一个新的ListKeyManager
,并把上一步查出来的清单当参数传入
使用相关的键盘事件及设定状态的方法,来达到互动效果。
接下来我们使用FocusKeyManager
来做示范,看看该如何使用吧!
我们将以之前建立的问卷调查页面作为范本,让一些表单元件可以用不一样的方式来切换focus状态!
由于需要使用@ViewChildren
来取得一系列包含focus()
方法的元件,最简单的方式是建立一个directive,并实作ListKeyManagerOption
的focus()
方法:
@Directive({
selector: '[appSurveyInput]'
})
export class SurveyInputDirective implements FocusableOption {
constructor(private element: ElementRef) {}
focus() {
this.element.nativeElement.focus();
}
}
接着我们把要互动的元件都加上刚刚建立的directive,如下:
<mat-form-field floatLabel="auto" [hideRequiredMarker]="true" hintLabel="最多輸入5個字">
<input name="name" matInput formControlName="name" maxlength="5" required appSurveyInput>
</mat-form-field>
...
接着我们使用@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的原始码。
上面程式执行结果如下:
虽然用图示可能看不太出来,但这里我们试用上下键来接换元件的focus状态的,同时因为刚才有加入withWrap()
的关系,所以当setNextItemActive()
却没有下一笔元件可以focus时,就会切换到第一笔啰!
这样一个切换来切换去的复杂功能,透过Angular CDK的FocusKeyManager,只要十几行就搞定,实在太强啦!
接下来我们来聊聊CdkFocusTrap,在之前介绍dialog时,我们曾经看到过dialog内的表单,在使用Tab / Shift + Tab切换时,是不会跳到dialog之外的,这样的功能,我们可以透过FocusTrap提供的一系列directives,来达到目标,我们会使用到以下几个directives:
cdkTrapFocus
:用来形成一个FocusTrap区间,一般情况下,使用Tab将无法跳出这里。
cdkFocusRegionStart
:FocusTrap的范围起点。
cdkFocusRegionEnd
:FocusTrap的范围终点。
cdkFocusInitial
:区间出现时,一开始会focus的来源。
这里有几项我们需要注意
CdkFocusTrap主要是用在非静态的区间,也就是由程式逻辑判断产生的,当需要产生时,就会预设进入FocusTrap的范围内
我们依然可以用在静态的区间,但会产生一个问题,当Tab进这个区间时,会从cdkFocusRegionEnd
开始,而非cdkFocusRegionStart
或cdkFocusInitial
。这是目前已知的问题。
来看看程式码吧:
<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 …
结果如下:
很怪异吧!结果竟然是外面 -> 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>
结果如下:
当动态产生时,预期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>
结果如下:
虽然文件还不太齐全,但只要好好地善用这些directives,真的可以帮助我们节省很多程式码哩!
接下来我们来介绍一些其他的功能,有兴趣的读者也可以自己玩玩看啰。
我们可以使用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是一个用来检查元件被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');
}
刚刚介绍的FocusMonitor会需要写比较多程式,如果我们只需要针对不同的focus状态来改变样式的时候,可以使用cdkMonitorElementFocus
或cdkMonitorSubtreeFocus
这两种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