Angular cdk 学习之 Overlay

芮意
2023-12-01

       cdk Overlay主要用来实现在界面上创建浮动面板,例如弹窗(Dialog),下拉框(select)等等都可以通过cdk Overlay来实现。接下来咱们将对Overlay的使用做一个非常简单的介绍。特别推荐大家直接去看官网,官网是最详细的也是最好的文档https://material.angular.io/cdk/overlay/overview

       在上一篇文章中我们大概讲了下cdk Portals的使用,咱们这次要讲的 Overlay内部试下就是在Portals的基础之上做的。OverlayRef(Overlay service里面的create函数创建)就对应着Portals里面的PortalOutlet,Overlay里面我们需要放置的内容就对应着Portals里面的Portal。如果你有兴趣或许你可以先看一看前一篇文章关于cdk Portals使用的介绍。

       Overlay的使用也很简单,关键在两个地方:位置策略、滑动策略。一个处理overlay的显示位置,一个处理有滑动的时候overlay的动作。

一 位置策略(PositionStrategy)

        位置策略(PositionStrategy):用来确定overlay在页面中的显示位置。overlay里面提供了OverlayPositionBuilder类来构建PositionStrategy。overlay也给我们提供了三种PositionStrategy:GlobalPositionStrategy、ConnectedPositionStrategy和FlexibleConnectedPositionStrategy。

1.1 GlobalPositionStrategy

       当overlay的PositionStrategy设置成GlobalPositionStrategy的时候,overlay的位置是相对整个窗口而言的。

GlobalPositionStrategy 常用方法

export declare class GlobalPositionStrategy implements PositionStrategy {
    /**
     * overlay距离上边
     */
    top(value?: string): this;
    /**
     * overlay距离左边
     */
    left(value?: string): this;
    /**
     * overlay距离下边
     */
    bottom(value?: string): this;
    /**
     * overlay距离右边
     */
    right(value?: string): this;
    /**
     * overlay宽度
     */
    width(value?: string): this;
    /**
     * overlay 高度
     */
    height(value?: string): this;
    /**
     * 水平居中
     */
    centerHorizontally(offset?: string): this;
    /**
     * 垂直居中
     */
    centerVertically(offset?: string): this;
}

1.2 ConnectedPositionStrategy

       当overlay的位置需要依赖于另外一个视图的位置的时候采用该ConnectedPositionStrategy来确定overlay的位置。因为ConnectedPositionStrategy完全可以用FlexibleConnectedPositionStrategy来代替。所有我们直接看FlexibleConnectedPositionStrategy。

1.3 FlexibleConnectedPositionStrategy

       overlay的位置依赖于某个视图的位置。

export declare class GlobalPositionStrategy implements PositionStrategy {

    ...
    
    /**
     * 当origin视图位置改变之后,可以调用该函数来重写设置overlay的位置
     */
    reapplyLastPosition(): void;
    /**
     * Sets the list of Scrollable containers that host the origin element so that
     * on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
     * Scrollable must be an ancestor element of the strategy's origin element.
     * 看上面英文的意思是,当origin视图包含在CdkScrollable里面的时候,需要设置。我现在还没搞明白这个函数的使用,等后续看了scrolling 看下
     */
    withScrollableContainers(scrollables: CdkScrollable[]): void;
    /**
     * 设置overlay的位置 (我们要重点解释下为什么是数组,比如有这种情况,你本来想把overlay放在origin视图的下面的,有可能会有这种情况吧
     * 比如,放下面已经放不下了,在窗口外面去了,这个时候就会去取数组里面的第二个元素布局了)
     * ConnectedPosition其实也很好理解,origin视图的哪个点(originX,originY)和overlay(overlayX, overlayY)重合确定位置
     */
    withPositions(positions: ConnectedPosition[]): this;
    /**
     * 设置overlay相对窗口的margin。确定位置判断的时候会用到
     */
    withViewportMargin(margin: number): this;
    /**
     * 设置是否限制在窗体内,设置为true的时候withScrollableContainers里面的数组就有效果了
     */
    withFlexibleDimensions(flexibleDimensions?: boolean): this;
    /** Sets whether the overlay can grow after the initial open via flexible width/height. */
    withGrowAfterOpen(growAfterOpen?: boolean): this;
    /** Sets whether the overlay can be pushed on-screen if none of the provided positions fit. */
    withPush(canPush?: boolean): this;
    /**
     * ScrollStrategy设置RepositionScrollStrategy的时候,
     * 如果是true,overlay会一直跟着origin视图。false的时候,overlay滑倒窗口边缘的时候就不会动了
     */
    withLockedPosition(isLocked?: boolean): this;
    /**
     * 设置origin视图(overlay依赖的视图)
     */
    setOrigin(origin: ElementRef | HTMLElement): this;
    /**
     * 默认x的偏移量
     */
    withDefaultOffsetX(offset: number): this;
    /**
     * 默认y偏移量
     */
    withDefaultOffsetY(offset: number): this;
    
    ...
}

二 滑动策略(ScrollStrategy)

        滑动策略(ScrollStrategy):当PositionStrategy是ConnectedPositionStrategy或者FlexibleConnectedPositionStrategy的时候,如果overlay依赖的控件位置改变的时候overlay的位置应该怎么变化。overlay里面通过ScrollStrategyOptions来创建ScrollStrategy。同样overlay里面也给我们提供了四种ScrollStrategy:NoopScrollStrategy、CloseScrollStrategy、BlockScrollStrategy和RepositionScrollStrategy。

ScrollStrategy的使用一般配合PositionStrategy的ConnectedPositionStrategy、FlexibleConnectedPositionStrategy来使用。因为GlobalPositionStrategy的时候很少有scroll的情况。所以文章中提到的origin指的是overlay依赖的那个视图。

ScrollStrategy解释
NoopScrollStrategyorigin滚动的时候,overlay位置不动
CloseScrollStrategyorigin位置变动的时候,overlay会自动关掉
BlockScrollStrategyorigin的滚动也消失了,直接把origin的滚动干没了
RepositionScrollStrategyoverlay会跟着origin位置的变动而变动

三 Overlay(Service)

       Overlay是一个Service。Overlay主要用来帮我们干三件事:创建OverlayRef(对应overlay视图,然后我们就可以把自定义的组件或者ng-tempalate里面的内容attach到OverlayRef上。这样就是overlay了)、创建PositionStrategy、创建ScrollStrategy。Overlay主要方法介绍:

export declare class Overlay {
    /**
     * ScrollStrategyOptions - ScrollStrategy构造
     */
    scrollStrategies: ScrollStrategyOptions;

    /**
     * 构造函数,这个我们不用管,我们只需要知道service怎么用就可以了
     */
    constructor(
        scrollStrategies: ScrollStrategyOptions, _overlayContainer: OverlayContainer
        , _componentFactoryResolver: ComponentFactoryResolver
        , _positionBuilder: OverlayPositionBuilder
        , _keyboardDispatcher: OverlayKeyboardDispatcher
        , _injector: Injector, _ngZone: NgZone
        , _document: any, _directionality: Directionality
        , _location?: Location | undefined);
    /**
     * 创建OverlayRef
     */
    create(config?: OverlayConfig): OverlayRef;
    /**
     * OverlayPositionBuilder - PositionStrategy构造器
     */
    position(): OverlayPositionBuilder;
}

默认情况下overlay是直接添加在body的第一次层节点下面的,这部分的内容是通过OverlayContainer Service来实现的。有兴趣的可以看下OverlayContainer内部的实现。同时cdk Overlay里面也给提供了FullscreenOverlayContainer Service用来应对可能我们某些组件需要设置全屏的情况,比如有一个播放节点video。有全屏播放的功能。在全屏播放的时候你也想弹出overlay来的。这个时候就得添加providers: [{provide: OverlayContainer, useClass: FullscreenOverlayContainer}]。

四 指令

       cdk Overlay里面给提供了两个指令CdkOverlayOrigin和CdkConnectedOverlay。一个对应origin(overlay定位依赖的视图),一个对应overlay。

4.1 CdkOverlayOrigin

       添加了CdkOverlayOrigin指令的视图表示该视图是overlay的origin视图。CdkOverlayOrigin指令的使用非常简单没有@Input、@Output。

       Selector: [cdk-overlay-origin] [overlay-origin] [cdkOverlayOrigin]

       Exported as: cdkOverlayOrigin

4.2 CdkConnectedOverlay

       添加了CdkConnectedOverlay指令的嵌入视图元素(一般都是加载ng-template上)表明该嵌入元素是一个overlay。

       Selector: [cdk-connected-overlay] [connected-overlay] [cdkConnectedOverlay]

       Exported as: cdkConnectedOverlay

CdkConnectedOverlay指令提供的属性有

属性类型解释
backdropClass: string@Input(‘cdkConnectedOverlayBackdropClass’)背景层class
flexibleDimensions: boolean@Input(‘cdkConnectedOverlayFlexibleDimensions’)overlay是否需要限制在窗口内
growAfterOpen: boolean@Input(‘cdkConnectedOverlayGrowAfterOpen’)覆盖层是否可以在初始打开后增长
hasBackdrop: any@Input(‘cdkConnectedOverlayHasBackdrop’)是否给overlay设置背景层 RepositionScrollStrategy的时候overlay是否一致跟着origin走,就算origin滑出到屏幕外面去了也跟出去
height: number | string@Input(‘cdkConnectedOverlayHeight’)overlay 高度
lockPosition: any@Input(‘cdkConnectedOverlayLockPosition’)
minHeight: number | string@Input(‘cdkConnectedOverlayMinHeight’)overlay 最小高度
minWidth: number | string@Input(‘cdkConnectedOverlayMinWidth’)overlay最小宽度
offsetX: number@Input(‘cdkConnectedOverlayOffsetX’)overlay x偏移
offsetY: number@Input(‘cdkConnectedOverlayOffsetY’)overlay y偏移
open: boolean@Input(‘cdkConnectedOverlayOpen’)overlay显示隐藏
origin: CdkOverlayOrigin@Input(‘cdkConnectedOverlayOrigin’)设置overlay的origin(依赖的视图)
panelClass: string | string[]@Input(‘cdkConnectedOverlayPanelClass’)overlay添加class
positions: ConnectedPosition[]@Input(‘cdkConnectedOverlayPositions’)overlay位置设定
push: boolean@Input(‘cdkConnectedOverlayPush’)如果我们给overlay提供的位置都不适合的时候,是否可以重叠显示
scrollStrategy: ScrollStrategy@Input(‘cdkConnectedOverlayScrollStrategy’)ScrollStrategy
viewportMargin: number@Input(‘cdkConnectedOverlayViewportMargin’)overlay窗口margin
width: number | string@Input(‘cdkConnectedOverlayWidth’)overlay 宽度
attach: EventEmitter@Output()overlay attach的时候回调
backdropClick: EventEmitter@Output()点击了overlay背景层的回调
detach: EventEmitter@Output()overlay detach的时候调用
overlayKeydown: EventEmitter@Output()overlay显示的时候有键盘按键按下
positionChange: EventEmitter@Output()overlay位置改变的时候的回调
dir: Directionoverlay里面的布局方向
overlayRef: OverlayRefoverlay对应的OverlayRef(对overlay的一个封装,类似ElementRef)

五 Overlay使用

       虽然咱们上面讲了一大堆,然而都是让我们知道什么去使用overlay。下面我们通过几个简单的例子来看下overlay怎么使用。例子里面没有写传递参数的情况,如果有传递参数的情况可以参考上一篇文章 Angular cdk 学习之 Portals

       非常重要:使用之前要加上Overlay库里面的css同时导入OverlayModule

添加Overlay库里面的css 文件,推荐在最外层的styles.css里面添加

@import '~@angular/cdk/overlay-prebuilt.css';

导入OverlayModule

import {NgModule} from '@angular/core';
...
import {OverlayModule, OverlayContainer, FullscreenOverlayContainer} from "@angular/cdk/overlay";
...

@NgModule({
    imports: [
        ...
        OverlayModule,
        ...
    ],
    ...
})
export class CdkOverlayModule {
}

       在实例之前我们先自定义一个非常简单的动态组件,后面的实例我们都会用到这个组件,注意哦这个动态组件除了在declarations里面需要申明,在entryComponents里面也需要申明。代码如下

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-overlay-panel',
  template: `
    <p class="wu-overlay-pane">Overlay展示</p>
  `,
  styles: [`
    .wu-overlay-pane {
      margin: 0;
      padding: 10px;
      border: 1px solid black;
      background-color: skyblue;
    }
  `]
})
export class OverlayPanelComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

@NgModule({
    ...
    declarations: [
        ...
        OverlayPanelComponent
    ],
    entryComponents: [
        OverlayPanelComponent
    ]
})
export class CdkOverlayModule {
}

5.1 把OverlayPanelComponent组件显示在屏幕中间

       这里主要是GlobalPositionStrategy的使用,以及hasBackdrop的使用。代码如下。

import {Component, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {Overlay, OverlayConfig} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {OverlayPanelComponent} from './panel/overlay-panel.component';

@Component({
    selector: 'app-cdk-overlay',
    template: `
        <!-- 全局显示 页面中心显示 (点击的时候显示) -->
        <button (click)="showOverlayGlobalPanelCenter()">页面中心显示</button>
    `,
    encapsulation: ViewEncapsulation.None,
    preserveWhitespaces: false,
})
export class CdkOverlayComponent {

    constructor(public overlay: Overlay
        , public viewContainerRef: ViewContainerRef) {
    }

    /**
     * overlay 在整个屏幕的中间显示
     */
    showOverlayGlobalPanelCenter() {
        // config: OverlayConfig overlay的配置,配置显示位置,和滑动策略
        const config = new OverlayConfig();
        config.positionStrategy = this.overlay.position()
            .global() // 全局显示
            .centerHorizontally() // 水平居中
            .centerVertically(); // 垂直居中
        config.hasBackdrop = true; // 设置overlay后面有一层背景, 当然你也可以设置backdropClass 来设置这层背景的class
        const overlayRef = this.overlay.create(config); // OverlayRef, overlay层
        overlayRef.backdropClick().subscribe(() => {
            // 点击了backdrop背景
            overlayRef.dispose();
        });
        // OverlayPanelComponent是动态组件
        // 创建一个ComponentPortal,attach到OverlayRef,这个时候我们这个overlay层就显示出来了。
        overlayRef.attach(new ComponentPortal(OverlayPanelComponent, this.viewContainerRef));
        // 监听overlayRef上的键盘按键事件
        overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {
            console.log(overlayRef._keydownEventSubscriptions + ' times');
            console.log(event);
        });
    }
}

5.2 把OverlayPanelComponent组件显示在屏幕上,自己控制位置

        GlobalPositionStrategy的使用,GlobalPositionStrategy自定义位置。

import {Component, Inject, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {Overlay, OverlayConfig} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {OverlayPanelComponent} from './panel/overlay-panel.component';
import {DOCUMENT} from '@angular/common';

@Component({
    selector: 'app-cdk-overlay',
    template: `
        <!-- 全局显示 页面中显示位置自己控制 -->
        <button (click)="showOverlayGlobalPanelPosition()">页面中显示,自己控制位置</button>
    `,
    encapsulation: ViewEncapsulation.None,
    preserveWhitespaces: false,
})
export class CdkOverlayComponent {

    globalOverlayPosition = 0;

    constructor(public overlay: Overlay
        , public viewContainerRef: ViewContainerRef
        , @Inject(DOCUMENT) public _document: any) {
    }

    /**
     * overlay 在整个屏幕位置,自己控制位置
     */
    showOverlayGlobalPanelPosition() {
        const config = new OverlayConfig();
        config.positionStrategy = this.overlay.position()
            .global()
            .left(`${this.globalOverlayPosition}px`) // 自己控制位置
            .top(`${this.globalOverlayPosition}px`);
        this.globalOverlayPosition += 30;
        config.hasBackdrop = true;
        const overlayRef = this.overlay.create(config);
        overlayRef.backdropClick().subscribe(() => {
            overlayRef.dispose(); // 点击背景关掉弹窗
        });
        overlayRef.attach(new ComponentPortal(OverlayPanelComponent, this.viewContainerRef));
    }
}

5.3 overlay里面显示ng-template内容

       怎么在overlay上显示ng-template里面的内容。

import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
import {Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
import {TemplatePortalDirective} from '@angular/cdk/portal';

@Component({
    selector: 'app-cdk-overlay',
    template: `
        <!-- 鼠标移入的时候显示 ng-template对应的内容,移出的时候不显示 -->
        <button style="margin-left: 10px" (mouseenter)="showOverlayPanelTemplate()"
                (mouseleave)="dismissOverlayPanelTemplate()">
            显示 ng-template 内容
        </button>
        <!-- ng-template overlay 将要显示的内容 -->
        <ng-template cdk-portal #overlayGlobalTemplate="cdkPortal">
            <p class="template-overlay-pane"> ng-temtortelliniTemplateplate显示 </p>
        </ng-template>
    `,
    styles: [`
        .template-overlay-pane {
            padding: 10px;
            border: 1px solid black;
            background-color: skyblue;
        }`],
    encapsulation: ViewEncapsulation.None,
    preserveWhitespaces: false,
})
export class CdkOverlayComponent {

    globalOverlayPosition = 0;
    private _overlayTemplateRef: OverlayRef;

    @ViewChild('overlayGlobalTemplate') templateGlobalPortals: TemplatePortalDirective;

    constructor(public overlay: Overlay) {
    }

    /**
     * 显示 ng-template 的内容
     */
    showOverlayPanelTemplate() {
        const config = new OverlayConfig();
        config.positionStrategy = this.overlay.position()
            .global()
            .centerHorizontally()
            .top(`${this.globalOverlayPosition}px`);
        this.globalOverlayPosition += 30;
        this._overlayTemplateRef = this.overlay.create(config);
        this._overlayTemplateRef.attach(this.templateGlobalPortals);
    }

    /**
     * 移除 ng-template 内容
     */
    dismissOverlayPanelTemplate() {
        if (this._overlayTemplateRef && this._overlayTemplateRef.hasAttached()) {
            this._overlayTemplateRef.dispose();
        }
    }
}

5.4 overlay依赖于某个视图(origin)显示

       FlexibleConnectedPositionStrategy的使用,怎么通过FlexibleConnectedPositionStrategy来控制显示的位置(ConnectedPosition)。同时也有ScrollStrategy的使用

import {Component, ElementRef, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {OverlayPanelComponent} from './panel/overlay-panel.component';

@Component({
    selector: 'app-cdk-overlay',
    template: `
        <!-- 依附某个组件或者template显示,鼠标移入的时候显示,移出来的时候不显示 -->
        <button style="margin-left: 10px" #connectComponentOrigin
                (mouseenter)="showOverlayPanelConnectComponent()"
                (mouseleave)="dismissOverlayPanelConnectComponent()">
            overlay connect 组件显示
        </button>
    `,
    encapsulation: ViewEncapsulation.None,
    preserveWhitespaces: false,
})
export class CdkOverlayComponent {

    private _overlayConnectRef: OverlayRef;

    @ViewChild('connectComponentOrigin') _overlayConnectComponentOrigin: ElementRef;

    constructor(public overlay: Overlay
        , public viewContainerRef: ViewContainerRef) {
    }

    /**
     * overlay connect origin 显示,依附某个组件显示
     */
    showOverlayPanelConnectComponent() {
        const strategy = this.overlay.position()
            .flexibleConnectedTo(this._overlayConnectComponentOrigin.nativeElement)
            .withPositions([{
                originX: 'center',
                originY: 'bottom',
                overlayX: 'center',
                overlayY: 'top',
                offsetX: 0,
                offsetY: 0
            }]); // 这么理解 origin 组件(依附空组件) 的那个点(originX, originY) 和 overlay组件的点(overlayX, overlayY)
        // 重合,从而确定overlay组件显示的位置
        strategy.withLockedPosition(true);
        const config = new OverlayConfig({positionStrategy: strategy});
        config.scrollStrategy = this.overlay.scrollStrategies.reposition(); // 更随滑动的策略
        this._overlayConnectRef = this.overlay.create(config);
        this._overlayConnectRef.attach(new ComponentPortal(OverlayPanelComponent, this.viewContainerRef));
    }

    dismissOverlayPanelConnectComponent() {
        if (this._overlayConnectRef && this._overlayConnectRef.hasAttached()) {
            this._overlayConnectRef.dispose();
        }
    }
}

5.5 指令使用

       cdk-overlay-origin和cdk-connected-overlay怎么配合起来使用,怎么把两个指令关联起来。

import {Component, ViewEncapsulation} from '@angular/core';
import {Overlay} from '@angular/cdk/overlay';

@Component({
    selector: 'app-cdk-overlay',
    template: `
        <button cdk-overlay-origin #trigger="cdkOverlayOrigin" (click)="isMenuOpen = !isMenuOpen">
            指令实现
        </button>

        <ng-template cdk-connected-overlay
                     [cdkConnectedOverlayOrigin]="trigger"
                     [cdkConnectedOverlayWidth]="500"
                     cdkConnectedOverlayHasBackdrop
                     [cdkConnectedOverlayOpen]="isMenuOpen"
                     (backdropClick)="isMenuOpen=false">
            <div class="menu-wrap">
                我是通过指令实现的Overlay
            </div>
        </ng-template>
    `,
    styleUrls: ['./cdk-overlay.component.less'],
    encapsulation: ViewEncapsulation.None,
    preserveWhitespaces: false,
})
export class CdkOverlayComponent {

    /**
     * overlay是否显示
     */
    isMenuOpen = false;

    constructor(public overlay: Overlay) {
    }

}


       关于cdk Overlay里面的内容咱们就先将这么多。里面还有很多高级的用法是我们没有讲到的。等待大伙去发掘。如果大家有疑问也可以留言。文章里面涉及的代码在都可以找到https://github.com/tuacy/angular-cdk-study

 类似资料: