しかたらむーの てっくぶろ

組み込みソフトの古参エンジニアがモダンなフロントエンドエンジニアに転生 その進化の記録です

Vue.js + Typescriptで作る『モーダルダイアログ』

はじめに

UIを構築において必要不可欠な要素とも言ってよいモーダルダイアログをVueとTypeScriptで作ってみましたので記事にまとめておきます。作成に当たってはこの公式サイトの例がシンプルでとても参考になったので、これをさらにシンプルにして要点を絞って解説してみます。

出来上がりはこんな感じ!

いつもと同じようにまずは、出来上がりのイメージです。
ボタンクリックでモーダルダイアログを表示、OKで入力テキストを返しダイアログを閉じてテキストを表示、Cancelで何もせずダイアログを閉じる。といった振る舞いのサンプルに挑戦です。

f:id:Shikataramuno:20181130093123p:plain
モーダルダイアログの出来上がりイメージ
コードはここです。 GitHub - Shikataramuno/modal-dialog-sample at sample

モーダルダイアログの出し方(考え方)

公式サイトを見て、私なりに理解した内容です。
モーダルダイアログを子コンポーネントとして定義しておき、親コンポーネント側でv-ifディレクティブを使って子コンポーネントの表示を制御するというアプローチです。

コンポーネント(モーダルダイアログコンポーネント側)のスタイル指定でz-indexを大きな値にして画面全体にマスクをかけ、その上にダイアログの要素を配置するようにします。こうすることで、子コンポーネントを表示させたときに親コンポーネントがマスクされ、子コンポーネントがモーダルダイアログとして表示されるようになります。ダイアログを閉じるときには子コンポーネントから親コンポーネントへイベントをemitし、v-ifディレクティブの条件を非表示に切り替える様にします。

では、簡単なサンプルを作りながら確認していきしょう。

開発環境

使用したツールは以下のとおり。(他の記事と同様です。)

> node --version
v8.9.4
> npm --version
5.6.0
> vue --version
3.1.1

プロジェクトの作成

以下のコマンドでプロジェクトを作成します。

vue create moda-dialog-sample

パラメタの指定はこの記事を参照してください。

techblo.shikataramuno.com

コンポーネントの構成

作成したプロジェクトのsrc/componentsフォルダにダイアログを呼び出す側のコンポーネントとダイアログ本体のコンポーネントをそれぞれ定義します。

src
│  App.vue
│  main.ts
│  shims-tsx.d.ts
│  shims-vue.d.ts
│  
├─assets
│      logo.png
│      
├─components
│      ModalDialog.vue
│      ModalDialogSample.vue
│      
└─style
        modal.css

ModalDialogSample.vueがダイアログを呼び出す側(i.e. 親)コンポーネント、ModaiDialog.vueがダイアログ本体(i.e. 子)コンポーネントとなります。

コンポーネント(ModalDialogSample.vue)

HTML

まずはHTMLの構造からですが、とてもシンプルです。

<template>
  <div class="ModalDialogSampleBase">
    <h1>{{ msg }}</h1>
    <button @click="show"'>ダイアログの表示</button>
    <div class="message-field">ダイアログ入力メッセージ<br>{{message}}</div>
    <ModalDialog v-if="showDialog" @ok='ok' @cancel='cancel'/>
  </div>
</template>

6行目で子コンポーネントを配置しています。ここでは v-if ディレクティブを使いshowDialogの真偽で表示/非表示を切り替えています。

  • ダイアログの表示
    このshowDialogは4行目のボタンのClickイベントハンドラ内でfalse→trueにセットしダイアログを表示させます。

  • ダイアログの非表示
    コンポーネントからEmitされるイベントを補足し、showDialogをtrue→falseにリセットしダイアログを非表示にします。 6行目の v-if の後にある@ok@cancel が子コンポーネントからのイベントを補足してイベントハンドラを呼び出す定義になります。

次にScriptです。

Script

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ModalDialog from './ModalDialog.vue';

@Component({
  components: {
    ModalDialog,
  },
})
export default class ModalDialogSample extends Vue {
  name: string = 'ModalDialogSample';
  msg: string = 'モーダルダイアログのサンプル';
  message: string = '';
  showDialog: boolean = false;
  show(): void {
    this.showDialog = true;
  }
  ok(message: string): void {
    this.showDialog = false;
    this.message = message;
  }
  cancel(): void {
    this.showDialog = false;
  }
}
</script>

三つのイベントハンドラを定義しています。

ダイアログ表示ボタンのイベントハンドラshow()、子コンポーネントからのイベントハンドラok()cancel() になります。 それぞれメンバ変数のshowDialogの真偽をセット、リセットして表示/非表示を切り替えています。

ここで ok(message: string) は引数に string 型の message が定義されていますが、これは ok イベントのパラメタでダイアログの input フィールドに入力された文字列を子コンポーネントから受けとる為です。 では、次に子コンポーネント側を見てみましょう。

コンポーネント(ModalDialog.vue)

コンポーネントのModalDialog.vueの実装です。

HTML

同じようにまずは、HTMLの構造からです。

<template>
  <transition name="modal">
    <div class="modal-mask">
      <div class="modal-wrapper">
        <div class="modal-container">
          <div class="modal-header">モーダルダイアログ
          </div>
          <div class="modal-body">
            <input type='text' v-model='message' placeholder="メッセージ入力欄です">
          </div>
          <div class="modal-footer">
            <button class="modal-default-button" @click='ok(message)'>OK</button>
            <button class="modal-default-button" @click='cancel()'>Cancel</button>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

modal-mask が親コンポーネントを覆う為の要素になります。Styleで背景色を透過させてダイアログを表示した際でも親コンポーネントが薄く見えるように、それっぽくしています。

ダイアログ本体は modal-container 要素の中身がそれにあたります。ヘッダ、ボディ、フッタを定義していてボディにinput要素を、フッタにok、cancelのボタンを配置しています。 この辺りはbootstrap*1CSSが参考になります。

12,13行目でok、cancelのイベントハンドラを登録しています。ok ハンドラでは引数にmessageを指定していますが、この message は 9行目で input にバインドされているので、入力テキストが入ります。ここがイベントハンドラ経由で子→親へ入力テキストを伝える動作の起点になります。

Script

最後にダイアログ本体のScriptです。

<script lang="ts">
import { Component, Emit, Vue } from 'vue-property-decorator';

export default class ModalDialog extends Vue {
  message: string = '';
  @Emit('ok')
  ok(str: string): void {
    //
  }
  @Emit('cancel')
  cancel(): void {
    //
  }
}
</script>

これも至ってシンプルです。 ok、cancelのイベントハンドラを定義しているだけです。TypeScript なので従来の this.$emit にあたる実装がデコーレータを使った記述の仕方になっています。 例えば6~9行目の ok イベントハンドラは 従来の JavaScript で書くと

ok(str) {
  this.$emit('ok', str)
}

という感じだと思うのですが、デコーレータを使った記述では、@Emitでemitするイベントを定義し、その後にハンドラを実装するという書き方になります。 また、親コンポーネントに渡すパラメタの指定が暗黙的になってしまっている気がして個人的には違和感を感じて馴染めていません。この辺りご意見あればぜひ、お願いしたいと思います。

出来上がり!

npm run serve

で動かします。
TypeScript 故の戸惑いもありましたが、ひとまずモーダルダイアログの実装に加えコンポーネント間のデータ受け渡しも完成しました。必ずといっていいほど使う局面が多いモーダルダイアログの実装、自分が見るレシピとしても活用したいと思います。

謝辞

最後まで読んでいただきありがとうございます。
今回はモーダルダイアログの実装を具体的に解説してみました。少しでもお役に立てれば幸いです。

*1:npm install bootstrap --saveでインストールできます