BLOG

Paakのスタッフが得た知見や情報を発信するブログです。
たまに会社の出来事に関する記事もお届けします。

診断系アンケート機能の実装方法 - firestore

下記サイトのような診断系アンケート機能の実装方法を簡単に解説します。

https://store.medulla.co.jp/questions?_ga=2.212619607.257598691.1623203044-1132281498.1618216955

データの保存方法はfirestoreを利用します。
firestoreのプロジェクト作成方法は省きます。

まず、htmlとcssを下記の感じで用意します。

<div class="c-firestore02 c-inner">
   <h2 class="page-head">Ask three questions</h2>
   <button class="c-button" id="js-start-button">START</button>
 
   <div class="c-question01" id="js-question01">
     <h2 class="page-head mt-40">1.髪の長さは?</h2>
 
     <form class="input-area" id="form1" onsubmit="return false">
       <div class="input-box">
         <div class="radio-wrap">
           <p class="input-wrap"><input type="radio" name="hairLength" id="hairLength1" value="SHORT" required checked><label for="hairLength1">SHORT</label></p>
           <p class="input-wrap"><input type="radio" name="hairLength" id="hairLength2" value="MEDIUM"><label for="hairLength2">MEDIUM</label></p>
           <p class="input-wrap"><input type="radio" name="hairLength" id="hairLength3" value="LONG"><label for="hairLength3">LONG</label></p>
         </div>
       </div>
       <input class="c-button" type="submit" name="" id="js-next-button01" value="NEXT">
       <button class="c-back-button mt-10" id="js-back-button01">BACK</button>
     </form>
   </div>
 
   <div class="c-question02" id="js-question02">
     <h2 class="page-head mt-40">2.頭皮の状態は?</h2>
 
     <form class="input-area" id="form2" onsubmit="return false">
       <div class="input-box">
         <div class="radio-wrap">
           <p class="input-wrap"><input type="radio" name="hairStatus" id="hairStatus1" value="ノーマル" required checked><label for="hairStatus1">ノーマル</label></p>
           <p class="input-wrap"><input type="radio" name="hairStatus" id="hairStatus2" value="乾燥・かゆみ"><label for="hairStatus2">乾燥・かゆみ</label></p>
           <p class="input-wrap"><input type="radio" name="hairStatus" id="hairStatus3" value="べたつき"><label for="hairStatus3">べたつき</label></p>
         </div>
       </div>
       <input class="c-button" type="submit" name="" id="js-next-button02" value="NEXT">
       <button class="c-back-button mt-10" id="js-back-button02">BACK</button>
     </form>
   </div>
 
   <div class="c-question03" id="js-question03">
     <h2 class="page-head mt-40">3.くせレベルは?</h2>
 
     <form class="input-area" id="form3" onsubmit="return false">
       <div class="input-box">
         <div class="radio-wrap">
           <p class="input-wrap"><input type="radio" name="hairHabit" id="hairHabit1" value="直毛" required checked><label for="hairHabit1">直毛(縮毛矯正含む)</label></p>
           <p class="input-wrap"><input type="radio" name="hairHabit" id="hairHabit2" value="うねり"><label for="hairHabit2">うねり</label></p>
           <p class="input-wrap"><input type="radio" name="hairHabit" id="hairHabit3" value="カール"><label for="hairHabit3">カール(パーマ含む)</label></p>
         </div>
       </div>
       <input class="c-button" type="submit" name="" id="js-next-button03" value="NEXT">
       <button class="c-back-button mt-10" id="js-back-button03">BACK</button>
     </form>
   </div>
 
   <div class="c-result" id="js-result">
     <h2 class="page-head mt-40">診断結果</h2>
     <div class="result-box">
       <p class="result-text" id="js-result-text"></p>
     </div>
     <button class="c-button" id="js-end-button">閉じる</button>
   </div>
 </div>

 .c-inner {
   width: 90%;
   max-width: 140rem;
   margin-right: auto;
   margin-left: auto;
 }
 
 .c-firestore02 {
   margin-top: 5rem;
   margin-bottom: 5rem;
 
   .page-head {
     margin-bottom: 10rem;
     font-size: 4.5rem;
     text-align: center;
   }
 
   .c-button {
     display: block;
     width: 80%;
     max-width: 35rem;
     padding: 1.7rem 0;
     margin: 0 auto;
     // font-family: 'Montserrat';
     font-size: 1.8rem;
     color: #fff;
     letter-spacing: 0.1em;
     cursor: pointer;
     background-color: #555;
     border: 1px solid #555;
     border-radius: 1rem;
 
     @include r.mq(mdover) {
       &:hover {
         color: #555;
         background-color: #fff;
       }
     }
 
     &:active {
       color: #fff !important;
       background-color: #444 !important;
     }
   }
 
   .c-back-button {
     display: block;
     min-width: 15rem;
     padding: 0.5rem 2rem;
     margin-right: auto;
     margin-left: auto;
     border: 1px solid #555;
     border-radius: 6px;
   }
 
   .c-question01,
   .c-question02,
   .c-question03,
   .c-result {
     position: fixed;
     top: 0;
     left: 0;
     z-index: -1;
     width: 100%;
     height: 100%;
     text-align: center;
     background-color: #f0f0f0;
     opacity: 0;
   }
 
   .c-question01.is-view,
   .c-question02.is-view,
   .c-question03.is-view,
   .c-result.is-view {
     z-index: 10;
     opacity: 1;
   }
 
   .input-area {
     text-align: center;
   }
 
   .input-box {
     display: inline-block;
     margin: 0 auto 10rem;
   }
 
   .radio-wrap {
     display: flex;
     align-items: center;
   }
 
   .input-wrap {
     display: flex;
     align-items: center;
     margin: 0 1rem;
 
     label {
       margin-left: 0.5rem;
     }
   }
 
   .result-box {
     display: inline-block;
     width: 90%;
     max-width: 60rem;
     margin-bottom: 10rem;
   }
 }

cssで最低限重要なのは.c-question〇〇と.c-question〇〇.is-viewなので、それ以外のスタイルは自由に変更しても大丈夫です。

ここからJSでBACKボタンとNEXTボタンを押した際にフォームが動的に遷移するようにしていきます。
TypeScriptが少し入っていますが、JSで書く場合は不必要な型宣言などを適宜消してください。

 // firebaseのパッケージをインポート(予めパッケージはインストールしておいてください)
 import firebase from 'firebase/app'
 import 'firebase/analytics'
 import 'firebase/auth'
 import 'firebase/firestore'
 
 {
    // firebase初期化
    firebase.initializeApp({
      apiKey: '<apiKey>',
      authDomain: '<authDomain>',
      projectId: '<projectId>',
    })
    const db = firebase.firestore()
    
    // 各ボタンを取得して変数化
    const startButton = document.querySelector('#js-start-button') as HTMLElemen
    const nextButton01 = document.querySelector(
      '#js-next-button01'
    ) as HTMLElement
    const nextButton02 = document.querySelector(
      '#js-next-button02'
    ) as HTMLElement
    const nextButton03 = document.querySelector(
      '#js-next-button03'
    ) as HTMLElement
    const backButton01 = document.querySelector(
      '#js-back-button01'
    ) as HTMLElement
    const backButton02 = document.querySelector(
      '#js-back-button02'
    ) as HTMLElement
    const backButton03 = document.querySelector(
      '#js-back-button03'
    ) as HTMLElement
    const endButton = document.querySelector('#js-end-button') as HTMLElement
  
    // 各フォームのブロックを取得して変数化
    const question01 = document.querySelector('#js-question01') as HTMLElement
    const question02 = document.querySelector('#js-question02') as HTMLElement
    const question03 = document.querySelector('#js-question03') as HTMLElement
    const result = document.querySelector('#js-result')
  
    // 各フォームを取得して変数化
    const form1 = document.getElementById('form1') as HTMLFormElement
    const form2 = document.getElementById('form2') as HTMLFormElement
    const form3 = document.getElementById('form3') as HTMLFormElement
  
    // 結果画面のテキスト
    const resultText = document.getElementById('js-result-text') as HTMLElement
  
    // 全ブロックからis-viewクラスを削除する処理を関数化しておく
    const removeViewClass = () => {
      question01?.classList.remove('is-view')
      question02?.classList.remove('is-view')
      question03?.classList.remove('is-view')
      result?.classList.remove('is-view')
    }
  
    // ボタンのクリックイベントを関数化しておく(第一引数にボタンの変数,第二引数に先頭に表示するブロックの変数(必要な場合))
    const clickButtonEvent = (trigger: HTMLElement, view: HTMLElement | null) => {
      trigger?.addEventListener('click', () => {
        removeViewClass()
        if (view) {
          view?.classList.add('is-view')
        }
      })
    }
  
    // ボタンのクリック処理
    clickButtonEvent(startButton, question01)
    clickButtonEvent(nextButton01, question02)
    clickButtonEvent(nextButton02, question03)
  
    clickButtonEvent(backButton01, null)
    clickButtonEvent(backButton02, question01)
    clickButtonEvent(backButton03, question02)
  
    // 最後のnextボタン
    nextButton03?.addEventListener('click', () => {
      removeViewClass()
      result?.classList.add('is-view')
  
      // 各フォームのラジオボタンで選択された値を取得
      const resultValue01 = form1.hairLength.value
      const resultValue02 = form2.hairStatus.value
      const resultValue03 = form3.hairHabit.value
  
      const execute = async () => {
        // 書き込み
        const write = () => {
          return new Promise((resolve: any) => {
            db.collection('<コレクション名>')// コレクション名,フィールド名(field1,..)は自由に変更してください。
              .add({
                field1: resultValue01,
                field2: resultValue02,
                field3: resultValue03,
                timestamp: firebase.firestore.Timestamp.fromDate(new Date()),// 書き込み時にtimestampを設定すると追加順に並び替えられる
              })
              .then(() => {
                resolve()
              })
              .catch((error: any) => {
                console.log('Error adding document: ', error)
                alert('保存に失敗しました')
              })
          })
        }
  
        // フォームの値によって結果に表示するテキストを分岐する
        if (resultValue02 === 'ノーマル' && resultValue03 === '直毛') {
          resultText.innerHTML = 'あなたはタイプAです。'
        } else if (
          resultValue02 === '乾燥・かゆみ' &&
          resultValue03 === 'カール'
        ) {
          resultText.innerHTML = 'あなたはタイプBです。'
        } else if (resultValue02 === 'べたつき' && resultValue03 === 'うねり') {
          resultText.innerHTML = 'あなたはタイプCです。'
        } else {
          resultText.innerHTML = 'あなたはタイプDです。'
        }
  
        // write(firestoreに書き込む処理)を待ってからフォームの値をリセットする
        await write()
        form1.reset()
        form2.reset()
        form3.reset()
      }
      execute()
    })
  
    // 結果画面の閉じるボタン
    clickButtonEvent(endButton, null)
 }

必要な説明はコード中に書きました。
簡単に説明すると、NEXTボタンをクリックした時に次に表示するフォームのブロックにis-viewクラスを付けて、z-indexとopacityを操作して先頭に表示させてます。BACKボタンも同様です。
そして、最後のNEXTボタンを押した時だけにfirestoreに書き込む処理を行い、結果に表示するテキストを分岐して代入し、フォームをリセットさせてます。

CONTACT /

webサイトでお困りのことがあれば、お気軽にご相談ください。

お問い合わせはこちら

CONTACT /

Paakについて、案件のご相談、採用についてはお問い合わせフォームからご連絡ください。