欢迎各位兄弟 发布技术文章
这里的技术是共享的
今天我们来讲Angular CDK中两个跟版面配置有关的功能,分别是Bidirectionality、Layout。
Bidirectionality主要是用来调整LTR(Left To Right)跟RTL(Right To Left)配置及侦测的工具。
而Layout则是用来侦测浏览器可用的宽度与高度,来判断目前网站使用在什么样的平台上,如果不使用任何其他的RWD工具,Layout可是Angular CDK中实现RWD不可或缺的帮手哩!
响应式网页设计(RWD) Responsive Web Design
要使用Bidirectionality(之后简称为bidi),需要加入BidiModule
:
import { BidiModule } from '@angular/cdk/bidi';
@NgModule({
exports: [
BidiModule
]
})
export class SharedMaterialModule {}
Bidi模组提供了一个dir
的directive,方便我们改变排列方式,也就是LTR和RTL的状态,举个例子,我们在目前画面的toolbar加上两个按钮,来切换LTR和RTL的状态:
<button mat-button *ngIf="bidiMode === 'ltr'" (click)="bidiMode = 'rtl'">LTR</button>
<button mat-button *ngIf="bidiMode === 'rtl'" (click)="bidiMode = 'ltr'">RTL</button>
画面如下:
当然,这时候按下去还没有任何反应,只是切换一个变数的资料而已,接着我们在需要改变排列方式,例如
<mat-sidenav-content [dir]="bidiMode">
...
</mat-sidenav-content>
这时候<mat-sidenav-content>
里面的内容就可以依照我们想要的去做配置啦!
使用dir
这个directive之后,会为我们的元件扩充一个dirChange
事件,我们可以透过这个事件得知目前bidi状态的改变
<mat-sidenav-content [dir]="bidiMode" (dirChange)="logDirChange($event)">
结果如下:
Directionality
是bidi
提供的一个service,它的功能非常简单,当某个component注入这个service后,我们就能够过它来知道目前component的排列是LTR还是RTL,当排列方现变更时,也能够过change
事件得知;我们先加入一个BidiTestComponent
来试试看:
<mat-sidenav-content [dir]="bidiMode" (dirChange)="logDirChange($event)">
<app-bidi-test></app-bidi-test>
...
</mat-sidenav-content>
接着在BidiTestComponent
中注入Directionality
,并侦测变化
@Component({ })
export class BidiTestComponent implements OnInit {
constructor(private directionality: Directionality) {}
ngOnInit() {
console.log(`目前dir: ${this.directionality.value}`);
this.directionality.change.subscribe((dir: Direction) => {
console.log(`component的dir被改變了: ${dir}`);
});
}
}
结果如下:
Layout可以帮助我们侦测浏览器大小的变化,进而让我们能依照不同的萤幕大小给予不同的呈现方式,我们须先加入LayoutModule
import { LayoutModule } from '@angular/cdk/layout';
@NgModule({
exports: [
LayoutModule
]
})
export class SharedMaterialModule {}
BreakpointObserver
是一个类似media query的萤幕大小侦测器。有两个主要的方法。
我们能使用isMatched()
并传入与media query一样的语法,来判断目前的萤幕是否与media query符合:
export class DashboardComponent implements OnInit {
constructor(private breakpointObserver: BreakpointObserver) {}
ngOnInit() {
const isSmallScreen = breakpointObserver.isMatched('(max-width: 599px)');
}
}
结果如下:
当我们萤幕比较大时,isSmallScreen
会是false
,而当把萤幕拉小重新整理后,就会看到isSmallScreen
变成true
啦!
上面使用isMatch()
虽然方便,但有时候我们的萤幕大小是动态的,这时候我们就可以使用observe()
来判断,我们可以加入多组的media query,当其中一个判断结果改变,就会得到目前状态:
this.breakpointObserver.observe('(orientation: portrait)').subscribe(result => {
console.log(`{portrait: ${result.matches}`);
});
this.breakpointObserver.observe('(orientation: landscape)').subscribe(result => {
console.log(`{landscape: ${result.matches}`);
});
结果如下:
Material Design已经有订出一些基本的breakpoints,而Angular Material也有把这些breakpoints也都考量进来了,包含了以下几个breakpoints:
Handset
Tablet
Web
HandsetPortrait
TabletPortrait
WebPortrait
HandsetLandscape
TabletLandscape
WebLandscape
我们可以直接用这些已经设定好的breakpoints,节省我们写media query的时间,如下:
this.breakpointObserver.observe([Breakpoints.HandsetLandscape, Breakpoints.HandsetPortrait])
.subscribe(result => {
console.log(`Handset: ${result.matches}`);
});
结果如下:
透过这种方式,我们就能够针对不同大小的装置,来决定画面该如何呈现了,例如我们之前曾经介绍过的Datepicker元件,我们可以在行动装置时,开启Touch UI模式;非行动装置的大小时就直接显示,这样在不同的画面上,都能以比较适合的方式呈现:
export class SurveyComponent implements OnInit, AfterViewInit {
isHandeset$: Observable<boolean>;
constructor(private breakpointObserver: BreakpointObserver) {}
ngOnInit() {
this.isHandset$ = this.breakpointObserver.observe(Breakpoints.Handset).map(match => match.matches);
}
}
画面上则针对isHandset$
来设定touchUi
属性:
<mat-form-field>
<input type="text" name="birthday" matInput placeholder="生日"
[matDatepicker]="demoDatepicker">
<mat-datepicker [touchUi]="isHandset$ | async"></mat-datepicker>
</mat-form-field>
结果如下:
透过Layout相关的service,要打造RWD的程式也不再是件难事啦!
响应式网页设计(RWD) Responsive Web Design
今天我们介绍了两个跟画面配置有关的功能。
Bidirectionality可以帮助我们设定LTR和RTL模式,也能对于模式的切换加以侦测;对于跨国网站来说,这可能会是影响客户来源的一大议题!
而Layout则是用来判断浏览器萤幕大小的变化,在不搭配其他library的情况下,善用Layout,可以让我们的网站符合RWD的精神,在各种不同大小的装置上都能给予最好的显示方式,让网站操作上更加方便!
响应式网页设计(RWD) Responsive Web Design
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-27-cdk-bidirectionality-layout
分支:day-27-cdk-bidirectionality-layout
来自 https://ithelp.ithome.com.tw/articles/10197159
今天我们要介绍两个比较简单的Angular CDK功能分类,分别是Observables和Scroll。这两个功能使用上非常简单,但乍看之下使用的机会不高,因此我们也会来稍微偷看一下Angular Material的原始码,看看有什么样让人意想不到的使用情境!
Angular CDK的Observables分类中,目前只有一个cdkObserveContent
,在加入ObserversModule
后就可以直接用啦
import { ObserversModule } from '@angular/cdk/observers';
@NgModule({
exports: [
ObserversModule
]
})
export class SharedMaterialModule {}
在自己设计元件时,为了方便客制化,我们常常会元件的内容交给使用元件的人去决定,而元件设计上则使用<ng-content>
把外面的内容放到里面的某个位置上,这时候我们可以使用cdkObserveContent
这个directive来得知<ng-content>
内容的变化,而这个cdkObserveContent
本身也是一个@Output
事件,签章如下:
@Directive({
selector: '[cdkObserveContent]',
exportAs: 'cdkObserveContent',
})
export class CdkObserveContent implements AfterContentInit, OnDestroy {
@Output('cdkObserveContent') event = new EventEmitter<MutationRecord[]>();
}
参考:https://github.com/angular/material2/blob/5.0.x/src/cdk/observers/observe-content.ts
我们简单的来设计一个元件,并使用看看,就知道变化了!
先建立一个CdkObserveContentDemoComponent
并在画面上加上cdkObserveContent
<div class="content-wrapper" (cdkObserveContent)="projectContentChanged($event)">
<ng-content></ng-content>
</div>
CdkObserveContentDemoComponent
对应的程式内容如下:
@Component({ ... })
export class CdkObserveContentDemoComponent {
count = 0;
projectContentChanged($event: MutationRecord[]) {
++this.count;
console.log(`資料變更,第${this.count}次`);
console.log($event, this.count);
}
}
MutationRecord
是dom的原生物件,可以参考MDN
来看看结果吧:
可以看到每次<ng-content>
内的资料变更时,我们也能立刻得知变化了!
cdkObserveContent
另外提供了一个debounce
参数,方便我们在大量变化时减少程式码执行的浪费,只有在边画发生后持续debounce
设定的时间内没有再次发生,才会触发cdkObserveContent
事件:
<div class="content-wrapper" (cdkObserveContent)="projectContentChanged($event)" debounce="1000">
<ng-content></ng-content>
</div>
成果如下:
刚按下去变更资料时,事件不会立刻触发,而是1秒内没有再次变更时,才会触发。
在Angular Material中,使用的情境目前有3个:Tab、Checkbox和SlideToggle。
应用上来说,大致上都相同,主要都是内容变更时,重新进行资料的调整,或是要求进行变更侦测等行为。
例如<mat-tab-nav>
的部分原始码内容:
<div class="mat-tab-links" (cdkObserveContent)="_alignInkBar()">
<ng-content></ng-content>
<mat-ink-bar></mat-ink-bar>
</div>
参考:https://github.com/angular/material2/blob/5.0.x/src/lib/tabs/tab-nav-bar/tab-nav-bar.html
一看就知道是内容异动时,要求把tab页签下针对active状态重新调整(ink bar)
或是以<mat-checkbox>
来说,可以看到以下的部分原始码内容:
<span class="mat-checkbox-label" #checkboxLabel (cdkObserveContent)="_onLabelTextChange()">
参考:https://github.com/angular/material2/blob/5.0.x/src/lib/checkbox/checkbox.html
而在程式中_onLabelTextChange()
做了什么呢?
/** Method being called whenever the label text changes. */
_onLabelTextChange() {
// This method is getting called whenever the label of the checkbox changes.
// Since the checkbox uses the OnPush strategy we need to notify it about the change
// that has been recognized by the cdkObserveContent directive.
this._changeDetectorRef.markForCheck();
}
参考:https://github.com/angular/material2/blob/5.0.x/src/lib/checkbox/checkbox.ts
简单的说就是因为效能关系,变更侦测的策略为OnPush
,所以在内容变更时,主动通知变更啦!
可以看到在打造高效能元件时,cdkObserveContent
也是很好的小帮手哩!!
在Angular CDK的Scrolling里面,只有一个cdkScrollable
directive和ScrollDispatcher
service,主要都是用来得知scroll是何时发生的,我们需要加入ScrollDispatchModule
:
import { ScrollDispatchModule } from '@angular/cdk/scrolling';
@NgModule({
exports: [
ScrollDispatchModule
]
})
export class SharedMaterialModule {}
cdkScrollable
这个directive在单独使用时,不会有任何感觉,它不会对付加上的元件产生任何变化,也没有任何的@Input()
或@Output
;虽然它能得知元件的scroll状态变更,但无法透过#someTemplate="cdkScrollable"
之类的方式管理,而实际上他做的事情很简单,只有两个:
监听scroll状态
把自己注册给ScrollDispatcher
当我们需要知道某个元件的scroll状态时,必须在元件上加入cdkScrollable
,之后再注入ScrollDispatcher
来管理,例如:
<mat-sidenav-content cdkScrollable>
...
</mat-sidenav-content>
之后在程式码中加入ScrollDispatcher使用:
@Component({ ... })
export class DashboardComponent implements OnInit {
constructor(private scrollDispatcher: ScrollDispatcher) {}
ngOnInit() {
this.scrollDispatcher.scrolled().subscribe((scrollable: CdkScrollable) => {
console.log('發生scroll了,來源為:');
console.log(scrollable.getElementRef().nativeElement);
});
}
}
结果如下:
当画面卷动时,就会自动得到一个讯息,并且得知是谁发生的啦!
ScrollDispatcher
的scrolled()
内也能加入一个auditTimeInMs
参数,代表的是停止卷动后多久,才触发事件,例如:
this.scrollDispatcher.scrolled(1000).subscribe((scrollable: CdkScrollable) => {
console.log('發生scroll了,來源為:');
console.log(scrollable.getElementRef().nativeElement);
});
结果:
除了元件本身的scroll状态之外,我们也能得知某个目标有加入cdkScrollable的祖先scroll状态,也就是以某个元件往祖先找,当祖先有cdkScrollable
且产生scroll时,就会发生事件,例如我们直接在某个元件中不加入cdkScrollable
,但在程式中直接使用ScrollDispatcher
的ancestorScrolled()
来得知外部有cdkScrollable
元件的状态:
@Component({ })
export class SomeChildComponent implements OnInit {
constructor(private scrollDispatcher: ScrollDispatcher, private elementRef: ElementRef) {}
ngOnInit() {
this.scrollDispatcher.ancestorScrolled(this.elementRef, 1000).subscribe((scrollable: CdkScrollable) => {
console.log('祖先發生scroll了,來源為:');
console.log(scrollable.getElementRef());
});
}
}
我们把目前元件的ElementRef
丢给ancestorScrolled()
,当祖先元件包含cdkScrollable
且发生scroll时,我也能够及时收到通知啦!
如果一定需要等到scroll发生时,才知道有什么祖先 显灵发生scroll,那未免也太没效率了,因此我们也能使用getAncestorScrollContainers()
取得包含cdkScrollable
的祖先。如下:
console.log(this.scrollDispatcher.getAncestorScrollContainers(this.elementRef));
这样操作上就能够更加主动灵活啰!
在Angular Material中,目前使用Scrolling的功能只有一个-Tooltip!
Tooltip在滑鼠移到目标上时才会显示,所以当卷动事情发生时,也变相等于滑鼠移开tooltip了,那么会发生什么事呢?tooltip消失也是很正常的,但实际上真的是如此吗?
实际上我们可以看到,当scroll发生时,tooltip竟然没有立刻消失!而是等了一小段时间才消失,这么一来我们就可以避免一卷动就看不到tooltip的问题,又不用担心tooltip一直在那里赖着不走 不消失,可以说是非常贴心的小功能。
而透过偷看Angular Material的程式码,?我们能够在Toolip中的程式找到一小段片段如下:
private _createOverlay(): OverlayRef {
const scrollableAncestors = this._scrollDispatcher.getAncestorScrollContainers(this._elementRef);
strategy.withScrollableContainers(scrollableAncestors);
}
参考:https://github.com/angular/material2/blob/5.0.x/src/lib/tooltip/tooltip.ts
这里用到的OverlayRef
会在后天介绍,但我们可以看到的是,Tooltip把包含cdkScroll
的祖先找了出来,虽然我们没仔细去看到里面的实作细节,但也不难猜出tooltip会侦测祖先cdkScroll
的状态,再来决定要不要立刻消失哩!
今天我们介绍了两个使用上不太容易有感觉的Angular CDK功能分类,分别是Observables和Scroll。这些功能都能让我们发现一些细微的变化发生,进而做一些细部的调整。虽然看起来使用的机会不高,但在打造高效能,注重细节的高品质元件时,这些可都是不可或缺的功能啊!
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-28-cdk-observables-scrolling
分支:day-28-cdk-observables-scrolling
来自 https://ithelp.ithome.com.tw/articles/10197285
接下来我们要介绍Angular CDK中的Portal分类,透过这里面的功能,我们可以很容易的动态切换各种不同的元件和templates,让动态产生内容不再是件麻烦的事情!
其实在Angular中,我们已经能够使用<ng-container>
搭配ngTemplateOutlet
或ngComponentOutlet
来切换不同的template或component了,而Angular CDK的Portal可以想像成是它的简单好用加强版!甚至我们可以透过Portals里面的功能,把元件放在整个Angular程式的控制范围之外,但元件依然还在Angular的控制内,非常的强大!
就让我们继续往下看吧!!
Angular CDK中的Portal类别中提供了一些方便的directives和services,用来产生动态的内容,使用前要先加入PortalModule
:
import { PortalModule } from '@angular/cdk/portal';
@NgModule({
exports: [
PortalModule
]
})
export class SharedMaterialModule {}
在使用Portal相关功能之前,有两个简单名词要先介绍:
Portal:真正要被切换的内容,这些内容会用Portal包起来,如果要切换的内容是一个template reference,则会使用TemplatePortal
;如果是元件(component),则使用ComponentPortal
。
PortalOutlet:实际放置内容的地方,如果Portal是插头,那么PortalOutlet就可以想像成是插座。
接着我们来实作一个简单的Tab功能,说明Portal如何动态切换内容!
我们先设计好要放置插座的地方,也就是PortalOutlet,这里我们设计了一个如下的画面
<div class="portal-demo">
<div class="tabs">
<button mat-button (click)="changePortal1()">功能1</button>
<button mat-button (click)="changePortal2()">功能2</button>
<button mat-button (click)="changePortal3()">功能3</button>
<button mat-button (click)="changePortal4()">功能4</button>
</div>
<div class="tab-content">
<div [cdkPortalOutlet]="currentPortal"></div>
</div>
</div>
其中的[cdkPortalOutlet]="currentPortal"
就是我们插座放置的位置,接着我们要在程式中宣告一下这个插头currentPortal
:
@Component({ ... })
export class MainComponent implements OnInit {
currentPortal: Portal<any>;
之后只要替换掉插头,就可以切换不同内容显示啰。
cdkPortal是一个简单的directive,其实就是TemplatePortal
衍生的directive版本,透过这个directive,我们可以很容易的透过@ViewChild
或@ViewChildren
取得画面上的portal。
cdkPortal
需要放在<ng-template>
里面:
<ng-template cdkPortal>
<p>
功能1:我放在ng-template + cdkPortal = TemplatePortal裡面
</p>
</ng-template>
当然,这种语法也能换成语法糖asterisk(*)的方式:
<p *cdkPortal>
功能2:我放在一般的HTML Element內,加上cdkPortal後變成了一個TemplatePortal
</p>
一般的<ng-template>
不要加上cdkPortal
可以吗?当然可以,只是需要再加工一下,我们先摆上来:
<ng-template #template>
功能3:我放在ng-template內,但不是TemplatePortal,要用我要記得先包裝一下
</ng-template>
cdkPortal的用法就这样,很简单吧!
接下来我们要在程式中动态切换cdkPortalOutlet
的内容,使用cdkPortal
的话,一切都简单很多:
@Component({ ... })
export class MainComponent implements OnInit {
@ViewChildren(CdkPortal) templatPortals: QueryList<CdkPortal>;
changePortal1() {
this.currentPortal = this.templatPortals.first;
}
changePortal2() {
this.currentPortal = this.templatPortals.last;
}
}
只要把加上cdkPortal
的直接设定就好啦!
至于一般的TemplateRef
该怎么办呢?只要把它用TemplatePortal包装起来就好啦!
@Component({ ... })
export class MainComponent implements OnInit {
@ViewChild('template') template3: TemplateRef<any>;
constructor(private viewContainerRef: ViewContainerRef) {}
...
changePortal3() {
// 使用TemplatePortal把一般的TemplateRef包裝起來
this.currentPortal = new TemplatePortal(this.template3, this.viewContainerRef);
}
}
这时候就可以来看看结果啦!
一个简单的tab功能就这样完成啦!
刚刚我们示范了一般的template放到PortalOutlet的方法,但template只会放在一般的HTML内,不像元件(component)那么容易被重复使用,有没有办法放置元件呢?答案当然是可以,只要把元件包装成ComponentPortal
就可以啰!
changePortal4() {
this.currentPortal = new ComponentPortal(Portal4Component);
}
温馨提醒:老样子,动态产生的元件记得加入entryComponents。
成果如下:
简单的不得了吧!
到目前为止,我们已经学会如何轻易且动态的切换内容,但是实务上常常未必那么简单,我们必须让切换的template或component能够理解外部的状态,因此必须传入一些外部资讯给里面的template或component,越复杂的程式越有可能需要这么做,那么要如何才能够加入其他内容呢?
在我们建立TemplatePortal
时,会需要3个参数,分别是:
template: TemplateRef<any>
:要传入的template的参考
viewContainerRef: ViewContainerRef
:画面上的ViewContainerRef
,来源可由注入取得
context?: any
:要传入的外部内容。
因此我们可以在自行建立TemplatePortal时,使用context
参数,决定要传入的资讯:
changePortal3() {
// 使用TemplatePortal把一般的TemplateRef包裝起來
this.currentPortal = new TemplatePortal(this.template3, this.viewContainerRef, { nameInObject: this.name});
}
这里我们在第3个参数context
传入了要加入的内容,接着在<ng-template>
上,我们就可以使用let-xxx="yyy"
的方式,来取得我们的资讯:
<ng-template #template let-nameInTemplate="nameInObject">
Hi,{{ nameInTemplate }},功能3:我放在ng-template內,但不是TemplatePortal,要用我要記得先包裝一下
</ng-template>
你可能会问为什么不直接用
{{ name }}
就好了,实际上这样是最快的方法没错,但有时候要传入的可能是整理后比较复杂的资料,不一定有一个现成的变数放在那边可以接,就会有需要啰。
let-nameInTemplate="nameInObject"
可以想像成是宣告成javascript的let nameInTemplate = context.nameInObject
,只是在画面上无法这样直接写而已。
结果如下:
如果是加入cdkPortal
的元素呢?只需要设定它的context
属性即可:
this.templatPortals.first.context = { nameInObject: this.name };
跟使用template不太一样的是,我们的第三个参数必须是一个Injector
,而在component时必须透过注入取得资讯,因此会比较复杂一点,我们接着来看看如何实作吧!
export const PORTAL4_INJECT_DATA = new InjectionToken<any>('portal4-inject-data');
这个PORTAL4_INJECT_DATA
就会是我们之后要注入资料的依据。
如以下程式,我们写了一个_createInjector
,来把我们想要注入的token,和当前的Injector
,合并成一个新的PortalInjector
@Component({ ... })
export class MainComponent implements OnInit {
name : string;
constructor(private viewContainerRef: ViewContainerRef, private injector: Injector) {}
...
private _createInjector(): PortalInjector {
const injectionTokens = new WeakMap();
injectionTokens.set(PORTAL4_INJECT_DATA, this.name);
return new PortalInjector(this.injector, injectionTokens);
}
}
之后,在建立ComponentPortal时,只需要把这个PortalInjector
也传入即可
changePortal4() {
const portalInjector = this._createInjector();
this.currentPortal = new ComponentPortal(Portal4Component, undefined, portalInjector);
}
建立ComponentPortal的第二个参数一样是ViewContainerRef
,但这只有在Angular的管理范围外建立动态元件的时候,需要一个来源ViewContainer
,如何在Angular范围外动态建立元件呢?稍后我们会在DomPortalOutlet
介绍。
所以这里我们不用传入,只要传入undefined
即可。
而在Component内,则可以直接透过注入PORTAL4_INJECT_DATA
,来取得对应的资料:
@Component({ ... })
export class Portal4Component implements OnInit {
get name() {
return this.data.nameInObject;
}
constructor(@Inject(PORTAL4_INJECT_DATA) private data: any) {}
ngOnInit() {
}
}
这时候画面上就可以直接使用啦!
我们也能在程式中直接控制cdkPortalOutlet要显示什么样的资料,原来的画面只要找得到cdkPortalOutlet
的reference就好:
<div cdkPortalOutlet #portalOutlet="cdkPortalOutlet"></div>
接着就可以在程式中直接使用啦!
@Component({ ... })
export class MainComponent implements OnInit {
@ViewChild('portalOutlet') portalOutlet: CdkPortalOutlet;
changePortal2() {
this.portalOutlet.attach(portalOutlet);
}
}
CdkPortalOutlet
有以下几个主要方法:
attach()
:附加一个Portal上去,可以是ComponentPortal,也可以是TemplatePortal
attachComponentPortal()
:附加一个ComponentPortal
attachTemplatePortal()
:附加一个TemplatePortal
deatch()
:把目前附加的Portal拿掉
hasAttached()
:用来检查目前是否有任何Portal被附加上去了
DomPortalOutlet
是一个很有趣的插座,他可以帮助我们把插座产生在Angular的管理范围内,以一般的Angular程式来说,就是<app-root>
之外,听起来很不可思议吧!立刻来看看该怎么做吧!
说穿了,DomPortalOutlet就是操作DOM来做些事情,以及把产生的内容丢到一个<app-root>
外的插座上,但其实它还是在管理范围内,只是不住在<app-root>
里面而已,因此在建立时,还是需要把可以管理他的范围界定起来,这些也是DomPortalOutlet要建立时所相依的类别:
@Component({ ... })
export class MainComponent implements OnInit {
domPortalOutlet: DomPortalOutlet;
constructor(
@Inject(DOCUMENT) private document: any,
private viewContainerRef: ViewContainerRef,
private injector: Injector,
private componentFactoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef
) {}
以上在建构式注入的,除了document以外,都是建立DomPortalOutlet
的必要条件,那么注入document
到底是用来干嘛的呢?我们要使用这个物件来建立插座。
因为超过<app-root>
的范围,因此伸手直接去摸DOM基本上是不可避免的,我们可以直接用document
来操作DOM,但在这里我们却另外注入了一个document的DOCUMENT token(有点饶舌),这是为什么呢?
一般情况下,我们注入的document
,在网页上其实就是window.document
,但Angular是一个可以跨不同平台的设计,因此到了其他平台,就不一定了,另外在撰写单元测试时,为了避免单元测试下只有JavaScript而没有DOM,中间垫了一层也是比较好的!也因为如此,虽然我们可以直接使用window.document
,但还是选择了使用注入的方式,来隔离相依。
有了这样的概念后,我们就来建立一个插座吧!
createOutletOutOfApp() {
const element = this.document.createElement('div');
element.innerHTML = '<br>我在<app-root>之外';
this.document.body.appendChild(element);
this.domPortalOutlet = new DomPortalOutlet(element, this.componentFactoryResolver, this.appRef, this.injector);
}
在这边的程式我们直接建立一个<div>
element,并把它附加到document.body
,也就直接脱离了<app-root>
,这是一般JavaScript的写法,指示document有被我们垫了一层而已。
之后我们再使用new DomPortalOutlet()
把这个element变成可以被管理的插座。
从这样的程式不难发现,我们不仅可以建立<div>
,也能透过getElementById()
等等的方式,把<app-root>
之外的某个现有HTML,直接变成插座,来插入我们想要的元件,光用想的就觉得潜力无穷啊!
建立插座后,要插入内容就简单多啦!我们可以使用attachTemplatePortal()
来插入TemplatePortal,或是使用attachComponentPortal()
来插入ComponentPortal,使用方法跟我们之前使用PortalOutlet
一模一样!
addTemplatePortal() {
this.domPortalOutlet.attachTemplatePortal(this.templatPortals.last);
}
直接来看看结果吧!
当我们按下产生插座时,在<app-root>
外产生了一个插座;按下插入内容后,就把我们想要的Portal插进来啦!
在Angular Material中的Dialog,显示dialog时会有一个backdrop,为了避免这个backdrop被其他元件画面干扰,设计时就是使用DomPortalOutlet的技巧,在<app-root>
范围外的一个Overlay上(关于Overlay,是另外一个Angular CDK功能,明天会介绍)建立插座,再把dialog放在上面,如下图:
简单的程式码片段如下:
@Injectable()
export class Overlay {
private _createPaneElement(): HTMLElement {
const pane = this._document.createElement('div');
pane.id = `cdk-overlay-${nextUniqueId++}`;
pane.classList.add('cdk-overlay-pane');
this._overlayContainer.getContainerElement().appendChild(pane);
return pane;
}
private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet {
return new DomPortalOutlet(pane, this._componentFactoryResolver, this._appRef, this._injector);
}
}
可以看到只要是超过<app-root>
以外的元件显示应用,都很适合使用DomPortalOutlet
呢!
今天我们介绍了Angular CDK中动态显示不同画面的功能-Portal,透过cdkPortal
与cdkPortalOutlet
,要动态切换不同的template变成了一件简单的事情;若要动态切换component,也只需要简单的程式码即可达成。同时我们也学到如何让动态的template和component如何与外界沟通,在比较复杂的应用时会非常需要。
如果我们需要在<app-root>
之外动态切换不同的template或component,也能够使用DomPortalOutlet
达成!这是一个非常酷,让SPA超过SPA!不再被一个范围限定死,能够动态的把功能延伸的管理范围之外,又同时不会失去控制,让我们在设计时更加的有弹性!
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-29-cdk-portal
分支:day-29-cdk-portal
来自 https://ithelp.ithome.com.tw/articles/10197393
今天我们要来介绍Angular CDK中的Overlay!Overlay在Angular Material中可以说是随处可见,只要是任何会从萤幕上弹出资讯的功能,如Select、Dialog等等都免不了要使用Overlay;因此也能说Overlay是Angular Material中让画面更具有立体感的大功臣,到底这个功能能帮助我们达到多少目标呢?就让我们继续看下去吧!
首先当然不能忘记,要加入OverlayModule
:
import { OverlayModule } from '@angular/cdk/overlay';
@NgModule({
exports: [
OverlayModule
]
})
export class SharedMaterialModule {}
Overlay里面有许多不同大大小小的classes,其中主宰一切的关键,来自于一个service-Overlay,透过这个service,我们可以用来决定一个component或template动态的产生,以及要产生在什么位置,甚至可以"黏在"另外一个元件的旁边!
我们先来小试身手一下,做一个经典的功能,再画面右下角加入一个floating action button,并在点选后呈现另一个选单!
画面设计如下:
<button mat-fab color="accent" class="float-menu" (click)="displayMenu()" #originFab>
<mat-icon>add</mat-icon>
</button>
<ng-template #overlayMenuList>
<div class="fab-menu-panel">
<mat-nav-list>
<a mat-list-item>新增信件</a>
<a mat-list-item>管理聯絡人</a>
</mat-nav-list>
</div>
</ng-template>
我们在画面上加了一个按钮,以及一个选单的样板,这个样板稍后会出现在按钮的附近,接着我们透过CSS让按钮固定在画面右下方,CSS如下:
.float-menu {
position: fixed !important;
right: 15px;
bottom: 15px;
z-index: 2;
}
.fab-menu-panel {
border: 1px solid black;
background-color: white;
}
.fab-menu-panel .mat-nav-list {
padding-top: 0px;
}
最后就是程式部分啦!
export class InboxComponent implements OnInit {
@ViewChild('overlayMenuList') overlayMenuList: TemplateRef<any>;
@ViewChild('originFab') originFab: MatButton;
overlayRef: OverlayRef;
constructor(private overlay: Overlay, private viewContainerRef: ViewContainerRef) {}
ngOnInit() {
const strategy = this.overlay
.position()
.connectedTo(this.originFab._elementRef, { originX: 'end', originY: 'top' }, { overlayX: 'end', overlayY: 'bottom' });
this.overlayRef = this.overlay.create({
positionStrategy: strategy
});
}
displayMenu() {
if (this.overlayRef && this.overlayRef.hasAttached()) {
this.overlayRef.detach();
} else {
this.overlayRef.attach(new TemplatePortal(this.overlayMenuList, this.viewContainerRef));
}
}
}
以上程式中的步骤大致描述如下:
注入Overlay
在ngOninit()
中,使用const strategy = this.overlay.position().connectedTo()
,建立一个ConnectedPositionStrategy
,代表overlay要与某个物件连结的策略,其中的参数分别为:
要被连结的物件(也就是我们的originFab这个按钮)要被连结的物件(也就是我们的originFab这个按钮)
连结物件的连结点位置,以这里的程式来说,就是右上角为连结点。
overlay连结物件时的连结点位置,以这里的程式来说,就是右下角为连结点。
用图解释的话大概是这种感觉:
使用overlay.create()
建立一个新的OverlayRef
,create()
方法可以传入许多设定资料,在这里我们设定上一步骤所建立的连结策略。
在displayMenu()
方法中,检查是否有attach东西上去,如果有,就执行detach()
,如果没有,就把overlayMenuList
这个template转成TemplatePortal
并attach上去。
对于
attach()
有感到眼熟吗?没错!就是昨天介绍的Portal功能,而Overlay正是使用Portal的功能,来决定要把什么东西放到Overlay上面!
接着我们就可以来看看结果啦!
当我们点下固定在右下角的按钮,就会看到一个简单的选单自动「黏」在我们的按钮上!很酷吧!
在设定完connectedTo()
后,我们能接着设定withFallbackPosition()
,如果connectedTo
显示overlay时会超过萤幕画面,会改使用withFallbackPosition()
的设定值,因此我们可以透过withFallbackPosition()
来设定无法完整显示时,重新调整连结点的Plan B、Plan C…Plan Z 。
例如之前我们在介绍menu时提过,当画面卷动时,menu会先试着 抢镜头 让自己能被完整呈现,实作的程式码大致看起来如下:
this._overlay.position()
.connectedTo(this._element, {originX, originY}, {overlayX, overlayY})
.withFallbackPosition(
{originX: originFallbackX, originY},
{overlayX: overlayFallbackX, overlayY})
.withFallbackPosition(
{originX, originY: originFallbackY},
{overlayX, overlayY: overlayFallbackY},
undefined, -offsetY)
.withFallbackPosition(
{originX: originFallbackX, originY: originFallbackY},
{overlayX: overlayFallbackX, overlayY: overlayFallbackY},
undefined, -offsetY);
参考:https://github.com/angular/material2/blob/5.0.x/src/lib/menu/menu-trigger.ts
刚刚我们已经成功让选单物件连结到按钮物件上了,接下来我们要试试看让选单不要连结到任何画面上,只需要把原来的strategy修改一下:
const strategy = this.overlay
.position()
.global()
.width('500px')
.height('100px')
.centerHorizontally()
.centerVertically();
在这里我们改用overlay.position().global()
来产生一个GlobalPositionStrategy
,代表部连结任何物件,是全域的显示策略。接着用width()
和height()
给予基本的尺寸,再加上centerHorizontally()
和centerVertically()
来调整放到画面的正中间,成果如下:
一个显示在画面正中央的选单就出现啦!
如果不希望显示在正中间,也可以使用top()
、bottom()
、left()
、right()
来设定overlay显示的座标。
接下来我们来看看在overlay.create()
时,可以使用哪些参数,让显示更加灵活!
看过上面两种Overlay显示方式后,我们再来看看使用overlay.create()
时,可以加入哪些参数,这些参数的型别为OverlayConfig
,以下简单说明:
hasBackdrop
:是否要显示一个预设灰底的backdrop。
backdropClass
:让我们能自订backdrop的样式。
direction
:LTR或RTL。
height
、minHeight
和maxHeight
:设定高度相关资讯。
width
、minWidth
和maxWidth
:设定宽度相关资讯。
panelClass
:给予显示的Overlay(也就是panel)一个基本的样式,需要特别注意这个属性,在ConnectedPositionStrategy
上面时overlayRef.deatch()
会正常把panelClass
拿掉;但GlobalPositionStrategy
时,使用overlayRef.deatch()
时会无法拿掉这个样式,需要改用overlayRef.dispose()
,而dispose()
的话,下次还需要使用overlay.create()
重新建立。
positionStrategy
:显示位置的策略,文章前半段提到的就是在切换这个策略。
scrollStrategy
:当画面卷动时,该如何处置Overlay的策略,稍后会详细说明。
接着我们来看看特定几个属性的设定。
设定这个属性为true后,会显示一个基本的灰底backdrop。
const config = new OverlayConfig({
hasBackdrop: true,
positionStrategy: strategy
});
this.overlayRef = this.overlay.create(config);
我们也可以设定当backdrop被点击后,就自动关闭目前的overlay:
this.overlayRef.backdropClick().subscribe(() => {
this.overlayRef.detach();
});
成果如下:
预设Angular CDK的Overlay会帮我们加上一个cdk-overlay-dark-backdrop
的css class,我们可以透过backdropClass
更换它,例如Angular CDK内建了一个cdk-overlay-transparent-backdrop
可以帮我们移除掉灰色的背景,但依然有一个透明的backdop在中间,让我们不能直接跟底层的元件互动:
const config = new OverlayConfig({
hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop',
positionStrategy: strategy
});
this.overlayRef = this.overlay.create(config);
成果如下:
有了cdk-overlay-transparent-backdrop
,滑鼠移到按钮上时,就没有hover的效果,直到按下去关掉overlay时,才一切又正常了。
最后我们来聊一下scrollStrategy
,在ConnectedPositionStrategy
模式下,我们能透过设定scrollStrategy
来决定当滑鼠滚轮卷动时,overlay该如何处置,例如预设如下:
const config = new OverlayConfig({
scrollStrategy: this.overlay.scrollStrategies.noop()
});
此时滑鼠卷动不会影响overlay,overlay的位置依然呈现在原来的位置。
如果希望跟着连结的元件一起移动,可以设定为reposition()
const config = new OverlayConfig({
scrollStrategy: this.overlay.scrollStrategies.reposition()
});
成果如下:
这里可以看到选单会盖过上面的toolbar,这是因为overlay预设的
z-index
为1000,所以我们只需要把toolbar的z-index
设定为超过1000,就可以解决这个问题啰。
例外还有两个可以设定的策略:
close()
:卷动时自动关闭overlay。
block()
:不允许卷动。
应该不难想像结果,就不多写程式拖时间啰,有兴趣的读者可以自己修改看看。
当然,设定hasBackdrop后,因为连scroll bar都被backdrop盖掉无法互动,所以这些卷动就会自动失效,变成类似
block()
的状态了。
今天我们把Angular CDK目前(5.0.0)主功能分类的最后一块拼图-Overlay给介绍完了。这个功能可以让我们的操作介面更具立体感,应用层面也非常广,非常多的Angular Material元件都依赖着Overlay功能,因此要写的程式也不少,不过相信大致操作过一遍后,就能发现这个功能的强大及易用!而且光是想像自己要达到这些功能需要写多少程式码,考量到多少状态,就觉得Angular CDK实在是太贴心啦!!
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-30-cdk-overlay
分支:day-30-cdk-overlay
终于写完30篇了,写到这篇,我们也算完全把目前所有Angular Material文件上的Components和CDK两大主轴全部都介绍了一遍,说真的是满辛苦的,为了不要传递错误的讯息,整个Angular Material的文件来来回回看了不下10遍,还要不断的找原始码来弥补文件上不足的部分,偏偏内容又多到无法完整介绍完!为了兼具能够呈现最多讯息与最重要的讯息,真的是绞尽脑汁在规划,几乎每篇都花费3个小时以上再规划,有几篇甚至花了5个小时,根本是时间太多吧(误)!心里还默默地认为自己说不定是目前中文社群里面最熟悉Angular Material的人了吧(大误)
但看到订阅人数有在上升,也与不少网友互动,真的特别有成就感。虽然有种累到明年不想再参加的感觉,但是...我想明年应该还是会再次跳入火坑XD。
在这次完赛后,让我再次感受到Angular Material的威力及潜力,所以30篇完赛不是终点,而是推坑的开始,之后我会努力推人进Angular Material这美好坑的(误)。
然后偷偷的说,接下来我还会再写几篇相关的实用小技巧,敬请期待XD
来自 https://ithelp.ithome.com.tw/articles/10197492
今天我们来讲两个Angular CDK文件上没有介绍,但很有机会使用到的 隐藏版功能,分别是型别转换(coercion)、平台侦测(platform )。
在之前那么多天介绍Angular Material的元件时,不知道你有没有发现一件有趣的事情,以曾经介绍过的<mat-chip>
为例,当我要设定selected
属性时,当时是这么写的:
<mat-chip selected="true">JavaScript</mat-chip>
我们都知道要处理属性绑定(property binding)时,如果资料是number
、boolean
或变数名称之类的话,应该要用中括号[]
把属性包起来,如:[selected]="true"
,否则传进去的直指会被当作是string
,如果是单纯的字串,则[property]="'string'"
或property="string"
都是可以的。
因此假设我们以selected="false"
撰写的时候,程式中若这样判断
if(selected) {
// do something
}
这样的条件还是会进入的,因为字串'false'
,在if检查时是会过的,要真正的boolean值false
,才不会进入。
偏偏上面提到的selected
很有趣,写成selected="true"
会被选取没有问题,而写成selected="false"
也完全没问题不会被选取,因为Angular Material都帮我们处理好这种小细节了!
这时候我们可以想看看这个用来处理true
或false
的属性selected
,它明明是一个<mat-chip>
的@Input
,但偏偏它又可以不用中括号设定非字串资料,那他宣告的型别到底要是string
还是boolean
呢?如果是string
,后续又该如何处理?
其实我们不用想太多,因为这种贴心小细节在Angular Material中被使用的机会太高了!因此也被拉到Angular CDK中,也就是型别转换功能-coercion。
至于该怎么使用呢?我们继续往下看。
Coercion只有三个现成的方法,方便我们做型别转换,不需要加入任何module,直接使用就可以了!
coerceBooleanProperty
是用来把输入内容转成boolean的一个方法,它的逻辑很简单,输入的参数若是null
或字串的'false'
,就会被当作boolean的false
。
因此我们可以为我们的元件加入一个可以接受字串'false'
当作boolean的属性,例如:
// 記得import這個方法
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({ ... })
export class CoercionDemoBoxComponent implements OnInit {
private _display: boolean;
@Input()
get display(): boolean {
return this._display;
}
// value: boolean,代表預期的參數型別是boolean
// 但我們都知道,javascript其實是弱型別語言
set display(value: boolean) {
// 就算傳進來是string,也會被轉成boolean
this._display = coerceBooleanProperty(value);
}
someMethod() {
if(display) {
// do something
}
}
}
这时候我们就可以使用
<!-- display會被當作boolean的true -->
<app-coercion-demo-box display="true"></app-coercion-demo-box>
或是
<!-- display會被當作boolean的false -->
<app-coercion-demo-box display="false"></app-coercion-demo-box>
就不用再多花力气加上中括弧[display]="false"
,看起来也更加清爽!
跟刚刚介绍的coerceBooleanProperty
一样的意思,coerceNumberProperty
是用来帮助我们把传入的资料强制转为number的工具方法,例如'100.5' + 10
的结果是'100.510'
,这只要有一定JavaScript经验的开发人员都知道,而透过coerceNumberProperty('100.5') + 10
就能够得到110.5
的结果啦!
另外我们coerceNumberProperty
的第二个参数,也能帮助我们设定当转成number失败时,预设的值是多少(没填的话预设为0),例如coerceNumberProperty('xxx')
就会得到结果为0
。
以下面程式码为例:
import { coerceNumberProperty } from '@angular/cdk/coercion';
@Component({ })
export class CoercionDemoBoxComponent implements OnInit {
private _height: number;
@Input()
get height(): number {
return this._height;
}
set height(value: number) {
this._height = coerceNumberProperty(value);
}
}
component的内容为:
<p>
我的高度是 {{ height }},再加上10會變成 {{ height + 10 }}
</p>
这时候使用上就可以在设定height
属性时不用加上中夸号啦!
<app-coercion-demo-box display="true" height="10"></app-coercion-demo-box>
有前面两个方法的经验,应该不难猜出来coerceArray
的用途就是把资料转为阵列,当使用coerceArray<number>(1)
的时候,就会得到结果[1]
,如此一来我们就能让我们的属性接受单一值或阵列值,反正到component时我们都用程式转为阵列啦!
跟前面的
coerceXxxxxProperty
不一样,这个方法名字为coerceArray
,没有Property
后缀,要特别注意。
在Angular CDK中,把浏览器支援度相关的功能放在Platform分类中,我们需要先加入PlatformModule
:
import { PlatformModule } from '@angular/cdk/platform';
@NgModule({
exports: [
OverlayModule
]
})
export class PlatformModule {}
如此便可使用Platform
service来得知目前使用的浏览器。例外Pltform中还提供一个getSupportedInputTypes
工具方法,来取得目前浏览器针对<input>
所支援的types,单独使用这个方法的话,不需要加入PlatformModule
。
透过Platform
,我们可以用来侦测目前使用者使用的浏览器,虽然前端技术与浏览器都越来越纯熟,针对标准的支援度也越来越好,但毕竟还是有些差异,更不用说若使用版本过旧,差异就更大了!真的遇到这种状况时,就能够使用Platform
这个service,先针对使用者的浏览器检查一下了!
Platform
service包含几个判断属性:
属性 | 说明 |
---|---|
isBrowser | 是否为使用浏览器(要知道Angular可以使用的范围可是跨出浏览器的) |
边缘 | 浏览器是否为EDGE |
潮流 | 浏览器的render engine是否为Microsoft Trident |
眨 | 浏览器的render engine是否为Blink |
网页套件 | 浏览器的render engine是否为WebKit |
iOS | 作业系统是否为iOS |
安卓 | 作业系统是否为Android |
FIREFOX | 浏览器是否为firefox |
苹果浏览器 | 浏览器是否为safari |
我们可以先在程式中注入Platform
service,再来检查这些属性:
import { Platform } from '@angular/cdk/platform';
@Component({ })
export class MainComponent {
constructor(public platform: Platform) {}
}
HTML如下:
<div>
<p>Is Browser: {{ platform.isBrowser }}</p>
<p>Is Android: {{ platform.ANDROID }}</p>
<p>Is iOS: {{ platform.IOS }}</p>
<p>Is Firefox: {{ platform.FIREFOX }}</p>
<p>Is Blink: {{ platform.BLINK }}</p>
<p>Is Webkit: {{ platform.WEBKIT }}</p>
<p>Is Trident: {{ platform.TRIDENT }}</p>
<p>Is Edge: {{ platform.EDGE }}</p>
</div>
以笔者使用Macbook+Chrome的结果如下:
getSupportedInputTypes()
是一个工具方法,用来取得目前浏览器所支援<input>
的type清单,回传结果为Set<string>
,直接看程式比较简单:
import { getSupportedInputTypes } from '@angular/cdk/platform';
@Component({ })
export class MainComponent {
supportInputTypes = getSupportedInputTypes();
}
HTML如下:
<h2>目前瀏覽器支援的input types</h2>
<ul>
<li *ngFor="let type of supportInputTypes">{{ type }}</li>
</ul>
以笔者使用Macbook+Chrome的结果如下:
今天我们补充了两个Angular CDK目前文件没介绍的功能:
Coercion:用来帮助我们在设计元件时,打造更好的开发人员使用经验,节省许多时候property binding的无谓程式,也让HTML画面更加一致好懂,这样的元件设计思维非常直得让人学习,不愧是以高品质为目标的Angular Material,不仅使用者体验一流,连开发人员也能得到一流的体验!
Platform:与浏览器平台相关的功能,目前可以判断使用者的浏览器,也能得知input支援的types,虽然使用的机会可能不多,但真的遇到需要针对不同平台做细微调整时,我们也知道了有个现成的工具可以用哩。
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-31-cdk-coerce-platform
分支:day-30-cdk-overlay
到这边我们终于把所有Angular CDK中用来打造一流元件的功能都介绍了一遍,当然还有几个属于元件的分类是我们之前使用Angular Material元件时就有感觉的,因此没有多加介绍,但有过使用Angular Material元件的经验,要阅读文件也会很容易上手!
透过Angular CDK,真的能帮助我们节省很多程式码,笔者目前工作上也已经开始在专案中加入Angular CDK相关的功能,来打造一些Angular Material目前无法提供的功能,体验到了其强大的威力,真的非常适合推荐给所有使用Angular的开发人员,既然要使用轮子,当然要使用最高级的轮子啦!!XD
接下来我们会再花几天时间介绍一些关于使用Angular Material和Angular CDK的相关小技巧,明天见!
来自 https://ithelp.ithome.com.tw/articles/10197609
Angular Material内建了4种不同主题的theme,未来应该还会持续增加,但这些theme未必是我们喜欢的,而在Angular Material中,要设计自己的theme非常简单,我们就来看看该如何做吧!
Angular Material使用SCSS来设计,并提供了许多的@mixin
可以使用,让我们轻易就能够客制化颜色的功能,首先第一步,我们先在专案下建立一个custom-theme.scss
档案,如下图:
加入包含Angular Material的theming,并汇入基本的样式:
@import '~@angular/material/theming';
@include mat-core();
在执行@include mat-core()
后,会将所有元件都共用的基本样式都加入,因此这个动作只需要做一次就好。
接着我们就可以来设定theme的颜色啰,Material Design已经订了许多颜色的调色盘,同时在Angular Material中都有设计好对应主色的变数,举例来说:light blue的颜色就可以使用$mat-light-blue
,而pink则可以使用$mat-pink;我们可以透过Angular Material另位提供的辅助工具mat-palette()
,来设定这些颜色的亮度。例如:
$custom-primary: mat-palette($mat-light-blue);
另外我们也能提供3个参数,分别为,颜色主要的亮度(预设为500),前色的色调以及深色的色调
$custom-accent: mat-palette($mat-orange, 500, A100, A700);
不过最后两个参数在theme中其实用不到,我们可以忽略它。
在这里我们先把Material Design中的三种主要色彩都定义好,如下:
$custom-primary: mat-palette($mat-light-blue, 500); /* 500是預設值,也可以忽略 */
$custom-accent: mat-palette($mat-green);
$custom-warn: mat-palette($mat-brown);
接下来我们可以使用mat-light-theme
建立浅色主题,或mat-dark-theme
建立深色主题,之后只需要使用@include angular-material-theme()
就可以取得所有的颜色结果啦!
/* 建立深色主題 */
$custom-theme: mat-dark-theme($custom-primary, $custom-accent, $custom-warn);
@include angular-material-theme($custom-theme);
最后我们要在style.css中加入样式,不过要记得我们目前是用SCSS,因此要将style.css改为style.scss,同时修改.angular-cli.json的app.styles
,最后在style.scss中加入我们自订的样式,就完成啦!
结果如下:
看起来是不是别有一番风味啊!
如果不知道颜色怎么搭比较好,可以到Material Design Color Palette Generator这个网站,随意选择两种颜色,就可以看到效果参考啰!
要建立多个theme也很简单,把@include angular-material-theme();
的部分放到一个css class下,再切换不同的class就可以了,如下:
@import '~@angular/material/theming';
@include mat-core();
$custom-primary: mat-palette($mat-light-blue);
$custom-accent: mat-palette($mat-green);
$custom-warn: mat-palette($mat-brown);
$custom-theme: mat-dark-theme($custom-primary, $custom-accent, $custom-warn);
.custom-theme-1 {
@include angular-material-theme($custom-theme);
}
$custom-primary-2: mat-palette($mat-yellow, 800);
$custom-accent-2: mat-palette($mat-deep-orange);
$custom-warn-2: mat-palette($mat-pink);
$custom-theme-2: mat-dark-theme($custom-primary-2, $custom-accent-2, $custom-warn-2);
.custom-theme-2 {
@include angular-material-theme($custom-theme-2);
}
上面的程式中,我们使用两组变数,并分别放到.custom-theme-1
及.custom-theme-2
之中,只需要切换不同的class,就可以改变整个画面啰!
当有多个theme时,由于overlay通常会在theme的范围之外,因次在需要dialog这类的程式显示为异常,如下:
这时候我们必须做额外的设定,这时候我们需要注入OverlayContainer
,并透过它取得overlay的container,然后把样式加上去:
import { OverlayContainer } from '@angular/cdk/overlay';
@Component({ })
export class DashboardComponent implements OnInit {
theme = 'custom-theme-1';
constructor(private overlayContainer: OverlayContainer) { }
ngOnInit() {
this.overlayContainer.getContainerElement().classList.add(this.theme);
}
toggleTheme() {
const originalTheme = this.theme;
this.theme = this.theme === 'custom-theme-1' ? 'custom-theme-2' : 'custom-theme-1';
this.overlayContainer.getContainerElement().classList.remove(originalTheme);
this.overlayContainer.getContainerElement().classList.add(this.theme);
}
}
这时候再切换就会一切正常啦!
今天我们学到使用Angular Material所提供的SCSS,并了解到Material Design的调色盘中,在Angular Material都有对应的颜色,只需要使用$mat-xxxx
变数即可,而透过mat-palette()
可以得到实际颜色的配置,包含亮色调及暗色调。
最后我们使用mat-dark-theme()
来取得深色的主题颜色,当然也能够使用mat-light-theme
来得到亮色的主题,最后再使用angular-material-theme()
得到完整Angular Material里面相关元件的class。
由于SCSS的特性,我们也能轻易把这些主题样式包装到另外一个class之中,来达到切换样式的效果,可以说是非常的有弹性啊!
本日的程式码GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-32-theme
下面这几个可以看一看 看颜色
https://material.io/design/color/#
https://www.materialpalette.com/
https://material.io/design/color/#color-theme-creation
https://github.com/angular/components/blob/5.0.x/src/lib/core/theming/_palette.scss
分支:day-32-theme
来自 https://ithelp.ithome.com.tw/articles/10197670
今天来分享一些笔者近期撰写Angular Material文章,以及开始在实际专案中使用Angular Material所整理出来的一些CSS小技巧,有些在文件上可以轻松找到,有些则是遇到后才整理出来的,希望能对各位读者大大们在使用Angular Material时有所帮助!
昨天的文章在介绍自订theme时,我们提到了内建颜色的$mat-XXXX
变数,这些变数的实际原始码看起来如下:
要直接取得里面的颜色,可以搭配使用SASS的map-get()
方法,例如要取得红色色票中亮度为500的颜色代码,可以使用如下方式:
.custom-style {
color: map-get($mat-red, 500);
}
如此一来就能取得#f44336
的色码啦!
另外在Angular Material中还有定义一个mat-color()
方法,方便我们设定颜色,以及他的透明度,甚至可以不用担心亮度,直接使用darker
或lighter
参数,来决定定义好的亮度,除此之外也可以直接设定透明度,被用在许多Angular Material中的背景颜色的设定,如以下样式我们选用暗色调的红色,以透明度50%,作为样式的背景:
.custom-color {
background: mat-color(mat-palette($mat-red), darker, 0.5);
}
关于颜色的定义,除了上Material Design网站上观看调色盘外,也可以直接查Angular Material的_palette.scss,可以看到完整的颜色变数。
其实这已经脱离Angular Material的范围了,但很值得简单介绍一下,因为Angular Material的颜色一定要搭配SASS并写在scss档中,有时候比较懒惰,只想要单纯使用颜色时,若有人直接把相关颜色都订成对应的class,那实在是太方便了!而material-colors套件就是一个帮我们把Material Design颜色都订成CSS class的好东西!
加入这个style样式后,我们可以直接使用class="color-red-500"
,来取得红色色票且亮度为500的颜色,另外被景色也可以使用class="bg-red-500"
来达到预期的效果。
除此之外material-colors套件也有定义好的SASS档,相当值得参考。
在Material Design中,为了让画面具有立体感,对于阴影(shadows)的使用可以说是非常讲究,这种阴影设计也被称为elevation,作为元件与元件之间高低差的概念,也就是阴影越深,高度看起来就越高。
下图是从Material Design设计指南中撷取的图片,对于各种高度效果的建议:
数值越高代表在画面呈现上应该在越上层,例如Dialog类型的应该要在最高的位置24,选单Menu在8,而子选单会比选单在高一点,因此在高度9的位置。
当我们自己在设计元件时,为了让元件更有立体感,可以参考这个高度表,评估一下自己设计的元件属于哪种元件分类,进而选择适当的高度,来达到画面呈现的一致感!
至于具体的CSS样式到底该如何设计呢?其实Angular Material已经都帮我们设计好啦!只要套用mat-elevation-zX
的样式就可以了!X
的部分,请直接换成对应的高度,例如我们在使用overlay并显示在整个画面上时,通常是类似dialog的角色,这时候就可以要显示元件上加一个class="mat-elevation-z24"
,就能够具有一致的显示啦!
另外我们也能够使用mat-elevation()
这个mixin,只需要以适当的高度当作参数带入,就会产生对应高度的阴影样式:
@import '~@angular/material/theming';
.my-custom-button {
@include mat-elevation(2);
}
等于得到如下的阴影样式:
.my-custom-button {
box-shadow:
0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14),
0px 1px 5px 0px rgba(0, 0, 0, 0.12);
}
真是省时又省力啊!
这个技巧在之前其实已经提过几次,但真的很实用也很重要,因此在这边再次提一下。在Angular Material中,几乎所有的元件与directive,都会加上一个与它自己同名的CSS class,例如mat-button
这个directive会在它的元件上加入同名的CSS class。因此我们可以直接在撰写CSS时,透过这个class来调整样式,例如:
.mat-button {
font-size: 24px;
}
透过这样的方式,要针对某个元件为调整符合需求的呈现就变得非常简单哩。
Angualr Material中,有许多popup的呈现,都能透过设定panelClass来改变整个popup的呈现,以笔者实际遇到的一个状况为例:专案需要一个dialog,已经由美工设计好,这个dialog是没有padding的,但使用Angular Material打开的dialog预设会套用一个mat-dialog-container
样式,团队讨论后不希望直接像上一段描述的调整全域mat-dialog-container
样式,这时候我们就替要打开的dialog设定了一个panelClass:custom-dialog
,并调整样式把里面mat-dialog-container
的padding设为0,如下:
.custom-dialog {
.mat-dialog-container {
padding: 0px;
}
}
之后打开dialog时,程式调整成:
this.dialog.open(SomeDialogComponent, {
panelClass: 'custom-dialog'
});
就能够改变显示dialog底层panel的样式啦!
其他可能的状况如背景颜色、调整阴影等等,都可以透过panelClass来做设定!
这个技巧在所有popup类型的元件都可以使用,除了dialog、snackbar是使用程式控制以外,其他元件如<mat-select>
或<mat-menu>
等,都有一个panelClass
的input可以直接使用。
另外使用Angular CDK的overlay时,也能够透过panelClass来设定样式。
要单独使用某个元件其实不难,只要import对应的Module就好,就算把所有Module都import近来好了,Angular Material也会在build时透过tree shaking机制来甩掉用不到的程式,因此原则上是不用担心会入过多程式造成档案庞大的问题,但这是程式面的部分,而CSS就不是这么一回事了!
当我们使用@include angular-material-theme()
的同时,也就代表所有Angular Material元件的样式都被加了进来,但或许我们真的只需要用到整个Angular Material的1~2个功能,又对档案大小有所要求时,其实我们还是能单独汇入特定元件的样式。
简单的步骤说明如下:
要单独使用某个样式,当然就必须抛弃现有theme的CSS档(或看看未来Angular Material会不会提供单纯theme的SCSS定义档),因此我们必须自己定义theme的颜色,这部分在昨天的文章已经介绍过了,如下:
@import '~@angular/material/theming';
@include mat-core();
$custom-primary: mat-palette($mat-light-blue);
$custom-accent: mat-palette($mat-green);
$custom-warn: mat-palette($mat-brown);
$custom-theme: mat-dark-theme($custom-primary, $custom-accent, $custom-warn);
接下来我们要加入核心的样式,这些样式是大部分元件都通用的,因此必须先加进来,除了mat-core()
已经有了最基本的共用样式外,还需要加入mat-core-theme()
.custom-theme-1 {
@include mat-core-theme($custom-theme);
}
最后,我们只加入想要的元件样式就好,Angular Material中将所有的元件都依模组切成了不同的mixin,只需要针对对应模组加入mat-xxxxx-theme
,例如button对应的就是mat-button-theme
.custom-theme-1 {
@include mat-core-theme($custom-theme);
@include mat-button-theme($custom-theme);
}
这时候就只有按钮元件会套用相关的样式啦!
上列步骤是一个简单且保险的方法,但还是可能会加入许多不必要的样式;举例来说,mat-core()
包含了overlay相关的样式,在只使用button时根本用不到;而mat-core-theme()
里面其实也没有与button相关的共用样式,加入这个设定只会产生用不到的样式而已,这时候我们可以来看看_core.scss中mat-core()
和mat-core-theme()
到底加入了些什么共用的样式:
// Mixin that renders all of the core styles that are not theme-dependent.
@mixin mat-core($typography-config: null) {
// Provides external CSS classes for each elevation value. Each CSS class is formatted as
// `mat-elevation-z$zValue` where `$zValue` corresponds to the z-space to which the element is
// elevated.
@for $zValue from 0 through 24 {
.#{$_mat-elevation-prefix}#{$zValue} {
@include mat-elevation($zValue);
}
}
@include angular-material-typography($typography-config);
@include mat-ripple();
@include mat-option();
@include mat-optgroup();
@include cdk-a11y();
@include cdk-overlay();
}
// Mixin that renders all of the core styles that depend on the theme.
@mixin mat-core-theme($theme) {
@include mat-ripple-theme($theme);
@include mat-option-theme($theme);
@include mat-optgroup-theme($theme);
@include mat-pseudo-checkbox-theme($theme);
// Wrapper element that provides the theme background when the
// user's content isn't inside of a `mat-sidenav-container`.
.mat-app-background {
$background: map-get($theme, background);
background-color: mat-color($background, background);
}
// Marker that is used to determine whether the user has added a theme to their page.
.mat-theme-loaded-marker {
display: none;
}
}
可以看到有些其实是用不到的,但当使用<mat-select>
时,mat-option-theme()
就可能是必要的!这时候我们就能够排除使用mat-core()
和mat-core-theme()
,自己选择要加入的mixin啰,再进一步的瘦身啦!
以上方法其实已经对于档案大小有吹毛求疵的要求了,一般状况很少用到,纯粹炫耀笔者对Angular Material的理解程度之深(误) 但当真的有所要求或追求极致时,就是一个可以考虑调教的地方啰!
顺带一提,以笔者目前的范例专案,使用Angular CLI 1.6.0的ng serve指令产出未压缩的
style.bundle.js
档,使用上述方法瘦身后,减少约150k的大小(当然,许多样式都消失了)。
Angular CDK是Angular Material共用的部分抽取出来的工具,在之前的文章我们也提过,不是每个专案都需要使用Material Design,但每个专案基本上都需要打造自己的元件!因此我们可以不装Angular Material,但强烈建议把Angular CDK加入专案中,但Angular CDK其实还是有基本的样式,像是overaly的灰底等等,那么只加入Angular CDK的专案,该怎么把这些样式加进来呢?
其实从上一章套用部分样式中我们就能略知一二,在mat-core()
中我们加入了cdk-a11y()
和cdk-overlay()
两个mixin,给Angular CDK使用,所以我们只需要把Angular CDK的SCSS路径找出来,再加入style.scss之中就好啰:
@import "~@angular/cdk/_a11y.scss";
@import "~@angular/cdk/_overlay.scss";
@include cdk-a11y();
@include cdk-overlay();
当然啦!如果没用到Angular CDK的a11y和overlay功能,一样可以不加入这两组样式。
在笔者一段不算长的时间学习以及正式专案中部分使用Angular Material及Angular CDK经验中,所遇到的一些需求,如下:
细部调整元件的呈现
打造更加一致具有Material Design风格的元件
档案瘦身
在Angular Material中已经帮我们把这些都设想好了,我们只要依照前人走好的路,就能更轻松达成这些目标,真的是太幸福啦!
用 mat-elevation-zX 來加陰影真的是很好用呢!
我看到 angular material 官網的導覽列上有加 mat-elevation-z6
官方文件也有說到這部分:
https://material.angular.io/guide/elevation
来自 https://ithelp.ithome.com.tw/articles/10197682
今天来聊聊笔者这些日子学习Angular Material的方式,希望可以帮助大家能以更快的速度深入Angular Material,并能灵活运用在自己的专案当中。
在笔者的经验中,Angular Material要学得好,除了需要一定程度的Angular知识以外,另外还有Angular Material三宝:文件、demo app和source code。
不论任何技术,想要快速地上手,文件绝对是不可或缺的一部份,Angular Material的文件本身已经提供不少详细的说明,但在目前还稍嫌不完整,但要帮助初学者上手已经非常足够了。
Angular Material的文件主要分成两个部分
Guides:包含了最基础的「Getting started」,让我们能快速开始使用Angular Material;还有一些持续在增加中的文章,补充Angular Material各种功能可以加强的部分。
Components、CDK:Angular Material的核心功能,包含了元件(Components)与CDK,在元件页面,详列了目前所有Angular Material可以使用的元件功能,每个功能的文件都包含了「Overview」、「API 」与「Examples」三个页签;CDK则只有「Overview」和「API」
在Overview页签中,我们可以看到整个功能的基本使用方式,以及一些常见可以设定的属性,方法等等。
在API页签中,则是该功能的module下所有的元件、service和directives等等,以及详列所有相关的属性和方法,对于要进一步使用元件的功能,这个页面是不可或缺的参考资料。
Examples则是一些简单的范例和程式码。
在Angular Material的文件中,包含了许多基本的范例DEMO,如下:
右方有两个按钮,第一个按钮是直接检视相关的原始码,不过目前大部分点开原始码出现的程式,版本都不正确,都是md-xxxx
而不是mat-xxxx
,这点需要非常小心:
再旁边还有一个按钮,则会跳到StackBlitz的网站,并显示这个范例的程式码,并且放在StackBlitz网站的程式码都是正确的!方便我们copy/paste
只要好好的把握文件的使用脉络,要能够掌握大部分Angular Material的功能,基本上绝对不是问题!
Angular Material的原始码本身包含了一个demo app,这个demo app具有非常多的范例程式,如果读者在阅读文件时觉得不太知道该如何应用,可以直接看看demo app的程式,会有很多收获!
使用demo app的方法很简单,首先我们把整个material2的专案都clone下来:
git clone https://github.com/angular/material2.git
是不是 地址变了 变成了 https://github.com/angular/components.git
(见 https://github.com/angular/components 它是由 https://github.com/angular/material2 跳转而来 )
(所以 原来的 git clone https://github.com/angular/material2.git 也是可以的,它们的内容是完全一样的)
为啥不行呢, 到 https://github.com/angular 里面去找找其它的吧
https://github.com/katan/angular-material-demos 这个肯定行ok 有大用
接着进入专案,还原npm套件后,执行demo-app
的script
cd material2
npm install
npm run demo-app
这时候在进入http://localhost:4200/,就能够看到一个基本的demo页面啰
点击左上方的选单按钮,就能够看到所有的功能demo连结
至于demo app的原始码,则是放在material2专案目录的src/demo-app
下面,读者可以在里面找到许多范例程式码,也能够直接修改里面的程式,看看demo app会对应产生什么变化。
虽然直接看原始码通常是最后手段,但不得不说Angular Material的原始码写得非常漂亮,可度性很高,如果在看过demo app后还是对于使用上有所疑惑,或是单纯好奇某个元件功能是怎么办到的时候,就是阅读原始码的时机啦!
Angular Material的主要元件功能程式码都方放在src/lib
中,另外Angular CDK的原始码则是放在src/cdk
。
偶尔多看一下Angular Material的原始码,不仅能更加深刻理解Angular Material背后运作的原理,更能从中学习到许多元件功能设计的思维,可以说是一举数得啊!
上面是笔者在学习Angular Material时,最常使用的三个资源,下面再补充几个学习Angular Material不可错过的好去处:
Angular Taiwan Facebook社团 | 论坛:最大的好处,不用多说当然就是里面所有的人都说中文啦!这里主要是讨论Angular的地方,但是如果对于Angular Material有问题,提出来也是很多高手会回答的!
StackOverflow - Angular Material:StackOverflow应该不用多说,是全世界最大学习各种技术copy/paste 的最佳来源,卡关的时候,先来这里找找就对了!
Angular Material的issues:在Angular Material的issues里面,不会提供某个元件该怎么用之类的说明,但是却可以帮助我们确认某些功能是否真的有它的限制,毕竟有时候某些功能无效,不一定都是我们得问题,也有可能是Angular Material本身的限制,甚至是bug,这时候上issues找最准确啦!
Gitter - angular/material2:Angular Material的线上聊天室,遇到问题时,在这里问问题也是很棒的选择!
今天介绍了笔者在学习Angular Material时所使用的资源,这些资源都有很丰富的内容可以学习,只要愿意稍微花点时间,相信要成为Angular Material高手绝对不是一件困难的事情!让我们一起迈向Angular Material大师的伟大航道吧!
Angular Taiwan Facebook社团 | 论坛
原来还有demo app这个大绝招!
我用yarn的指令也可以用,真的好棒啊,可以不停copy/paste!
来自 https://ithelp.ithome.com.tw/articles/10197691