Vue.js ダイアログボックスを表示する

# はじめに
　本記事は、GASならびにVue.jsを使ってプログラムの開発を行った事がある方を対象としています。
　また、GASでのWebアプリの開発について書いています。

# 概要
　タイトル通り、Vue.jsを使用してダイアログボックスを表示する方法を説明します。

# 使用するファイル
- Main.gs
- Index.html
- Vuejs.html
- modalStyle.html
- modalScript.html

```javascript:Main.gs
function doGet(e) {
  const template = HtmlService.createTemplateFromFile('Index');
  const title = '氏名一覧';
  const html = template.evaluate().setTitle(title).setSandboxMode(HtmlService.SandboxMode.IFRAME);

  return html;
}
```

```html:Index.html
<!DOCTYPE html>
<html>
  <head>
    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <!-- Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>

    <?!= HtmlService.createHtmlOutputFromFile('modalStyle').getContent(); ?>
    <?!= HtmlService.createHtmlOutputFromFile('modalScript').getContent(); ?>
  
    <base target="_top">
  </head>
  <body>
    <div id="app">
      <p></P>
      <div class="container">
        <table id="nameList" class="table table-hover">
          <thead>
            <tr class="alert-primary">
              <th class="head01">ID</th>
              <th class="head02">氏名</th>
              <th class="head03">メールアドレス</th>
              <th class="head04">住所</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="record in records" v-bind:key="record.id" @click="doShowModal(record.id)">
              <td class="col01">{{ record.id }}</td>
              <td class="col02">{{ record.name }}</td>
              <td class="col03">{{ record.eMail }}</td>
              <td class="col04">{{ record.address }}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <div class="container">
        {{ messageText }}
      </div>
      <div class="container">
        <button type="button" class="btn btn-primary btn-lg btn-block" id="show-modal" @click="doShowModal(0)">追 加</button>
      </div>
      <modal v-if="showModal" v-on:close-modal="doHideModal" v-on:add-data="doAddData" v-on:del-data="doDelData" v-bind:message_text="messageText" v-bind:button_name="buttonName" v-bind:cond="cond">
        <h3 slot="header">custom header {{ headerText }}</h3>
      </modal>
    </div>

    <?!= HtmlService.createHtmlOutputFromFile('Vuejs').getContent(); ?>
  
  </body>
</html>
```

```vuejs:Vuejs.html
<script>  
Vue.component('modal', {
  template: '#modal-template',
  props: {
    message_text: String,
    button_name: String,
    cond: {
      id: Number,
      name: String,
      eMail: String,
      address: String
    }
  },
  methods: {
    doAddData: function() {
      this.$emit('add-data', this.cond);
    },
    doDelData: function() {
      this.$emit('del-data', this.cond.id);
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    showModal: false,  // ダイアログボックスの表示を制御
    messageText: 'テストテスト',
    headerText: 'あいうえお',
    buttonName: '',
    records: [
      // 一覧表に表示されるデータ
      {id: 1, name: '鈴木太郎', eMail: 't-suzuki@xxxxx.xx.jp', address: '東京都新宿区'},
      {id: 2, name: '斉藤花子', eMail: 'h-saitoh@xxxxx.xx.jp', address: '埼玉県川越市'},
      {id: 3, name: '田中ひろみ', eMail: 'h-tanaka@xxxxx.xx.jp', address: '東京都清瀬市'}
    ],
    cond: {
      id: 0,
      name: '',
      eMail: '',
      address: ''
    }
  },
  mounted: function() {
    console.log('*** start');
    // 画面表示時に一覧表へデータを追加する処理
    this.records.push({id: 4, name: '山田一郎', eMail: 'i-yamada@xxxxx.xx.jp', address: '埼玉県所沢市'});
    this.records.push({id: 5, name: '佐藤順子', eMail: 'j-satoh@xxxxx.xx.jp', address: '福井県大野市'});
    console.log('*** end');
  },
  methods: {
    doShowModal: function(id) {
      // ダイアログボックスを表示する処理
      if(id === 0) {
        // 追加（新規）の場合
        this.buttonName = '追 加';
        this.cond.id = 0;
        this.cond.name = '';
        this.cond.eMail = '';
        this.cond.address = '';
      }
        // 更新（変更）の場合
      else {
        this.buttonName = '更 新';
        this.cond.id = id;
        for(record of this.records) {
          if(record.id === id) {
            this.cond.name = record.name;
            this.cond.eMail = record.eMail;
            this.cond.address = record.address;
          }
        }
      }
      this.showModal = true;
    },
    doHideModal: function() {
      // ダイアログボックスを非表示にする処理
      this.showModal = false;
    },
    doAddData: function(cond) {
      // 一覧表のデータを更新する処理
      this.showModal = false;
      if(cond.id === 0) {
        // 追加処理（新規）
        let maxId = 0;
        for(record of this.records) {
          if(record.id > maxId) {
            maxId = record.id;
          }
        }
        const newCond = {id: 0, name: '', eMail: '', address: ''};
        newCond.id = maxId + 1;
        newCond.name = cond.name;
        newCond.eMail = cond.eMail;
        newCond.address = cond.address;
        this.records.push(newCond);
      }
      else {
        // 更新処理（変更）
        for(record of this.records) {
          if(record.id === cond.id) {
            record.name = cond.name;
            record.eMail = cond.eMail;
            record.address = cond.address;
          }
        }
      }
    },
    doDelData: function(id) {
      // 一覧表のデータを削除する処理
      this.showModal = false;
      for(let i = 0; i < this.records.length; i++) {
        if(this.records[i].id === id) {
          this.records.splice(i, 1);
          break;
        }
      }
    }
  }
});
</script>  
```

```css:modalStyle.html
<style>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: table;
  transition: opacity 0.3s ease;
}

.modal-wrapper {
  display: table-cell;
  vertical-align: middle;
}

.modal-container {
  width: 550px;
  margin: 0px auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  transition: all 0.3s ease;
  font-family: Helvetica, Arial, sans-serif;
}

.modal-header h3 {
  margin-top: 0;
  color: #42b983;
}

.modal-body {
  margin: 0 0 20px 0;
  padding: 10px;
}

.modal-footer {
  display: block;
}

.modal-default-button {
  float: right;
}

/*
 * The following styles are auto-applied to elements with
 * transition="modal" when their visibility is toggled
 * by Vue.js.
 *
 * You can easily play with the modal transition by editing
 * these styles.
 */

.modal-enter {
  opacity: 0;
}

.modal-leave-active {
  opacity: 0;
}

.modal-enter .modal-container,
.modal-leave-active .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>
```

```vuejs:modalScript.html
<script type="text/x-template" id="modal-template">
  <transition name="modal">
    <div class="modal-mask">
      <div class="modal-wrapper">
        <div class="modal-container">

          <div class="modal-header">
            <slot name="header">
              default header
            </slot>
          </div>

          <div class="modal-body">
            <slot name="body">
              default body {{ message_text }}
            </slot>
            <form>
              <div class="form-group row" id="data-id-group" v-if="cond.id !== 0">
                <label for="data-id" class="col-sm-4 col-form-label">ID：</label>
                <div class="col-sm-8">
                  <input type="text" class="form-control" id="data-id" v-model="cond.id" readonly>
                </div>
              </div>
              <div class="form-group row">
                <label for="name" class="col-sm-4 col-form-label">名前：</label>
                <div class="col-sm-8">
                  <input type="text" class="form-control" id="name" v-model="cond.name">
                </div>
              </div>
              <div class="form-group row">
                <label for="eMail" class="col-sm-4 col-form-label">メールアドレス：</label>
                <div class="col-sm-8">
                  <input type="email" class="form-control" id="eMail" v-model="cond.eMail">
                </div>
              </div>
              <div class="form-group row">
                <label for="address" class="col-sm-2 col-form-label">住所：</label>
                <div class="col-sm-10">
                  <input type="text" class="form-control" id="address" v-model="cond.address">
                </div>
              </div>
            </form>
          </div>

          <div class="modal-footer">
            <slot name="footer">
              <div class="row">
                <div class="col-sm">
                  <button type="button" class="btn btn-primary btn-block" @click="doAddData">{{ button_name }}</button>
                </div>
                <div class="col-sm" v-if="cond.id !== 0">
                  <button type="button" class="btn btn-danger btn-block" @click="doDelData">削 除</button>
                </div>
                <div class="col-sm">
                  <button type="button" class="btn btn-secondary btn-block" @click="$emit('close-modal')">閉じる</button>
                </div>
              </div>
            </slot>
          </div>
        </div>
      </div>
    </div>
  </transition>
</script>
```

# 説明
　ダイアログボックス自体（HTML部分、実際は`<script>`です。）は、**modalScript.html** で、このファイルのスタイルシートが **modalStyle.html** なります。
　メインのファイル（**Index.html**）の

```html:Index.html
    <?!= HtmlService.createHtmlOutputFromFile('modalStyle').getContent(); ?>
    <?!= HtmlService.createHtmlOutputFromFile('modalScript').getContent(); ?>
```
（上記）で読み込まれて、（下記`<modal>`タグ）

```html:Index.html
      <modal v-if="showModal" v-on:close-modal="doHideModal" v-on:add-data="doAddData" v-on:del-data="doDelData" v-bind:message_text="messageText" v-bind:button_name="buttonName" v-bind:cond="cond">
        <h3 slot="header">custom header {{ headerText }}</h3>
      </modal>
```
で参照されます。
　ダイアログボックスの表示は、`v-if="showModal"`で制御されています。（変数`showModal`が`true`の時に表示）

　ダイアログボックスは、子コンポーネント（**Vuejs.html** の`Vue.component('modal', {…});`）として動作します。
　親コンポーネント（**Vuejs.html** の`var app = new Vue({…});`）と子コンポーネントのデータの関連付けは、**Index.html** の`v-bind:message_text="messageText"`（他も同様）で行われています。
　`slot="header"`の指定により、直接ダイアログボックスへ文字列を連携させる事（`<slot name="header">`タグの要素に置き換えられます）も可能です。

　子コンポーネントから親コンポーネントへのメソッド処理の関連付けは、**Index.html** の`v-on:add-data="doAddData"`（他も同様）で行われていて、以下の様に連携されます。

1. **modalScript.html** の`@click="doAddData`により、子コンポーネントの`doAddData`メソッドが実行されます。
1. 続けて、`this.$emit('add-data', this.cond)`（下記参照）により、前述で関連付けられた親コンポーネントの`doAddData`メソッドが実行されます。    
子コンポーネント（メソッド）→親コンポーネント（メソッド）へ引数でデータを渡す事も可能です。

```javascript:Vuejs.html
    doAddData: function() {
      this.$emit('add-data', this.cond);
    },
```

# 結果
　下記の様に画面が表示されます。
### 一覧画面
![一覧画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510221/7119aacd-9cd4-59ad-97cd-4679d6b37d03.jpeg)

- 〔追 加〕ボタン（`@click="doShowModal(0)"`）で、データ追加用のダイアログボックスを表示します。
- 一覧の行を選択する事により、データ更新用のダイアログボックスを表示します。    
**Index.html** の`<tr>`タグに`@click="doShowModal(record.id)"`を記述する事で実現しています。

### 追加画面
![追加画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510221/d1270853-098b-4263-f61b-6a82b6dfa209.jpeg)
### 更新画面
![更新画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510221/9b70a744-3a2d-db95-13a9-2c0e5120e39a.jpeg)
　各項目の値は、`cond`（子コンポーネントの`props`）のプロパティが、`v-model`ディレクティブでバインディングされます。
　**更新画面**を表示する場合、一覧で選択されたデータを親コンポーネントの`cond`のプロパティにセットしています。
　前述の`v-bind`ディレクティブで親コンポーネントの`cond`と子コンポーネントの`cond`バインディングされているので、選択行のデータがダイアログボックスに表示されます。