ステップ式フォームを作成する【javascript/hashchange】

JavaScript

長いフォームをステップ式で作成したのでメモ。
ステップ式といってもフォームの一部を非表示にして、ソレっぽく見せているだけです。
切り替えの判定はURLの#(ハッシュ)値です。ハッシュが変更されたら表示を切り替えています。

完成したフォームを確認する(デモ)

こちらが完成したフォームです。質問ページが一つ一つ切り替わります。
最後に質問結果が表示されますが、フォームの入力値をリアルタイムで反映しているためです。
サンプルではフォームによるサーバーとの通信はありません。

動作デモ

実際の動作はこちらでご確認ください!(別ウィンドウが開きます)

ソースコードを確認(全体)

HTML

今回質問が3つ、質問結果が1つで合計4つのページを切り替えます。
まず質問領域ごとにクラス名をつけます。ここではsection1〜4の名前にしました。このクラス名で質問領域の非表示を判定します。(緑色)
また、質問の共通のクラスform_sectionを設定します。(橙色)

そして、<a>タグのリンクにハッシュ値(#1〜#4)を設定します。(青色)
これでリンク先のURLの最後に#(ハッシュ)が付与されます。

例)https://m-kenomemo.com/sample/step-form/#1

<div class="form_wrap">
    <section class="form_section section1">
        <header class="form_section_header">
            <div class="step">
                <p class="progress"><span></span></p>
            </div>
        </header>
        <div class="form_section__body">
            <h2>Q1.あなたのお名前を教えてください</h2>
            <div class="form_section-item form_section__name">
                <input type="text" placeholder="姓" class="form-item sei">
                <input type="text" placeholder="名" class="form-item mei">
            </div>
        </div>
        <div class="form_section__btn">
            <ul>
                <li class="fas" style="visibility:hidden;"><a href="#1"><i class="fas fa-arrow-circle-left"></i></a></li>
                <li class="fas"><a href="#2"><i class="fas fa-arrow-circle-right"></i></a></li>
            </ul>
        </div>
    </section>
    <section class="form_section section2">
        <header class="form_section_header">
            <div class="step">
                <p class="progress"><span></span></p>
            </div>
        </header>
        <div class="form_section__body">
            <h2>Q2.あなたの性別を教えてください</h2>
            <div class="form__section-item form_section__sex">
                <div class="sex_wrap">
                <span class="radio_wrap">
                    
                    <label>
                        <input value="女性" type="radio" name="sex" class="radio form-item">女性
                    </label>
                </span>
                <span class="radio_wrap">
                    <label>
                        <input value="男性" type="radio" name="sex" class="radio form-item">男性
                    </label>
                </span>
            </div>
        </div>
        </div>
        <div class="form_section__btn">
            <ul>
                <li class="fas"><a href="#1"><i class="fas fa-arrow-circle-left"></i></a></li>
                <li class="fas"><a href="#3"><i class="fas fa-arrow-circle-right"></i></a></li>
            </ul>
        </div>
    </section>
    <section class="form_section section3">
        <header class="form_section_header">
            <div class="step">
                <p class="progress"><span></span></p>
            </div>
        </header>
        <div class="form_section__body">
            <h2>Q3.質問があればどうぞ!
            </h2>
            <div class="form__section-item form_section__question">
                <textarea id="kanso" class="form-item" name="kanso" cols="40" rows="10" maxlength="20" placeholder="ご質問を記入ください"></textarea>
            </div>
        </div>
        <div class="form_section__btn">
            <ul>
                <li class="fas"><a href="#2"><i class="fas fa-arrow-circle-left"></i></a></li>
                <li class="fas"><a href="#4"><i class="fas fa-arrow-circle-right"></i></a></li>
            </ul>
        </div>
    </section>
    <section class="form_section section4">
        <header class="form_section_header">
            <div class="step">
                <p class="progress"><span></span></p>
            </div>
        </header>
        <div class="form_section__body">
            <h2>結果
            </h2>
            <div class="form__section-item form_section__result">
                <dl>
                    <dt>Q1.あなたのお名前を教えてください</dt>
                    <dd id="q1"><span id="q1-1">入力されていません</span> <span id="q1-2"></span></dd>
                    <dt>Q2.あなたの性別を教えてください</dt>
                    <dd id="q2">入力されていません</dd>
                    <dt>Q3.質問があればどうぞ!</dt>
                    <dd id="q3">入力されていません</dd>
                </dl>
            </div>
        </div>
        <div class="form_section__btn">
            <ul>
                <li class="fas"><a href="#3"><i class="fas fa-arrow-circle-left"></i></a></li>
                <li class="fas" style="visibility:hidden;"><a href="#4"><i class="fas fa-arrow-circle-right"></i></a></li>
            </ul>
        </div>
    </section>
</div>

javascript

URLのハッシュ値によって、どの質問を表示するか制御します。
また、ページ切り替え時にプログレスバーを表示します。

(function () {
    //フォームの質問ページ全て取得
    const sectionAllElm = document.querySelectorAll('.form_section');

    // ページ切り替え
    function urlChangeHandler() {
        const pageid = parseUrl(location.hash);
        const targetElm = document.querySelector(`.section${pageid}`);

        // 全てのページ非表示
        sectionAllElm.forEach(function (elm) {
            elm.classList.remove('appear');
            elm.classList.add('disappear');
        });
        // ページ表示
        targetElm.classList.remove('page-leave');
        targetElm.classList.remove('disappear');
        targetElm.classList.add('page-enter');
        targetElm.classList.add('appear');

        //プログレスバー表示
        progress();
    }
    //プログレスバー表示
    function progress() {
        const sectionCount = sectionAllElm.length;
        const progressWrap = document.querySelectorAll('.progress');

        progressWrap.forEach(function (progress, i) {
            progress.style.width = 0;
            progress.querySelector('span').style.display = "none";
            setTimeout(function () {
                const percentEml = Math.ceil((100 / (sectionCount - 1)) * i) + "%";
                progress.style.width = percentEml;
                progress.querySelector('span').textContent = percentEml;
                progress.querySelector('span').style.display = "inline";
            }, 1000)

        });
    }

    // ページ番号取り出し
    function parseUrl(url) {
        return url.slice(1) || 1;
    }

    //ハッシュが変更されたらイベント実行
    window.addEventListener('hashchange', urlChangeHandler);

    // 初期イベント実行
    urlChangeHandler();

})();

CSS

CSSは長いので、ページ切り替えの動作に関係するコードを抜き出しました。
これらのクラス名は、javascriptで付与、削除を制御します。

//プログレスバー
.form_section .progress {
    width: 0;
    height: 2px;
    border-radius: 20px;
    background: #fff;
    text-align: left;
    line-height: 1.8;
    font-size: .8em;
    transition: all 1s ease;
}

.form_section .progress span {
    display: none;
    transition: all 1s ease;
}
//質問領域の表示
.form_section.appear {
    display: flex;
}
//質問領域の非表示
.form_section.disappear {
    display: none;
}
//アニメーション遷移
.form_section.page-enter {
    -webkit-animation: fadein 1.5s ease-out;
}
.form_section.page-leave {
    -webkit-animation: fadeout .4s ease-out;
}
@-webkit-keyframes fadein {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}
@-webkit-keyframes fadeout {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}

hashchangeイベントでページが切り替わるようにする

URLのハッシュが変更されたことを感知するイベントとしてhashchangeというイベントがあります。

今回、ハッシュが変更されたことを感知してページが切り替わるようにします。
こちらのイベントに、処理内容をまとめた関数urlChangeHandlerを設定します。

//ハッシュが変更されたらイベント実行
window.addEventListener('hashchange', urlChangeHandler);

ハッシュが変更された時の処理

hashchangeイベントに設定した関数urlChangeHandlerの処理は次の通りです。

  • 表示する質問領域をターゲット要素として取得(ハッシュ値で判定)
  • 全ての質問領域を非表示
  • ターゲット要素をアニメーションで表示させる

コードは次の通りになります。

// ページ切り替え
function urlChangeHandler() {
    const pageid = parseUrl(location.hash);
    const targetElm = document.querySelector(`.section${pageid}`);

    // 全てのページ非表示
    sectionAllElm.forEach(function (elm) {
        elm.classList.remove('appear');
        elm.classList.add('disappear');
    });
    // ページ表示
    targetElm.classList.remove('disappear');
    targetElm.classList.remove('page-leave');
    targetElm.classList.add('page-enter');
    targetElm.classList.add('appear');
}

表示する質問領域をターゲット要素として取得(ハッシュ値で判定)

表示する領域はURLの#(ハッシュ)値の番号で判定します。

おさらいですが、HTMLの各質問領域にはクラス名section1〜3のクラス名が設定されています。
そして、ページが切り替わるごとにURLのハッシュ値の番号が変わります。

URLのハッシュ値の番号と同じクラス名をターゲット要素として判定します。

ページ番号を取り出す

まず、現在表示しているページのURLからハッシュ値を取り出します
具体的にはこのURLからハッシュ値(#以降)だけを取得します。

例)https://m-kenomemo.com/sample/step-form/#1

ページ番号をとりだすコードはこちらです。

const pageid = parseUrl(location.hash);
 // ページ番号取り出し
function parseUrl(url) {
    return url.slice(1) || 1;
}

メソッドlocation.hashはURLの#以降の値を取得します。今回の場合(#1〜#4)のどれかが取得できます。

文字列.slice(1)メソッドは2番目以降の文字列を返します。これで#を除いた数字のみが取得できます。
ページの読み込み時は、ハッシュがURLに付与されていないので「1」を返すようにします。

一連の処理を関数parseUrl(url)にまとめます。戻り値としてページ番号を返すので、変数に代入して使用します。今回、pageidという変数に値を格納します。

ページ番号から、表示するターゲット要素を取得する

先ほど、取得したページ番号を変数pageidに格納しました。
このページ番号を使って表示領域の要素を取得します。

document.querySelector(<セレクタ>)メソッドを使うと、セレクタで指定した最初の要素が1つ取得できます。

先ほど作成した関数を使うと、このようにしてターゲット要素が取得できます。
最終的には変数targetElmに要素を格納します。

const pageid = parseUrl(location.hash);
const targetElm = document.querySelector(`.section${pageid}`);

全ての質問領域を非表示

ターゲット要素を表示する前に、全ての質問領域を非表示にします。
表示、非表示はCSSのクラス名を動的に付与することで実装します。クラス名は次の通りです。

クラス名動作
appear表示
disappear非表示

すべての質問領域を取得

全ての質問領域を非表示にするために、まず、質問領域をすべて取得します。
共通で設定したクラス名form_sectionで取得します。
document.querySelectorAll(emement)メソッドで各要素がオブジェクトで取得できます。

const sectionAllElm = document.querySelectorAll('.form_section');

コンソールログで確認すると値がオブジェクトで取得されています。

複数ある要素に非表示用のクラス名を設定する

要素が複数あるので、forEach文ですべての要素に非表示のクラス名を設定します。
その際、表示用のクラス名があれば削除します。

// 全てのページ非表示
sectionAllElm.forEach(function (elm) {
    elm.classList.remove('appear');
    elm.classList.add('disappear');
});

これで全ての要素を非表示することができました。

ターゲット要素をアニメーションで表示させる

すべての要素を非表示したら、ターゲット要素を表示します。
せっかくなので、画面が切り替わる際、アニメーションさせたいと思います。アニメーションは、CSSのクラス名を付与することで実装します。
アニメーションに関するCSSのクラス名は次のとおりです。

クラス名動作
page-enterふわっと表示
page-leaveふわっと非表示

これらのクラス名を先ほどの非表示用のクラス名と一緒に指定します。

// ページ表示
targetElm.classList.remove('page-leave'); →ふわっと非表示
targetElm.classList.remove('disappear'); →非表示
targetElm.classList.add('page-enter'); →ふわっと表示
targetElm.classList.add('appear'); →非表示

こうすると画面が切り替わる時、ふわっと表示されてソレっぽくなります。
これで、ページの切り替えができるようになりました!

まとめ

今回、hashchangeイベントでページが切り替わっているような挙動を実装しました。
サーバー側の実装ができない時、なんちゃってステップフォームとして使えるかもしれません。

ページ切り替え以外に、プログレスバーやフォームのリアル反映も実装しました。
こちらについては時間があり次第、実装方法をまとめたいと思います!