tuckerjt07
tuckerjt07

Reputation: 922

Using CDK Overlay Container in ShadowDOM

I am attempting to use Angular Material components inside of a web component. Everything is working except for one edge case with the cdk-overlays. So far I have had success using an override of _createContainer() to append the overlay container to a div inside of the shadowroot so that the styles will apply. However if I hide the parent element with an *ngIf and then toggle it back the overlay container is present in the DOM but it and its contained elements are not visible.

Is there something I'm missing where I need to manually clear or toggle something to get that behavior to work correctly?

Code is based off of this answer: Add MatDialog popup to Angular Root and not to body

@Injectable({ providedIn: "root" })
export class AppOverlayContainer extends OverlayContainer implements OnDestroy {
  constructor(@Inject(DOCUMENT) _document: any) {
    super(_document);
  }

  ngOnDestroy() {
    super.ngOnDestroy();
  }

  getRootElement(): Element {
    return this._document
      .querySelector("web-component-tag")
      .shadowRoot.querySelector("#angular-app-root");
  }

  createContainer(): void {
    this._createContainer();
  }

  protected _createContainer(): void {
    super._createContainer();
    this._appendToRootComponent();
  }

  private _appendToRootComponent(): void {
    if (!this._containerElement) {
     return;
    }
    const rootElement = this.getRootElement();
    const parent = rootElement || this._document.body;
    parent.appendChild(this._containerElement);
  }
}

HTML

<ng-container *ngIf="toggleWidget$ | async">


<app-conversations
    id="angular-app-root"
    [toggleWidget$]="toggleWidget$"
    [contextTitle]="contexttitle"
    [contexts]="contexts"
    [preloadConversations]="subscribeonload"
    [widgetIdentifier]="uniqueId"
  ></app-conversations>
</ng-container>
<ng-container
  *ngIf="(toggleWidget$ | async) === false && !overridelaunchbutton"
>
  <button
    mat-fab
    class="message-button"
    color="primary"
    (click)="toggleWidget$.next(true)"
  >
    <mat-icon svgIcon="message"></mat-icon>
    <div class="unread-pip" *ngIf="hasUnreadMessages">
      {{ unreadMessageCount }}
    </div>
  </button>
</ng-container>
<ng-container *ngIf="overridelaunchbutton">
  <button
    mat-icon-button
    [ngClass]="launchbuttonclass"
    [color]="launchbuttoncolor"
    (click)="toggleWidget$.next(true)"
  >
    <mat-icon svgIcon="{{ launchbuttonicon }}"></mat-icon>
    <div class="unread-pip" *ngIf="hasUnreadMessages">
      {{ unreadMessageCount }}
    </div>
  </button>
</ng-container>

Upvotes: 4

Views: 3202

Answers (3)

MatGar
MatGar

Reputation: 11

The method worked for me, there was one problem though:

Application used module federation, web component was opened on separate route. When visiting the route for the first time, all was good, but when changing to other route and then revisiting web component, it stopped working.

The problem was that div with class .cdk-overlay-container was created during the first visit only. Next visit resulted in missing div. There was no container some element could be attached to.

As a fix:

  1. inject(AppOverlayContainer) into AppComponent,
  2. execute createContainer method in ngAfterViewInit hook.

To make sure that AppOverlayContainer and OverlayContainer share the same instance, do the following in providers section of an AppModule

AppOverlayContainer, { provide: OverlayContainer, useExisting: AppOverlayContainer }

Upvotes: 0

Vijay Nirmal
Vijay Nirmal

Reputation: 5857

Reason for this behavior is because of the base type you are inheriting. Even when the whole web component is destroyed, it will still have the cached element you created in _createContainer.

There are 2 fixes for it

  1. Either implement the whole class yourself without inheriting OverlayContainer
  2. Or override the getContainerElement while is inside the based class like below
    override getContainerElement(): HTMLElement {
      if (!this._containerElement || !this._containerElement.isConnected) {
        this._createContainer();
      }
    
      return this._containerElement;
    }
    

Upvotes: 0

tuckerjt07
tuckerjt07

Reputation: 922

I never found root to this beyond the weird behavior occurs if the overlay container is wrapped by an *ngIf. When the *ngIf is shown the first time, toggled to not be visible and then displayed again it causes an issue with the content being rendered inside the container. Moving it outside of the *ngIf fixed the issue in my case.

Upvotes: 1

Related Questions