有没有可能在特定的DOM元素上打开angular/Material2 Snackbar?

5
3个回答

7
我们无法使用 viewContainerRef 选项来将 Snackbar 附加到自定义容器。根据 Angular Material 的 文档,此选项是:

作为 snackbar 的父级以进行依赖注入的视图容器。 ###注意:这不会影响 snackbar 插入 DOM 的位置。###

ComponentPortal 文档 中,我们可以找到 viewContainerRef 的更详细描述:

所附加组件在 Angular 的逻辑组件树中应该存在的位置。这与组件呈现的位置不同,后者由 PortalOutlet 确定。当宿主位于 Angular 应用程序上下文之外时,必须指定源。

在打开 Snackbar 时检查 DOM,我们会发现以下组件层次结构:
  1. Body - <body>
  2. OverlayContainer - <div class="cdk-overlay-container"></div>
  3. Overlay - <div id="cdk-overlay-{id}" class="cdk-overlay-pane">
  4. ComponentPortal - <snack-bar-container></snack-bar-container>
  5. Component - 我们的 Snackbar,例如:this.snackbar.open(message, action)
现在,我们实际上需要更改的是 OverlayContainer 在 DOM 中的位置,根据 文档,它是:

所有单独的覆盖元素都渲染的容器元素。

默认情况下,覆盖容器直接附加到文档主体。

从技术上讲,可以像 文档 中描述的那样,通过使用自定义提供程序来提供 自定义 覆盖容器。
@NgModule({
  providers: [{provide: OverlayContainer, useClass: AppOverlayContainer}],
 // ...
})
export class MyModule { }

一个简单的实现可以在这里找到。


但是,不幸的是,它是一个单例对象,并且可能会引起问题:

MatDialogMatSnackbarMatTooltipMatBottomSheet和所有使用 OverlayContainer 的组件将在此 AppOverlayContainer 中打开:

由于这种情况远非理想,我编写的自定义提供程序(AppOverlayContainer)需要根据需要仅更改overlaycontainer的位置。如果没有调用,则允许overlaycontainer附加到body上。

代码如下:

import { Injectable } from '@angular/core';
import { OverlayContainer } from '@angular/cdk/overlay';

@Injectable({
 providedIn: 'root'
})
export class AppOverlayContainer extends OverlayContainer {
  appOverlayContainerClass = 'app-cdk-overlay-container';

  /**
   * Append the OverlayContainer to the custom wrapper element
   */
  public appendToCustomWrapper(wrapperElement: HTMLElement): void {
    if (wrapperElement === null) {
      return;
    }
    
    // this._containerElement is 'cdk-overlay-container'
    if (!this._containerElement) {
      super._createContainer();
    }

    // add a custom css class to the 'cdk-overlay-container' for styling purposes
  this._containerElement.classList.add(this.appOverlayContainerClass);
   
    // attach the 'cdk-overlay-container' to our custom wrapper
    wrapperElement.appendChild(this._containerElement);
  }

  /**
   * Remove the OverlayContainer from the custom element and append it to body
   */
  public appendToBody(): void {
    if (!this._containerElement) {
     return;
    }

    // remove the custom css class from the 'cdk-overlay-container'
    this._containerElement.classList.remove(this.appOverlayContainerClass);

    // re-attach the 'cdk-overlay-container' to body
    this._document.body.appendChild(this._containerElement);
  }

}

因此,在打开 Snackbar 之前,我们必须调用:
AppOverlayContainer.appendToCustomWrapper(HTMLElement).

该方法将overlaycontainer附加到我们的自定义包装元素。

当 snackbar 关闭时,最好调用:

AppOverlayContainer.appendToBody();

该方法将从我们的自定义包装元素中移除overlaycontainer并重新附加到body

此外,由于AppOverlayContainer将被注入到我们的组件中并需要保持单例模式,因此我们将使用useExisting语法来提供它:

providers: [
   {provide: OverlayContainer, useExisting: AppOverlayContainer}
]

示例:

如果我们想要在自定义容器#appOverlayWrapperContainer1中显示 Snackbar:

<button mat-raised-button 
  (click)="openSnackBar(appOverlayWrapperContainer1, 'Snackbar 1 msg', 'Action 1')">
  Open Snackbar 1
</button>
<div class="app-overlay-wrapper-container" #appOverlayWrapperContainer1>
  Snackbar 1 overlay attached to this div
</div>

在我们的组件.ts中,我们有:
import { AppOverlayContainer } from './app-overlay-container';

export class SnackBarOverviewExample {

  constructor(
    private _snackBar: MatSnackBar,
    private _appOverlayContainer: AppOverlayContainer
  ) { }

  openSnackBar(
    overlayContainerWrapper: HTMLElement, 
    message: string, 
    action: string
  ) {

    if (overlayContainerWrapper === null) {
      return;
    }
  
  this.appOverlayContainer.appendToCustomWrapper(overlayContainerWrapper);

    this.snackbarRef = this._snackBar.open(message, action, {
      duration: 2000
    });
  }
}

此外,我们需要一些必须插入应用程序通用样式表中的 CSS:
.app-cdk-overlay-container {
  position: absolute;
}

完整代码和演示已经上传至 Stackblitz,这里我实现了多个 Snackbar 容器:

https://stackblitz.com/edit/angular-ivy-wxthzy


2
是的。 MatSnackBarConfigviewContainerRef 属性将接受一个 ViewContainer,用作将其 Portal 附加到的位置。
通过 ViewChild 查询或将其注入放置在元素上的指令中,可以获取元素的 ViewContainerRef。前者是更容易的选项:
component.html:
<div #wrapperContainer> </div>

component.ts:

@ViewChild('wrapperContainer', { read: ViewContainerRef }) container: ViewContainerRef;


ngAfterViewInit() {

  this.something = this.snackBarService.open(
      'message text',
      'button text',
      { viewContainerRef: this.container}
  );

}

起初我以为这正是我所需要的,但当我试图更改它时,似乎没有任何作用...目前似乎还没有可用的解决方案(https://github.com/angular/material2/issues/7764) - Daniel Delgado
嗯...我知道这个在 MatDialog 中可以工作,所以我想吐司条也应该一样。我猜两者都与它们的 Portal 有关。你可以像平常一样打开 snackbar,给它一个组件用作 snackbar,从该组件获取 templateRef,然后从 CDK 创建一个新的 Portal 并在那里呈现 templateRef,但这是完全错误的。不幸的是,在这个 bug 得到解决之前,你需要使用 hacky 的方法来修复它。 - joh04667

1
你应该查看他们的API,其中有参数viewContainerRef或者你可以使用verticalPosition定义高度。

verticalPosition 只接受两个值:topbottom - Edric
ViewContainerRef不起作用...目前似乎还没有可用的解决方案(github.com/angular/material2/issues/7764) - Daniel Delgado

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接