あらすじ
設問ごとにスライドするアンケートフォームを作成しました。
https://qiita.com/zukka/items/0d1adf1579635b392324
今回やりたいこと
前回作成したアンケートフォームを流用して、チャットボット風のフォームを作成。
仕様
チャットボットのように質問に答えると次の質問が下に表示される。
すべての質問に答えると送信ボタンが表示される。
チャット風を表現する
Transitionコンポーネントを応用。
ふわっと表示させて(opacity)、下からスライドさせながら表示(Y軸の移動)することでチャット風になるのでは。
コード
できあがったのが次のコードです。
本来は1ファイルですが、長くなる為HTML,CSS,JSで分割しています。
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<div class="contents">
<div class="header">
<p>アンケート</p>
</div>
<div class="contents_field">
<p>アンケートに答えてください。</p>
</div>
<!-- ここから前回から変更となっている箇所 -->
<div v-for="(question, questionKey) in questions">
<transition :name=trans_chat>
<div class="contents_field contents_question" :key=questionKey v-if="questionKey <= questionINdex">
<div class="fukidashi">
<div class="chaticon"><img src="チャットアイコン画像"/></div>
<p class="lines">問{{ questionKey + 1 }}. {{ question.question.text01 }}</p>
</div>
<div class="radiobox" >
<template v-if="question.option.text01">
<input v-bind:id="question.idname.text01" class="radiobutton" v-bind:name="'qualification' + (questionKey + 1)" type="radio"
v-bind:value="question.option.text01" v-on:click="selectAnswer(questionKey, question.option.text01)"/>
<label v-bind:for="question.idname.text01">{{ question.option.text01 }}</label>
</template>
<template v-if="question.option.text02">
<input v-bind:id="question.idname.text02" class="radiobutton" v-bind:name="'qualification' + (questionKey + 1)" type="radio"
v-bind:value="question.option.text02" v-on:click="selectAnswer(questionKey, question.option.text02)"/>
<label v-bind:for="question.idname.text02">{{ question.option.text02 }}</label>
</template>
<template v-if="question.option.text03">
<input v-bind:id="question.idname.text03" class="radiobutton" v-bind:name="'qualification' + (questionKey + 1)" type="radio"
v-bind:value="question.option.text03" v-on:click="selectAnswer(questionKey, question.option.text03)"/>
<label v-bind:for="question.idname.text03">{{ question.option.text03 }}</label>
</template>
<template v-if="question.option.text04">
<input v-bind:id="question.idname.text04" class="radiobutton" v-bind:name="'qualification' + (questionKey + 1)" type="radio"
v-bind:value="question.option.text04" v-on:click="selectAnswer(questionKey, question.option.text04)"/>
<label v-bind:for="question.idname.text04">{{ question.option.text04 }}</label>
</template>
<template v-if="question.option.text05">
<input v-bind:id="question.idname.text05" class="radiobutton" v-bind:name="'qualification' + (questionKey + 1)" type="radio"
v-bind:value="question.option.text05" v-on:click="selectAnswer(questionKey, question.option.text05)"/>
<label v-bind:for="question.idname.text05">{{ question.option.text05 }}</label>
</template>
</div>
</div>
</transition>
</div>
<div class="submitbtnFiled" v-if="questionINdex==3" >
<button class="submitBtn" v-on:click="sendQuestion" :disabled="disabled">
送信
</button>
</div>
</div>
<!-- ここまで変更 -->
</div>
</body>
</html>
JS
<script>
export default {
data() {
return {
title: "アンケート",
disabled:true, // 送信ボタンのdisable制御
trans_chat: '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:[],
}
},
methods: {
nextQuestion: function(){
this.trans_chat = 'next'
if(this.questionINdex <= question_no){
this.questionINdex = this.questionINdex + 1
if(this.questionINdex > 3){
this.disabled = false
this.questionINdex = 3
}
this.scrollTo()
}
},
sendQuestion: function(){
// アンケート送信処理
},
selectAnswer: function(questionINdex, value){
this.answers[questionINdex] = value
this.nextQuestion()
},
// ページ最下部にスクロール
scrollTo: function() {
this.$nextTick(() => {
var element = document.documentElement;
var bottom = element.scrollHeight - element.clientHeight;
window.scroll({
top:bottom,
behavior: 'smooth'
})
})
},
},
}
</script>
CSS
<style scoped>
:root {
/* --font-color: #684D35;*/
--font-color: #000000;
}
〜 中略 〜
.contents_question{
width:100%;
max-height:300px;
}
/* チャット風吹き出し */
.fukidashi{
margin: 30px 0;
display:flex;
justify-content: flex-start;
align-items: flex-start;
}
.chaticon img{
width: 80px;
height: auto;
}
.fukidashi .chaticon{
margin-right:25px;
}
.lines {
max-width:500px;
display: flex;
flex-wrap: wrap;
position: relative;
padding: 17px 13px 15px 18px;
border-radius: 12px;
background: #99dddd;
box-sizing:border-box;
margin:0 !important;
line-height:1.5;
}
.lines p{
margin:8px 0 0 !important;
}
.lines p:first-child{
margin-top:0 !important;
}
.lines:after {
content: "";
position: absolute;
border: 10px solid transparent;
}
.fukidashi .lines:after {
left: -26px;
border-right: 22px solid #99dddd;
}
/* 設問部分のアニメーション */
.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active {
transition: all .3s ease-out;
}
.next-enter-from {
opacity: 0;
transform: translateY(20px);
}
.next-enter-to {
opacity: 1;
transform: translateY(0);
}
.next-leave-from {
opacity: 1;
transform: translateY(0);
}
.next-leave-to {
opacity: 0;
transform: translateY(20px);
}
.prev-enter-from {
opacity: 1;
transform: translateY(0);
}
.prev-enter-to {
opacity: 0;
transform: translateY(20px);
}
.prev-leave-from {
opacity: 0;
transform: translateY(20px);
}
.prev-leave-to {
opacity: 1;
transform: translateY(0);
}
</style>
解説
チャット風アニメーション
<transition :name=trans_chat>
アニメーションする部分
</transition>
trans_chatの値を変更することでアニメーションの動作を制御しています。
今回の場合は、
next : 次の設問を表示するアニメーション
prev : 前の設問を表示するアニメーション
という値で制御しています。
アニメーションはopacityでふわっと表示されるを表現して、
translateYで下から浮かび上がる表現をしています。
/* 設問部分のアニメーション */
.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active {
transition: all .3s ease-out;
}
.next-enter-from {
opacity: 0;
transform: translateY(20px);
}
.next-enter-to {
opacity: 1;
transform: translateY(0);
}
.next-leave-from {
opacity: 1;
transform: translateY(0);
}
.next-leave-to {
opacity: 0;
transform: translateY(20px);
}
.prev-enter-from {
opacity: 1;
transform: translateY(0);
}
.prev-enter-to {
opacity: 0;
transform: translateY(20px);
}
.prev-leave-from {
opacity: 0;
transform: translateY(20px);
}
.prev-leave-to {
opacity: 1;
transform: translateY(0);
}
設問の回答選択処理
選択肢を選択すると、メソッドselectAnswerを発火します
引数に設問番号と選択した値を設定しています
selectAnswer: function(questionINdex, value){
this.answers[questionINdex] = value
this.nextQuestion()
},
回答用配列に選択肢の値を格納して、nextQuestionメソッドを呼び出しています。
nextQuestion: function(){
this.trans_chat = 'next'
if(this.questionINdex <= question_no){
this.questionINdex = this.questionINdex + 1
if(this.questionINdex > 3){
this.disabled = false
this.questionINdex = 3
}
this.scrollTo()
}
},
trans_chatに'next'をセットしてアニメーションを発火します。
this.trans_chat = 'next'
前の設問を選択し直したときに新しい設問が表示しないように、ifで最新の設問の回答であるかを判定しています。
if(this.questionINdex <= question_no){
設問番号をインクリメントします。
this.questionINdex = this.questionINdex + 1
最後の設問の回答時には次の設問がないので、設問番号を固定にします。
このときに送信ボタンを有効(disabledをfalse)にします。
if(this.questionINdex > 3){
this.disabled = false
this.questionINdex = 3
}
次の設問が表示されるときに画面外の表示となることがあるので、画面最下部へスクロールイベントを発火
this.scrollTo()
設問の表示
設問は配列に格納してv-forでループして順次取り出し、表示しています。
<div v-for="(question, questionKey) in questions">
// 設問表示
</div>
チャット風CSS
チャット風にする為設問は吹き出しをCSSで表現しています。
設問文は先のループで配列から取り出して表示しています。
<div class="fukidashi">
<div class="chaticon"><img src="チャットアイコン画像"/></div>
<p class="lines">問{{ questionKey + 1 }}. {{ question.question.text01 }}</p>
</div>