【Angular】ドラッグして動くモーダル画面実装

2020年2月7日

modal画面実装して見ました

  • Angularでモーダル画面を実装したサンプルはいくつかありますが、ドラッグで動かせるモーダルのサンプルは特になかったので、実装して見ました。
  • ついでに、親コンポーネントと子コンポーネントでデータの受け渡しもできるように実装して見ました。
  • 解説なんかいらんよって方は、ソースは以下にあるのでよかったら参考にしてください。
https://github.com/kanazawanao/angular8-modal-sample

一応処理について解説

親コンポーネント側

app.component.html

  • ngIfを利用して、ボタンクリックによって、コンポーネントの表示、非表示を切り替える。
  • モーダルクローズ時に、子コンポーネントから渡された値を{{ closeTest }}で表示する。
<div class="footer">
  <input
    id="footer_button"
    type="button"
    value="open modal"
    (click)="onClick($event)"
  />
</div>
<div *ngIf="showModal" class="overlay">
  <app-modal-template
    [modalContent]="modalContent"
    (finishEvent)="finishEvent($event)"
  >
  </app-modal-template>
</div>
{{ closeTest }}

app.component.ts

  • finishEventで子コンポーネントでの処理が終わった際に何をするか記載する。
  • 子コンポーネント側ではeventEmitterを利用して、emitを行うと、親コンポーネントのfinishEventが実行される。
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  showModal: boolean;
  modalContent: string;
  closeTest: string;
  constructor() {}

  ngOnInit() {
    this.showModal = false;
    this.modalContent = 'modal sample';
  }

  onClick(e) {
    this.showModal = true;
  }

  finishEvent(val: string) {
    this.showModal = false;
    this.closeTest = val;
  }
}

app.component.css

  • 親コンポーネント側でoverlayをの実装をしておく。
  • そうしておかないと、@HostListnerでドラッグしてモーダル移動処理を書く際に、モーダル以外のoverlay部分でドラッグしてもモーダル動いちゃう。
.overlay {
  top: 0;
  left: 0;
  margin: 0;
  padding: 0;
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: auto;
  background: rgba(0, 0, 0, 0.4);
  display: flex;
  justify-content: center;
  align-items: center;
}

子コンポーネント

modal-template.component.html

  • これがモーダルとして表示される。
  • 親コンポーネントから渡された値を{{ modalContent }}で表示する。
<div #modal class="modal">
  <div class="header">
    <label id="header_label">header title</label>
  </div>
  <div class="contents">
    {{ modalContent }}
  </div>
  <div class="footer">
    <input id="close_button" type="button" value="close" (click)="onClick()" />
  </div>
</div>

modal-template.component.ts

  • モーダルの移動処理を、mousedown、mouseup、document:mousemoveによって行う。(書きながら思ったけど、mousemoveだけdocument:が付いている、、、果たして必要なのか、、、
  • mousemoveはドラッグなのかどうか判定を行えないので、マウスダウン〜マウスアップまでのマウス移動処理をドラッグと判定するようにしておく。
  • 移動自体は、モーダルが今いる地点から、X、Y方向それぞれ動いた分だけを加算するという単純なもの。
  • ちなみにコンストラクタでconstructor(private el: ElementRef) {} みたいにして、el.nativeElementに対してstyleを指定しようとすると、styleが崩れるので注意。
  • style崩れると実行結果が面白いので、気になる方はやって見てください笑(私は仕事中に吹き出してしまいました)
import {
  Component,
  OnInit,
  Output,
  EventEmitter,
  Input,
  HostListener,
  ElementRef,
  ViewChild
} from '@angular/core';

@Component({
  selector: 'app-modal-template',
  templateUrl: './modal-template.component.html',
  styleUrls: ['./modal-template.component.css']
})
export class ModalTemplateComponent implements OnInit {
  @ViewChild('modal', { static: true }) modalRef: ElementRef;

  @Input()
  modalContent: string;

  drag: boolean;

  @Output()
  finishEvent: EventEmitter<string> = new EventEmitter<string>();
  constructor() {}

  ngOnInit() {
    this.drag = false;
  }

  public onClick() {
    this.finishEvent.emit('finish!');
  }

  @HostListener('mousedown', ['$event']) onMouseDown(event: MouseEvent) {
    this.drag = true;
  }

  @HostListener('mouseup', ['$event']) onMouseUp(event: MouseEvent) {
    this.drag = false;
  }

  @HostListener('document:mousemove', ['$event']) onMouseMove(e: MouseEvent) {
    if (this.drag) {
      this.modalRef.nativeElement.style.top =
        this.modalRef.nativeElement.offsetTop + e.movementY + 'px';
      this.modalRef.nativeElement.style.left =
        this.modalRef.nativeElement.offsetLeft + e.movementX + 'px';
    }
  }
}

modal-template.component.css

  • モーダル表示用のCSSで、.modal内の、left: 50%、top: 50%、transform: translate(-50%, -50%)という指定でからなず真ん中に表示されるように指定している。
.modal {
  width: 30%;
  height: 30%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: rgb(241, 245, 247);
  box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.4);
  border: solid 1px rgb(0, 0, 0);
}

.header {
  height: 20%;
  width: 100%;
  font-size: 1.2em;
  border-bottom: solid 1px black;
  display: flex;
  justify-content: center;
  align-items: center;
}

.header > #header_label {
  display: block;
}

.contents {
  height: 60%;
  width: 100%;
  text-align: center;
  font-size: 1.8em;
}

.footer {
  height: 20%;
  width: 100%;
  border-top: solid 1px black;
  display: flex;
  justify-content: center;
  align-items: center;
}

.footer > #close_button {
  display: block;
  width: 30%;
  height: 70%;
  font-size: 1.2em;
}
ここまで書いておきながら、実際にはAngular MaterialとAngular CDK使えば簡単に実装できるんですけどね。。。