vue.jsでアンケートフォームを作る

## やりたいこと
設問ごとにスライドするアンケートフォームの作成

## 前提
cssやjqueryなどを利用すると、同様の情報があり簡単に実装できそうだが、今回はvue.jsを利用する。

## 参考文献
https://qiita.com/ktn/items/65a0e3112246f86d04de
こちらの記事を参考にしました。

## 仕様
アンケートは選択方式で複数選択はなし。
アンケートを選択すると次の設問が表示される。
回答を戻れるように「戻る」ボタンを設置。
「戻る」ボタンがある為、「次へ」ボタンも設置。
最後のアンケート回答画面では「次へ」の代わりに「送信」ボタンが表示される。

## スライドを表現
スライドの表現について調べました。
参考文献がとても役に立ちました。

### Transitionコンポーネント
vueにはTransitionという組み込みコンポーネントが存在する。
https://ja.vuejs.org/guide/built-ins/transition

このtransitionコンポーネントを利用することでスライドアニメーションがvueで実現できそう。



## コード
できあがったのが次のコードです。
本当は１ファイルですが、長くなる為HTML,CSS,JSで分割しています。

### HTML
```vuejs
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app">
    <div class="contents">
		<div class="header">
			<p>アンケート</p>
		</div>
		<div class="contents_field">
			<p>アンケートに答えてください。</p>
		</div>
        
		<div class="slider-outer">
			<transition :name=trans_slider>
				<div class="contents_field contents_question" :key=questionINdex>
					<div class="radiobox" >
						<template v-if="currentQuestion.option.text01">
							<input v-bind:id="currentQuestion.idname.text01" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text01" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text01 )"/>
							<label v-bind:for="currentQuestion.idname.text01">{{ currentQuestion.option.text01 }}</label>
						</template>
						<template v-if="currentQuestion.option.text02">
							<input v-bind:id="currentQuestion.idname.text02" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text02" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text02)"/>
							<label v-bind:for="currentQuestion.idname.text02">{{ currentQuestion.option.text02 }}</label>
						</template>
						<template v-if="currentQuestion.option.text03">
							<input v-bind:id="currentQuestion.idname.text03" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text03" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text03)"/>
							<label v-bind:for="currentQuestion.idname.text03">{{ currentQuestion.option.text03 }}</label>
						</template>
						<template v-if="currentQuestion.option.text04">
							<input v-bind:id="currentQuestion.idname.text04" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text04" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text04)"/>
							<label v-bind:for="currentQuestion.idname.text04">{{ currentQuestion.option.text04 }}</label>
						</template>
						<template v-if="currentQuestion.option.text05">
							<input v-bind:id="currentQuestion.idname.text05" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text05" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text05)"/>
							<label v-bind:for="currentQuestion.idname.text05">{{ currentQuestion.option.text05 }}</label>
						</template>
					</div>
				</div>
			</transition>
		</div>

		<footer>
			<button v-on:click="nextQuestion" v-if="questionINdex < 3">
				次へ
			</button>
			<button v-if="questionINdex==3" class="submitBtn" v-on:click="sendQuestion" :disabled="disabled">
				送信
			</button>
			<button v-on:click="prevQuestion" v-if="questionINdex > 0">
				戻る
			</button>
		</footer>
	</div>
  </div>
</body>
</html>
```

### JS
```vue
<script>
export default {
	data() {
		return {
			title: "アンケート",
			trans_slider: 'next',
			questionINdex: 0,
			questions: [
				{
					question: {
						text01: '性別は？'
					},
					option: {
						text01: '男',
						text02: '女',
					},
					idname: {
						text01: '1-1',
						text02: '1-2',
					}
				},
				{
					question: {
						text01: '生まれた年号は？'
					},
					option: {
						text01: '大正',
						text02: '昭和',
						text03: '平成',
						text04: '令和',
					},
					idname: {
						text01: '2-1',
						text02: '2-2',
						text03: '2-3',
						text04: '2-4',
					}
				},
				{
					question: {
						text01: '出生地は？'
					},
					option: {
						text01: '北海道',
						text02: '東北',
						text03: '関東',
						text04: '北陸',
						text05: '中部',
						text06: '近畿',
						text07: '中国',
						text08: '四国',
						text09: '九州・沖縄',
					},
					idname: {
						text01: '3-1',
						text02: '3-2',
						text03: '3-3',
						text04: '3-4',
						text05: '3-5',
						text06: '3-6',
						text07: '3-7',
						text08: '3-8',
						text09: '3-9',
					}
				},
                {
					question: {
						text01: '血液型は？'
					},
					option: {
						text01: 'A',
						text02: 'B',
						text03: 'AB',
						text04: 'O',
						text05: '不明',
					},
					idname: {
						text01: '4-1',
						text02: '4-2',
						text03: '4-3',
						text04: '4-4',
						text05: '4-5',
					}
				},
			],
            answers:[],
		}
	},
	computed: {
		currentQuestion() {
			return this.questions[this.questionINdex];
		},
	},
	methods: {
		nextQuestion: function(){
			this.trans_slider = 'next'
			this.questionINdex = this.questionINdex + 1
			if(this.questionINdex > 3){	
				this.questionINdex = 3
			}
		},
		prevQuestion: function(){
			this.trans_slider = 'prev'
			this.questionINdex = this.questionINdex - 1
			if(this.questionINdex <= 0){	
				this.questionINdex = 0
			}
		},
		sendQuestion: function(){
			// アンケート送信処理
		},
        selectAnswer: function(questionINdex, value){
            this.answers[questionINdex] = value
			this.nextQuestion()
        },
	},
}
</script>


```


### CSS

```css

<style scoped>
:root {
	/* --font-color: #684D35;*/
	--font-color: #000000;
}

〜　中略　〜

.slider-outer{
  position:relative;
  width:450px;
  height:300px;
  overflow:hidden;
  margin: 0 auto 20px;
}
.contents_question{
  position: absolute;
  width:450px;
  height:300px;
}

.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active  {
  transition: all .6s ease-out;
}
.next-enter-from {
  transform: translateX(450px);
}
.next-enter-to {
  transform: translateX(0);
}
.next-leave-from {
  transform: translateX(0);
}
.next-leave-to {
  transform: translateX(-450px);
}

.prev-enter-from {
  transform: translateX(-450px);
}
.prev-enter-to {
  transform: translateX(0);
}
.prev-leave-from {
  transform: translateX(0);
}
.prev-leave-to {
  transform: translateX(450px);
}
</style>

```

## 解説

### アニメーション部分

```vuejs
<div class="slider-outer">
　　　　<transition :name=trans_slider>
　　　　　　　　アニメーションする部分
　　　　</transition>
</div>
```
trans_sliderの値を変更することでアニメーションの動作を制御できます。
今回の場合は、
next : 次の設問を表示するアニメーション
prev : 前の設問を表示するアニメーション
という値で制御しています。

次へボタンをクリックすると、nextQuestionが発火します。

```js
nextQuestion: function(){
    this.trans_slider = 'next'
    this.questionINdex = this.questionINdex + 1
    if(this.questionINdex > 3){	
        this.questionINdex = 3
    }
},
```
trans_sliderにnextをセット。
questionINdexに１を加算することで次の質問文を読み込んでいます。
最後の質問の場合はそれ以上進まないようにしています。

アニメーションの制御はCSSで行います。
参考にした記事はvue2で書かれているようで、vue3では少し記述がことなります。

```
next-enter → next-enter-from
next-leave → next-leave-from
prev-enter → prev-enter-from
prev-leave → prev-leave-from
```

この状態の記述が少し複雑でしたが、
https://ja.vuejs.org/guide/built-ins/transition
こちらの記事中のCSS でのトランジション＞トランジションクラス付近がわかりやすかったです。




### 質問文
質問はquestionsの配列の中に
question：質問分
option:選択肢
idname:radioタグID名
をオブジェクトとしてまとめています。

```js
questions: [
    {
        question: {
            text01: '性別は？'
        },
        option: {
            text01: '男',
            text02: '女',
        },
        idname: {
            text01: '1-1',
            text02: '1-2',
        }
    },
    // 以下略
```

## ハマったとこと
### アニメーションしない
ボタンをクリックしてもアニメーションしないという状態になりました。
結論として、**transition**内に:keyの値が必要でした。

```html
<div class="contents_field contents_question" :key=questionINdex>
```
keyとして質問文の番号を設定しました。

### スライドするときにページ下部にズレて表示されてしまう。
こちらはCSSの問題だったのですが、transitionの外側に大枠のブロックを作成することで解決しました。

```html
<div class="slider-outer">
```

**slider-outer**と**contents_question**それぞれのクラスにpositionと幅・高さを指定することで解決しました。

## 課題

### 未選択時のバリデーション
問題：選択肢を未選択の状態で「次へ」が押せる

対応：回答を格納する配列の設問部分が空だったら未回答と判定して、次へ進ませない。

```js
methods: {
    nextQuestion: function(){
        if(this.answers[this.questionINdex]){ // ここのifを追加。
            this.trans_slider = 'next'
			this.questionINdex = this.questionINdex + 1
			if(this.questionINdex > 3){	
				this.questionINdex = 3
			}
		}
    },
```

### 選択後のスライドで未選択となる
問題：回答を選択後にスライドすると、前の回答が初期化されてしまい未選択状態となる。

対応：質問配列に選択済みフラグ格納用の要素を追加。選択をフラグで判別。

```js
questions: [
     {
          question: {
               text01: '性別は？'
          },
          option: {
               text01: '男',
               text02: '女',
          },
          idname: {
               text01: '1-1',
               text02: '1-2',
          },
          checked:{          // 選択肢ごとにtrue or falseで状態を格納
               flg01:false,
          },
     },
```

```js
selectAnswer: function(questionINdex, value){
    this.answers[questionINdex] = value

    // 設問数分のフラグを初期化
	if(this.questions[questionINdex].checked.flg01){
		this.questions[questionINdex].checked.flg01 = false
	}
	if(this.questions[questionINdex].checked.flg02){
		this.questions[questionINdex].checked.flg02 = false
	}
	if(this.questions[questionINdex].checked.flg03){
		this.questions[questionINdex].checked.flg03 = false
	}
	if(this.questions[questionINdex].checked.flg04){
		this.questions[questionINdex].checked.flg04 = false
	}
	if(this.questions[questionINdex].checked.flg05){
		this.questions[questionINdex].checked.flg05 = false
	}

    // 回答したテキストと選択肢が一致したNoのフラグをTRUE
	if(this.questions[questionINdex].option.text01 == value){
		this.questions[questionINdex].checked.flg01 = true
	}
	if(this.questions[questionINdex].option.text02 == value){
		this.questions[questionINdex].checked.flg02 = true
	}
	if(this.questions[questionINdex].option.text03 == value){
		this.questions[questionINdex].checked.flg03 = true
	}
	if(this.questions[questionINdex].option.text04 == value){
		this.questions[questionINdex].checked.flg04 = true
	}
	if(this.questions[questionINdex].option.text05 == value){
		this.questions[questionINdex].checked.flg05 = true
	}
			
	this.nextQuestion()
},
```

```vue
<div class="slider-outer">
			<transition :name=trans_slider>
				<div class="contents_field contents_question" :key=questionINdex>
					<div class="radiobox" >
						<template v-if="currentQuestion.option.text01">
                            <!-- v-bind:checkedを追加し、選択用の要素で判定 -->
							<input v-bind:id="currentQuestion.idname.text01" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text01" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text01 )" v-bind:checked="currentQuestion.checked.flg01"/>
							<label v-bind:for="currentQuestion.idname.text01">{{ currentQuestion.option.text01 }}</label>
						</template>
						<template v-if="currentQuestion.option.text02">
							<input v-bind:id="currentQuestion.idname.text02" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text02" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text02)" v-bind:checked="currentQuestion.checked.flg02"/>
							<label v-bind:for="currentQuestion.idname.text02">{{ currentQuestion.option.text02 }}</label>
						</template>
						<template v-if="currentQuestion.option.text03">
							<input v-bind:id="currentQuestion.idname.text03" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text03" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text03)" v-bind:checked="currentQuestion.checked.flg03"/>
							<label v-bind:for="currentQuestion.idname.text03">{{ currentQuestion.option.text03 }}</label>
						</template>
						<template v-if="currentQuestion.option.text04">
							<input v-bind:id="currentQuestion.idname.text04" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text04" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text04)" v-bind:checked="currentQuestion.checked.flg04"/>
							<label v-bind:for="currentQuestion.idname.text04">{{ currentQuestion.option.text04 }}</label>
						</template>
						<template v-if="currentQuestion.option.text05">
							<input v-bind:id="currentQuestion.idname.text05" class="radiobutton" v-bind:name="'qualification' + questionINdex + 1" type="radio"
								v-bind:value="currentQuestion.option.text05" v-on:click="selectAnswer(questionINdex, currentQuestion.option.text05)" v-bind:checked="currentQuestion.checked.flg05"/>
							<label v-bind:for="currentQuestion.idname.text05">{{ currentQuestion.option.text05 }}</label>
						</template>
					</div>
				</div>
			</transition>
		</div>
  ```