How can i make a MatDialog draggable / Angular Material

27,585

Solution 1

Update since Angular Material 7

You can simply use cdkDrag directive from @angular/cdk/drag-drop

dialog.html

<h1 mat-dialog-title 
   cdkDrag
   cdkDragRootElement=".cdk-overlay-pane" 
   cdkDragHandle>
     Hi {{data.name}}
</h1>

Stackblitz Example

Previous answer:

Since there is no official solution for that, I'm going to write custom directive that will be applied on a dialog title and do all job for us:

dialog.html

@Component({
  selector: 'app-simple-dialog',
  template: `
    <h1 mat-dialog-title mat-dialog-draggable-title>Hi {{data.name}}</h1>
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    <div mat-dialog-content>
      ...
    </div>
    <div mat-dialog-actions>
      ...
    </div>
  `
})
export class SimpleDialogComponent {

Ng-run Example

enter image description here

The basic idea here is to use MatDialogRef.updatePosition method for updating dialog position. Under the hood this method changes margin-top|margin-left values and someone can argue that it's not the best option here and it would be better if we used transform but I simply want to show an example of how we can do it without some tricks and with the help of the built-in services.

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.

dialog-draggable-title.directive.ts

import { Directive, HostListener, OnInit } from '@angular/core';
import { MatDialogContainer, MatDialogRef } from '@angular/material';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { takeUntil } from 'rxjs/operators/takeUntil';
import 'rxjs/add/observable/fromEvent';
import { take } from 'rxjs/operators/take';

@Directive({
  selector: '[mat-dialog-draggable-title]'
})
export class DialogDraggableTitleDirective implements OnInit {

  private _subscription: Subscription;

  mouseStart: Position;

  mouseDelta: Position;

  offset: Position;

  constructor(
    private matDialogRef: MatDialogRef<any>,
    private container: MatDialogContainer) {}

  ngOnInit() {
    this.offset = this._getOffset();
  }

  @HostListener('mousedown', ['$event'])
  onMouseDown(event: MouseEvent) {
    this.mouseStart = {x: event.pageX, y: event.pageY};

    const mouseup$ = Observable.fromEvent(document, 'mouseup');
    this._subscription = mouseup$.subscribe(() => this.onMouseup());

    const mousemove$ = Observable.fromEvent(document, 'mousemove')
      .pipe(takeUntil(mouseup$))
      .subscribe((e: MouseEvent) => this.onMouseMove(e));

    this._subscription.add(mousemove$);
  }

  onMouseMove(event: MouseEvent) {
      this.mouseDelta = {x: (event.pageX - this.mouseStart.x), y: (event.pageY - this.mouseStart.y)};

      this._updatePosition(this.offset.y + this.mouseDelta.y, this.offset.x + this.mouseDelta.x);
  }

  onMouseup() {
    if (this._subscription) {
      this._subscription.unsubscribe();
      this._subscription = undefined;
    }

    if (this.mouseDelta) {
      this.offset.x += this.mouseDelta.x;
      this.offset.y += this.mouseDelta.y;
    }
  }

  private _updatePosition(top: number, left: number) {
    this.matDialogRef.updatePosition({
      top: top + 'px',
      left: left + 'px'
    });
  }

  private _getOffset(): Position {
    const box = this.container['_elementRef'].nativeElement.getBoundingClientRect();
    return {
      x: box.left + pageXOffset,
      y: box.top + pageYOffset
    };
  }
}


export interface Position {
  x: number;
  y: number;
}

Remember location

Since @Rolando asked:

I want to 'remember' where the modal was positioned so that when the button to open the modal is hit, the modal opens up where 'it was last located'.

let's try to support it.

In order to do that you can create some service where you will store dialog positions:

modal-position.cache.ts

@Injectable()
export class ModalPositionCache {
  private _cache = new Map<Type<any>, Position>();

  set(dialog: Type<any>, position: Position) {
    this._cache.set(dialog, position);
  }

  get(dialog: Type<any>): Position|null {
    return this._cache.get(dialog);
  }
}

now you need to inject this service in our directive:

dialog-draggable-title.directive.ts

export class DialogDraggableTitleDirective implements OnInit {
  ...

  constructor(
    private matDialogRef: MatDialogRef<any>,
    private container: MatDialogContainer,
    private positionCache: ModalPositionCache
  ) {}

  ngOnInit() {
    const dialogType = this.matDialogRef.componentInstance.constructor;
    const cachedValue = this.positionCache.get(dialogType);
    this.offset = cachedValue || this._getOffset();
    this._updatePosition(this.offset.y, this.offset.x);

    this.matDialogRef.beforeClose().pipe(take(1))
      .subscribe(() => this.positionCache.set(dialogType, this.offset));
  }

As you can as soon as dialog is going to be closed i save last offset.

Ng-run Example

This way dialog remembers where it was closed

enter image description here

Solution 2

in your module, import the cdk drag

import { DragDropModule } from '@angular/cdk/drag-drop';

and in html where dialog is for example, just add to any html element. i have added to first element and then i can drag the dialog anywhere i pick.

<mat-dialog-content cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
  content...
</mat-dialog-content>

Solution 3

In case anyone else runs into this, it's actually a noop to use cdkDrag and cdkDragHandle on the same element as is done in the examples here. The relevant GH Issue can be found here:

https://github.com/angular/components/issues/18984

Share:
27,585

Related videos on Youtube

HansDampfHH
Author by

HansDampfHH

Updated on July 09, 2022

Comments

  • HansDampfHH
    HansDampfHH almost 2 years

    Is it possible to make a Angular Material Dialog draggable? I installed angular2-draggable and can of course use the functionality on all other elements.

    But because the dialogs are dynamically created i can not use ngDraggable on a special element or can use a template variable.

    • ark
      ark over 6 years
      Did you got how to make mat-dialog draggable?
    • HansDampfHH
      HansDampfHH over 6 years
      No, there seems to be no solution until now. Maybe with upcoming Angular Material 5.
    • ark
      ark over 6 years
      Its seems like jquey ui needs to be used to achieve that..correct me if i'm wrong
  • TmTron
    TmTron over 5 years
    I had to set the offset in ngAfterViewInit() instead of ngOnInit() - now it work fine!
  • Hien Nguyen
    Hien Nguyen about 5 years
    I tried your demo, but how can I drag from one screen to another screen when I have 2 monitors?
  • Xander
    Xander about 4 years
    I've tried your updated solution with Angular 9 but the whole dialog acts as a drag handle, not just the title. Any thoughts?
  • yurzui
    yurzui about 4 years
    @Xander Can you please reproduce it in stackblitz?
  • Xander
    Xander about 4 years
    @yurzui Unfortunately I cannot. I used the Material 9 stackblitz example for dialogs and added these 3 directives and it works fine. So it's probably something specific to my app but I have no idea what.. at my wit's end here.
  • Andrew T Finnell
    Andrew T Finnell almost 4 years
    I just converted this application to Angular 9 and 10, and it stops dragging. The real issue is that there aren't any errors, warnings or other problems that show it. It just doesn't work. The cdk directives do not work if they are on a component that is opened with DialogRef. The fix is to remove cdkDragHandle from the h1 and leave it off because cdkDrag and cdkDragHandle cant be on the same element. It worked before due to a bug.
  • Mitulát báti
    Mitulát báti almost 4 years
    @Xander I have the same situation.
  • Admin
    Admin almost 4 years
    hi, can someone answer this question? thanks stackoverflow.com/questions/62807800/…
  • JW.
    JW. over 3 years
    Note that in some cases it seems necessary to move the cdkDrag and cdkDragRootElement to a parent element of cdkDragHandle, to prevent the whole dialog from being draggable.
  • yglodt
    yglodt about 3 years
    I suggest adding style="cursor: move" to the <h1> so the user understands the element is draggable.