【Vue.js】追加・削除できる入力欄を作る

# 概要
Vue、Vuetifyを使って、追加と削除ができる入力フォームを作り、0~nの数を取りうる項目を登録できるようしてみました。

開発環境は以下の通りです。
* Vue: 2.6.14
* Vuetify: 2.6.4
* Windows10

# 実装
こちらのページを参考にさせていただきました。
https://hand28.hatenadiary.jp/entry/2018/12/21/181550

今回は、例として同行者の入力欄を増減できるフォームを作ります。
先ほどのページを参考にして作ったものがこちらです。

![sample1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1130442/f4337f86-50ef-4d81-3c49-92b034257fde.gif)

```vue:Sample.vue
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="10">
        <v-card>
          <v-card-title>申し込みフォーム</v-card-title>
          <v-card-text>
            <v-row>
              <v-col cols="2">
                <v-subheader>代表者</v-subheader>
              </v-col>
              <v-col cols="5">
                <v-text-field
                  label="名前"
                  v-model="repName"
                ></v-text-field>
              </v-col>
              <v-col cols="3">
                <v-text-field
                  label="年齢"
                  v-model="repAge"
                  suffix="歳"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row>
              <v-col cols="2">
                <v-subheader>電話番号</v-subheader>
              </v-col>
              <v-col cols="8">
                <v-text-field
                  label="電話番号"
                  v-model="tel"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row>
              <v-col cols="2">
                <v-subheader>メールアドレス</v-subheader>
              </v-col>
              <v-col cols="8">
                <v-text-field
                  label="メールアドレス"
                  v-model="mail"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row v-for="member in memberList" :key="member.id">
              <v-col cols="2">
                <v-subheader>同行者 {{member.id + 1}}人目</v-subheader>
              </v-col>
              <v-col cols="5">
                <v-text-field
                  label="名前"
                  v-model="member.name"
                ></v-text-field>
              </v-col>
              <v-col cols="3">
                <v-text-field
                  label="年齢"
                  v-model="member.age"
                  suffix="歳"
                ></v-text-field>
              </v-col>
              <v-col cols="2">
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="removeInput(member.id)"
                >
                  <v-icon dark>mdi-minus</v-icon>
                </v-btn>
              </v-col>
            </v-row>
            <v-row justify="center">
              <v-btn 
                dark 
                small 
                color="grey" 
                class="ma-2" 
                @click="addInput()"
              >
                <v-icon dark>mdi-plus</v-icon>
              </v-btn>
            </v-row>
          </v-card-text>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn
              color="blue"
              dark
            >
              登録
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
  export default {
    name: 'Sample',
    data: () => ({
      repName: "田中太郎",
      repAge: 25,
      tel: "000-0000-0000",
      mail: "example@gmail.com",
      memberList: [{id: 0, name: "", age: null}]
    }),

    methods: {
      // 入力欄追加
      addInput () {
        this.memberList.push({id: this.memberList.length, name: "", age: null});
      },

      // 入力欄削除
      removeInput (id) {
        let inputList = this.memberList;
        inputList = inputList.filter((input) => { return input.id !== id;});
        this.memberList = this.makeNewInput(inputList);
      },

      // ID振り直し
      makeNewInput (inputList) {
        let newInputList = [];
        for (let i = 0; i < inputList.length; i++) {
          newInputList.push ({
            id: i,
            name: inputList[i].name,
            age: inputList[i].age
          });
        }
        return newInputList;
      }
    }
  }
</script>
```
### ポイント
* 同行者を入れる配列(memberList)を用意し、v-forで配列分入力欄を表示しています。
```vue
<v-row v-for="member in memberList" :key="member.id">
```
* プラスボタンを押すとaddInputが呼び出されます。addInput()では配列memberListにpushをして新しく要素を追加しています。
```js
addInput () {
  this.memberList.push({id: this.memberList.length, name: "", age: null});
},
```
* マイナスボタンで呼び出されるremoveInput()では、対象のID以外の要素を取得する形で削除を行っています。削除しただけだとIDが歯抜けの形になってしまうので、1から順に並ぶようにIDの振り直しも同時に行います。
```js
// 入力欄削除
removeInput (id) {
  let inputList = this.memberList;
  inputList = inputList.filter((input) => { return input.id !== id;});
  this.memberList = this.makeNewInput(inputList);
},
// ID振り直し
makeNewInput (inputList) {
  let newInputList = [];
  for (let i = 0; i < inputList.length; i++) {
    newInputList.push ({
      id: i,
      name: inputList[i].name,
      age: inputList[i].age
    });
  }
  return newInputList;
}
```
これでベースができたので、少し手を加えていきます。
## 任意の場所に追加できるようにする
現状では追加の際は最後にしか追加できないため、好きな場所に追加できるようにします。

まず、ボタンをマイナスボタンと同様に各入力欄の隣に配置し、引数としてIDを渡します。
```vue
<template>
 <!-- 省略 -->
              <v-col cols="3">
                <v-text-field
                  label="年齢"
                  v-model="member.age"
                  suffix="歳"
                ></v-text-field>
              </v-col>
              <v-col cols="2">
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="addInput(member.id)"
                >
                  <v-icon dark>mdi-plus</v-icon>
                </v-btn>
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="removeInput(member.id)"
                >
                  <v-icon dark>mdi-minus</v-icon>
                </v-btn>
              </v-col>
            </v-row>
 <!-- 省略 -->
</template>
```
addInput()では、配列の最後に追加をするpushではなく、spliceを使用して該当のIDの次に要素を追加しています。そして、削除時と同様にmakeNewIput()でIDの振り直しを行います。
```js
      // 入力欄追加
      addInput (id) {
        let inputList = this.memberList;
        inputList.splice(id+1, 0, {id: null, name: "", age: null});
        this.memberList = this.makeNewInput(inputList);
      },
```
このような形になりました。
![sample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1130442/3a74b98b-c2f6-dee6-3f7d-104fa69c1aa2.gif)
正直今回の例であげている同行者の登録では順番はあまり重要ではないですが、時系列など、順番を意識して登録したい場合は使えるのかなと思います。

## 要素が0の場合にテキストを表示する
今の状態だと、マイナスボタンを押しきった時に画像のようにラベルやプラスボタンも含め全部消えてしまうので、テキストとプラスボタンが表示されるよう変更します。
![フォーム2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1130442/534e3bd1-87c4-65c6-0dd0-7eff6b42b908.png)
v-ifを使い、memberListの長さが0の時のみ表示する要素を追加します。
```vue
<template>
<!-- 省略 -->
            <v-row v-if="memberList.length == 0">
              <v-col cols="2">
                <v-subheader>同行者</v-subheader>
              </v-col>
              <v-col cols="8">
                同行者なし
              </v-col>
              <v-col cols="2">
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="addInput(0)"
                >
                  <v-icon dark>mdi-plus</v-icon>
                </v-btn>
              </v-col>
            </v-row>
            
            <v-row v-for="member in memberList" :key="member.id">
              <v-col cols="2">
                <v-subheader>同行者 {{member.id + 1}}人目</v-subheader>
              </v-col>
<!-- 省略 -->
</template>
```
配列が空の場合にはテキストが表示され、そこから追加もできるようになりました。
![フォーム3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1130442/ed491d22-ced1-d57f-37d2-6b8662f38dc8.png)

## 登録上限を設ける
追加できる入力欄に上限を設けます。
今回は、4人まで同行者を登録できるようにします。

プラスボタンの表示条件としてv-if="member.List.length < 4"を追加しました。
```vue
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="addInput(member.id)"
                  v-if="memberList.length < 4"
                >
                  <v-icon dark>mdi-plus</v-icon>
                </v-btn>
```
このように4人までしか登録できないようになりました。
![sample3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1130442/f0044ca5-85ad-a9ff-ceec-9ae637110b82.gif)

## 最終的なコード
```vue
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="10">
        <v-card>
          <v-card-title>申し込みフォーム</v-card-title>
          <v-card-text>
            <v-row>
              <v-col cols="2">
                <v-subheader>代表者</v-subheader>
              </v-col>
              <v-col cols="5">
                <v-text-field
                  label="名前"
                  v-model="repName"
                ></v-text-field>
              </v-col>
              <v-col cols="3">
                <v-text-field
                  label="年齢"
                  v-model="repAge"
                  suffix="歳"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row>
              <v-col cols="2">
                <v-subheader>電話番号</v-subheader>
              </v-col>
              <v-col cols="8">
                <v-text-field
                  label="電話番号"
                  v-model="tel"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row>
              <v-col cols="2">
                <v-subheader>メールアドレス</v-subheader>
              </v-col>
              <v-col cols="8">
                <v-text-field
                  label="メールアドレス"
                  v-model="mail"
                ></v-text-field>
              </v-col>
            </v-row>
        
            <v-row v-if="memberList.length == 0">
              <v-col cols="2">
                <v-subheader>同行者</v-subheader>
              </v-col>
              <v-col cols="8">
                同行者なし
              </v-col>
              <v-col cols="2">
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="addInput(0)"
                >
                  <v-icon dark>mdi-plus</v-icon>
                </v-btn>
              </v-col>
            </v-row>

            <v-row v-for="member in memberList" :key="member.id">
              <v-col cols="2">
                <v-subheader>同行者 {{member.id + 1}}人目</v-subheader>
              </v-col>
              <v-col cols="5">
                <v-text-field
                  label="名前"
                  v-model="member.name"
                ></v-text-field>
              </v-col>
              <v-col cols="3">
                <v-text-field
                  label="年齢"
                  v-model="member.age"
                  suffix="歳"
                ></v-text-field>
              </v-col>
              <v-col cols="2">
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="addInput(member.id)"
                  v-if="memberList.length < 4"
                >
                  <v-icon dark>mdi-plus</v-icon>
                </v-btn>
                <v-btn 
                  dark 
                  small 
                  color="grey" 
                  class="ma-2" 
                  @click="removeInput(member.id)"
                >
                  <v-icon dark>mdi-minus</v-icon>
                </v-btn>
              </v-col>
            </v-row>
          </v-card-text>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn
              color="blue"
              dark
            >
              登録
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
  export default {
    name: 'Sample',
    data: () => ({
      repName: "田中太郎",
      repAge: 25,
      tel: "000-0000-0000",
      mail: "example@gmail.com",
      memberList: [{id: 0, name: "", age: null}]
    }),

    methods: {
      // 入力欄追加
      addInput (id) {
        let inputList = this.memberList;
        inputList.splice(id+1, 0, {id: null, name: "", age: null});
        this.memberList = this.makeNewInput(inputList);
      },

      // 入力欄削除
      removeInput (id) {
        let inputList = this.memberList;
        inputList = inputList.filter((input) => { return input.id !== id;});
        this.memberList = this.makeNewInput(inputList);
      },

      // ID振り直し
      makeNewInput (inputList) {
        let newInputList = [];
        for (let i = 0; i < inputList.length; i++) {
          newInputList.push ({
            id: i,
            name: inputList[i].name,
            age: inputList[i].age
          });
        }
        return newInputList;
      }
    }
  }
</script>
```
# まとめ
今回は、Vueを使って入力欄の数を追加、削除できるフォームを作成しました。
登録する際にバリデーションを設けるなど、まだまだ改良の余地はあるかなと思います。
最近仕事でVueを使い始めたので、また何か学んだことがあればまとめていこうと思います。