iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 30
2

今天我們要來介紹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 service

Overlay裡面有許多不同大大小小的classes,其中主宰一切的關鍵,來自於一個service-Overlay,透過這個service,我們可以用來決定一個component或template動態的產生,以及要產生在什麼位置,甚至可以"黏在"另外一個元件的旁邊!

使用Overlay讓物件與物件連結

我們先來小試身手一下,做一個經典的功能,再畫面右下角加入一個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));
    }
  }
}

以上程式中的步驟大致描述如下:

  1. 注入Overlay
  2. ngOninit()中,使用const strategy = this.overlay.position().connectedTo(),建立一個ConnectedPositionStrategy,代表overlay要與某個物件連結的策略,其中的參數分別為:
    1. 要被連結的物件(也就是我們的originFab這個按鈕)要被連結的物件(也就是我們的originFab這個按鈕)
    2. 連結物件的連結點位置,以這裡的程式來說,就是右上角為連結點。
    3. overlay連結物件時的連結點位置,以這裡的程式來說,就是右下角為連結點。
    4. 用圖解釋的話大概是這種感覺:
      cdkOverlay connectTo explain
  3. 使用overlay.create()建立一個新的OverlayRefcreate()方法可以傳入許多設定資料,在這裡我們設定上一步驟所建立的連結策略。
  4. displayMenu()方法中,檢查是否有attach東西上去,如果有,就執行detach(),如果沒有,就把overlayMenuList這個template轉成TemplatePortal並attach上去。

對於attach()有感到眼熟嗎?沒錯!就是昨天介紹的Portal功能,而Overlay正是使用Portal的功能,來決定要把什麼東西放到Overlay上面!

接著我們就可以來看看結果啦!

Float action menu demo

當我們點下固定在右下角的按鈕,就會看到一個簡單的選單自動「黏」在我們的按鈕上!很酷吧!

設定withFallbackPosition

在設定完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

使用Overlay讓物件顯示在畫面上,不連結任何物件

剛剛我們已經成功讓選單物件連結到按鈕物件上了,接下來我們要試試看讓選單不要連結到任何畫面上,只需要把原來的strategy修改一下:

const strategy = this.overlay
  .position()
  .global()
  .width('500px')
  .height('100px')
  .centerHorizontally()
  .centerVertically();

在這裡我們改用overlay.position().global()來產生一個GlobalPositionStrategy,代表部連結任何物件,是全域的顯示策略。接著用width()height()給予基本的尺寸,再加上centerHorizontally()centerVertically()來調整放到畫面的正中間,成果如下:

Global Menu

一個顯示在畫面正中央的選單就出現啦!

如果不希望顯示在正中間,也可以使用top()bottom()left()right()來設定overlay顯示的座標。

接下來我們來看看在overlay.create()時,可以使用哪些參數,讓顯示更加靈活!

設定OverlayConfig

看過上面兩種Overlay顯示方式後,我們再來看看使用overlay.create()時,可以加入哪些參數,這些參數的型別為OverlayConfig,以下簡單說明:

  • hasBackdrop:是否要顯示一個預設灰底的backdrop。
  • backdropClass:讓我們能自訂backdrop的樣式。
  • direction:LTR or RTL。
  • heightminHeightmaxHeight:設定高度相關資訊。
  • widthminWidthmaxWidth:設定寬度相關資訊。
  • panelClass:給予顯示的Overlay(也就是panel)一個基本的樣式,需要特別注意這個屬性,在ConnectedPositionStrategy上面時overlayRef.deatch()會正常把panelClass拿掉;但GlobalPositionStrategy時,使用overlayRef.deatch()時會無法拿掉這個樣式,需要改用overlayRef.dispose(),而dispose()的話,下次還需要使用overlay.create()重新建立。
  • positionStrategy:顯示位置的策略,文章前半段提到的就是在切換這個策略。
  • scrollStrategy:當畫面捲動時,該如何處置Overlay的策略,稍後會詳細說明。

接著我們來看看特定幾個屬性的設定。

設定hasBackdrop

設定這個屬性為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();
});

成果如下:

backdrop

設定backdropClass

預設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);

成果如下:

backdrop class transparent

有了cdk-overlay-transparent-backdrop,滑鼠移到按鈕上時,就沒有hover的效果,直到按下去關掉overlay時,才一切又正常了。

設定scrollStrategy

最後我們來聊一下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()
});

成果如下:

Reposition scroll

這裡可以看到選單會蓋過上面的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。

/images/emoticon/emoticon18.gif

在這次完賽後,讓我再次感受到Angular Material的威力及潛力,所以30篇完賽不是終點,而是推坑的開始,之後我會努力推人進Angular Material這美好坑的(誤)。

然後偷偷的說,接下來我還會再寫幾篇相關的實用小技巧,敬請期待XD


上一篇
[Angular Material完全攻略] Day 29 - Angular CDK(5) - Portal
下一篇
[Angular Material完全攻略]Angular CDK(隱藏版) - Coercion、Platform
系列文
Angular Material完全攻略34

2 則留言

0
keatkeat87
iT邦見習生 0 級 ‧ 2018-06-02 21:14:48

很感谢你的分享。加油
你的付出帮助到了许许多多的人。

0
keatkeat87
iT邦見習生 0 級 ‧ 2018-06-02 21:14:50

很感谢你的分享。加油
你的付出帮助到了许许多多的人。

我要留言

立即登入留言