5

component: https://material.angular.io/components/snack-bar/examples

I have a page that is a parent section ('#wrapper_container'). And I want to popup the snack bar in the centre of a child element of '#wrapper_element'.

Thanks

Edric
  • 24,639
  • 13
  • 81
  • 91
Daniel Delgado
  • 4,813
  • 5
  • 40
  • 48

3 Answers3

7

We can't use viewContainerRef option in order to attach the snackbar to a custom container. From Angular Material docs, this option is:

The view container that serves as the parent for the snackbar for the purposes of dependency injection. ###Note: this does not affect where the snackbar is inserted in the DOM.###

From ComponentPortal documentation, we can found a more detailed description of viewContainerRef:

Where the attached component should live in Angular's logical component tree. This is different from where the component renders, which is determined by the PortalOutlet. The origin is necessary when the host is outside of the Angular application context.

When inspecting the DOM when a snackbar is opened, we'll find the following component hierarchy:

  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 - our Snackbar, which is opened like: this.snackbar.open(message, action).

Now, what we really need to change is the position in the DOM of the OverlayContainer, which, according to docs, is:

the container element in which all individual overlay elements are rendered.

By default, the overlay container is appended directly to the document body.

Technically, it is possible to provide a custom overlay container, as described in documentation, by using a custom provider:

@NgModule({
  providers: [{provide: OverlayContainer, useClass: AppOverlayContainer}],
 // ...
})
export class MyModule { }

A simple implementation would be the one posted here.


But, unfortunately, it is a singleton and creates a potential problem:

MatDialog, MatSnackbar, MatTooltip, MatBottomSheet and all the components that use the OverlayContainer will open within this AppOverlayContainer:

Since this situation is far from ideal, the custom provider that I've wrote (AppOverlayContainer) needs to change the position of the overlaycontainer only on demand. If not called, it will let the overlaycontainer to be appended to the body.

The code is:

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);
  }

}

So, before opening a snackbar, we must call:

AppOverlayContainer.appendToCustomWrapper(HTMLElement).

method, which attaches the overlaycontainer to our custom wrapper element.

When snackbar closes, it is ideal to call:

AppOverlayContainer.appendToBody();

method, which removes the overlaycontainer from our custom wrapper element and re-attaches it to body.

Also, since AppOverlayContainer would be injected in our component and needs to remain a singleton, we'll provide it by using useExisting syntax:

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

Example:

If we want to display the snackbar in a custom container, #appOverlayWrapperContainer1:

<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>

In our component .ts we have:

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
    });
  }
}

Also, we need some css that must be inserted in the app generic stylesheet:

.app-cdk-overlay-container {
  position: absolute;
}

Full code and demo are on Stackblitz, were I've implemented multiple snackbar containers:

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

andreivictor
  • 7,628
  • 3
  • 48
  • 75
2

Yes. The viewContainerRef property of MatSnackBarConfig will accept a ViewContainer to use as a place to attach it's Portal onto.

Getting a ViewContainerRef of an element can be done through a ViewChild query or by injecting it into a directive that you place on the element. The former is an easier option:

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}
  );

}
joh04667
  • 7,159
  • 27
  • 34
  • At first i thought this would be exactly what i need but when i tried to change it, it didn't seem to do anything... It seems that there is no solution available yet (https://github.com/angular/material2/issues/7764) – Daniel Delgado Jul 24 '18 at 22:10
  • hmm....I knew this worked with `MatDialog`, so I figured the snack bar would work the same. I guess the two are related with their `Portal`s.You *could* open the snackbar as normal, give it a component to use as the snackbar, get a `templateRef` from said component, and then create a new `Portal` from the CDK and render the `templateRef` there, but that's a total anti-pattern. Unfortunately, you're looking at a hacky fix until this bug gets addressed. – joh04667 Jul 25 '18 at 02:22
1

You should check their API, there's the parameter viewContainerRef or you can define the height with verticalPosition.