2

I am trying to get the screen dimensions of a just opened Angular Material dialog box. What is the best way to do that?

Background: I am using Angular Material 5.2.5 and am unable to upgrade. If a dialog box is too tall for the window, it gets clipped at the top and the bottom and there is no way to reach the action buttons at the bottom. See Dialog position and size problems on GitHub.

When the dialog opens, I would like to check its position to see if any part of it is off screen. If so, I will apply a class to make the dialog full-screen and adjust the scrollable parts so the users can get to everything. (Other ideas welcome.)

What I've Tried:

1. I tried using a template variable on a wrapper element:

<button mat-raised-button (click)="showDialog()">Click me to show dialog</button>

<ng-template #dialogTemplate>
  <div #dialogFrame class="dialog-frame">
    <div> ... dialog content ... </div>
  </div>
</ng-template>

Then in the component's showDialog() function:

...
export class dialogButtonComponent implements OnInit {
  @ViewChild('dialogTemplate') private dialogModal: TemplateRef<any>;
  @ViewChild('dialogFrame', {read: ElementRef}) private dialogFrame: ElementRef;
  ...
  showDialog(config: MatDialogConfig = {}) {
    this.dialogRef = this.dialog.open(this.dialogModal, dialogConfig);
    const box = this.dialogFrame.nativeElement.getBoundingClientRect();
      // this.dialogFrame is undefined when this executes
  }
}

2. Based on this answer, I tried injecting MatDialogContainer to get a reference:

We also need to inject MatDialogContainer in our directive so that we can get initial position of dialog container. We have to calculate initial offset because Angular material library uses flex to center dialog and it doesn't get us specific top/left values.

Specifically:

const box = this.container['_elementRef'].nativeElement.getBoundingClientRect();

Where this.container is the injected MatDialogContainer. I tried doing this in my component, but had trouble injecting it. I haven't worked with directives as much, so I'm looking into that.

3. On the dialogRef returned from opening the dialog box, access dialogRef._containerInstance._elementRef:

...
const dialogRef = this.dialog.open(this.overlayModal, dialogConfig);
const dialogBoxElement = dialogRef._containerInstance._elementRef;

// dialogBoxElement now has an ElementRef that I can call getBoundingClientRect() on.

This actually works locally, but it won't build. The problem is that there's a Typescript error because "Property '_elementRef' is private and only accessible within class 'MatDialogContainer'."


I hope there is a simpler way to get the screen coordinates of an Angular Material dialog box. Any ideas?

Scott McD.
  • 129
  • 1
  • 13
  • In your dialog component put private var for elementRef in CTOR. Then put in properties for width and height or emit their values. The nativeElment property you want is actualwidth and actualheight. – JWP Apr 07 '20 at 23:26
  • @JohnPeters - Thanks! When I inject elementRef in the constructor function, the information I get is for the button you click to bring up the dialog box. (See number 1 above.) The box itself is in the template and given a template variable - it's not a separate component. It gets appended to the DOM and isn't a child of the button, so I can't drill down to find it. I suppose I could pull the template out and have the contents of the box be an actual separate component, but that would introduce other problems as well. – Scott McD. Apr 08 '20 at 21:04
  • Can you post simple solution to [stackblitz](https://stackblitz.com/edit/angular-te1ua4) so we can see the problem? – JWP Apr 08 '20 at 21:54

2 Answers2

0

I realise this is a 3 year old question, but here goes. Your 3 attempt would work if the _elementRef property wasnt private. However, private is only used by Typescript during the build process, so you could bypass this with the following -

const dialogBoxElement = (<any>dialogRef._containerInstance)._elementRef;

OK, its not neat & some would argue against it. But it also works.

daveD
  • 869
  • 1
  • 7
  • 24
0

This is also a late response, but this is how I was able solve this problem in Angular v14. Note that this solution uses legacy Angular Material components so it may not work 1:1 for the new MDC-based components in v15+.

In my dialog component I use a custom close() function to close the dialog and save its position in local storage. I invoke the close() function from a button in the template and also bind it to events so the dialog will close on Escape and if the backdrop is clicked. The local storage key is saved as a static property in the component class so I can reference it later:

import { Component, OnInit, ElementRef } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';

@Component({
  selector: 'app-my-dialog-component',
  templateUrl: './my-dialog.component.html',
  styleUrls: ['./my-dialog.component.scss']
})
export class MyDialogComponent implements OnInit {
  constructor(
    private _dialogRef: MatDialogRef<MyDialogComponent>,
    private _elementRef: ElementRef
  ){}
  
  // Local storage key.
  static readonly positionLocalStorageKey = "MyDialogComponentDialogPosition";

  // Bind `close()` to events.
  ngOnInit() {
    // Close on 'Escape' key press.
    this._dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => {
      if (event.key === "Escape") this.close();
    });

    // Close on backdrop click.
    this._dialogRef.backdropClick().subscribe(() => this.close());
  }
  
  // Close and save the position of the dialog.  
  close(): void {
    let element: HTMLElement = this._elementRef.nativeElement;
    let dialog = element.parentElement;
    let dialogPosition = dialog.getBoundingClientRect();
    try { localStorage.setItem(EditComponentDialogComponent.positionLocalStorageKey, JSON.stringify(dialogPosition)) } catch {}
    this._dialogRef.close();
  }
}

I then check local storage when opening the dialog to see if a position should be provided:

import { MyDialogComponent } from './my-dialog/my-dialog.component.ts';
import { MatDialog } from '@angular/material/dialog';

@Component({
  selector: 'app-some-other-component',
  templateUrl: './some-other.component.html',
  styleUrls: ['./some-other.component.scss']
})
export class SomeOtherComponent {
  constructor(private _dialog: MatDialog){}

  openDialog(): void {
    // Get last dialog position if defined.
    const dialogPositionKey: string = MyDialogComponent.positionLocalStorageKey;
    let dialogPosition: DOMRect | null;
    let left: string;
    let top: string;
    try { dialogPosition = JSON.parse(localStorage.getItem(dialogPositionKey)) } catch {}
    if (dialogPosition !== null) {
      left = `${dialogPosition.left.toString()}px`;
      top = `${dialogPosition.top.toString()}px`;
    }

    // Open the dialog.
    const dialogRef = this._dialog.open<MyDialogComponent>(MyDialogComponent, 
      {
        disableClose: true,
        position: { left, top }
      }
    );
  }
}

The dialogs position/state does not have to be saved in local storage, but this was the easiest solution for me when tackling this problem.