入力フォームには1つのページで完了するシンプルなものから複数のページを介して完了するものまでいろいろな形があります。本文書ではvue.jsを使ってマルチステップフォームを作成するための手順を説明しています。

各ステップにバリデーションが入っていないため実用的ではありませんが、マルチステップフォームを作る上での基本的は考え方とコンポーネントの理解を深めることができます。

作成するマルチステップフォームの概要

作成するフォームは4つのステップに別れ、1つのステップに1つのコンポーネントが対応します。最後のステップは確認画面のため入力フォームがあるのは3つのステップです。

1つ目のステップの入力フォームでは名前(firstName, lastName)を入力、2つ目のステップの入力フォームではEmail、電話番号を入力、3つ目は誕生日を入力するシンプルなものです。4つ目は確認画面なので1〜3で入力した項目の値を一括表示させます。

マルチステップフォームの作成

vue.jsはcdnを利用して行うため、vueプロジェクトの作成等は必要ありません。任意の場所に作成したindex.htmlファイルを使ってコードを記述していきます。ステップごとに対応するコンポーネントを作成し、index.htmlファイルにすべてのコードを記述します。

index.htmlファイルの作成

最初にベースとなるVueインスタンスとhtmlを記述します。入力フォームを作成するためにbootstrapを読み込んでいます。bootstrapは見栄えを少し良くするために利用しているので使用は必須ではありません。vue.js、bootstrapどちらもcdnから読み込んでいます。

dataプロパティには、stepNumberと入力フォームの値を保存するformオブジェクトを設定します。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Multi Step Form</title>
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" >
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>

<div id="app" class="container mt-5">
	<div class="row">
		<div class="col-md-8">
		<h1>Step:{{ stepNumber}}</h1>
		</div>
	</div>
</div>
<script>
	const app = new Vue({
		el: '#app',
		data: {
			stepNumber: 1,
			form: {
				firstName: null,
				lastName: null,
				Email: null,
				tel: null,
				birthday: null
			}
		},
	});
</script>
</body>
</html>

ブラウザで確認するとブラウザには、Step:1のみ表示されます。

初期画面
初期画面

1つ目のフォーム作成(Step:1)

名前を入力するフォームを持つコンポーネントform-nameを追加します。firstNameとlastNameを入力する2つのinput要素を持ち、v-modelディレクティブを使ってバイディングを行なっています。


Vue.component('form-name',{
	template: `<form>
				<h2>Name Info</h2>
				<div class="form-group">
				<label for="firstName">First Name</label>
				<input type="text" class="form-control" v-model="firstName" placeholder="Enter First Name">
				</div>					
				<div class="form-group">
				<label for="lastName">Last Name</label>
				<input type="text" class="form-control" v-model="lastName" placeholder="Enter Last Name">
				</div>
				</form>`,
	data: function(){
		return {
				firstName: null,
				lastName: null
		}
	},
});

作成したform-userタグをhtmlに追加し、v-ifディレクティブでstepNumberが1の場合のみ表示させる設定を行います。またデバッグ用にリアルタイムでformオブジェクトの値が確認できるように{{ form }}も追加しています。


<div id="app" class="container mt-5">
	<div class="row">
		<div class="col-md-8">
		<h1>Step:{{ stepNumber }}</h1>
		<form-name v-if="stepNumber === 1"></form-name>
		</div>

		<pre><code>{{ form }}</code></pre>
		
	</div>
</div>
<script>

入力フォームとformオブジェクトの値がブラウザに表示されます。

名前の入力フォーム
名前の入力フォーム

この段階でinput要素に入力を行なってもformの値は変わりません。入力フォームの入ったコンポーネントform-userは子コンポーネントなのでformオブジェクトを持つ親コンポーネントにデータを渡す必要があります。

子から親コンポーネントにデータを渡す時は、$emitを利用します。コンポーネント間でのデータの受け渡しについては下記の文書を参考にしてください。

form-userコンポーネントのformタグにinputイベントを設定します。input要素に入力を行うとsubmitメソッドが実行されます。

methodsにsubmitメソッドの追加を行います。submitメソッドの中で$emitを使用します。$emitを使って、データプロパティのfirstNameとlastNameを親コンポーネントに渡します。


Vue.component('form-name',{
	template: `<form @input="submit">
//中略
				</form>`,
	data: function(){
		return {
				firstName: null,
				lastName: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				firstName: this.firstName,
				lastName: this.lastName
			});
		}
	}
});

親コンポーネント側では子コンポーネントの$emitで指定したupdateイベントを受け取ります。


<form-name v-if="stepNumber === 1" @update="updateForm"></form-name>

updateイベントを受け取ったら、updateFormメソッドを実行するのでVueインスタンスのmethodsにupdateFormメソッドを追加します。updateFormメソッドではObject.assignを利用して、受け取ったデータでformの値を上書きしています。


const app = new Vue({
	el: '#app',
	data: {
		stepNumber: 1,
		form: {
			firstName: null,
			lastName: null,
			Email: null,
			tel: null,
			birthday: null
		}
	},
	methods:{
		updateForm:function(formData){
			Object.assign(this.form, formData);
		},
	}
});

入力フォームにfirsNameとlastNameを入れてブラウザのformオブジェクトの値に入力した値が表示されれば、子コンポーネントから親コンポーネントへのデータの受け渡しは正常に動作しています。下記ではフォームに入力したJohnがfirstNameにDoeがlastNameに入っていることを確認することができます。

入力した値がformに反映される
入力した値がformに反映される

ステップ移動処理

2つ目のフォームを追加する前に1つ目のステップから次のステップに移動するための処理を追加します。

ステップ間の移動はボタンで行います。そのためBackとNextの2つのボタンをform-userタグの下に追加します。


<button class="btn btn-primary" @click="backStep" >Back</button>
<button class="btn btn-primary" @click="nextStep" >Next</button>

2つのボタンにはクリックイベントを設定しているので、各ボタンのイベントにbackStepとnextStepメソッドを追加します。メソッドはstepNumberの数を減らす処理と増やす処理です。


methods:{
	updateForm:function(formData){
		Object.assign(this.form, formData);
	},
	backStep:function(){
		this.stepNumber--;
	},			
	nextStep:function(){
		this.stepNumber++;
	},
}

ブウラザで確認すると2つのボタンが表示されます。

Back, Nextボタン追加
Back, Nextボタン追加

Nextボタンを押すとstepNumberが2になるためform-userコンポーネントは非表示になり、フォームはブラウザから消えます。

stepNumberが増えるとフォームがブラウザから消えるのは、v-ifディレクティブを使ってfom-userコンポーネントが表示されるのはstepNumberが1の場合と設定しているためです。
fukidashi

stepNumberが2の場合に何もフォームが表示されませんが後ほどstepNumberの2に対応する別のコンポーネントを追加するので問題はありません。

Step:2には何も表示されない
Step:2には何も表示されない

Step:1からBackボタンを押すとStep:0の画面が表示されます。

Step:0画面が表示
Step:0画面が表示

Step:1は最初の画面なのでバックをさせてはいけません。バックさせないため、Step:1ではv-showディレクティブを使ってBackボタンが表示されないように設定を行います。またStep:4では次のステップはないのでNextボタンを非表示にする設定を行なっておきます。


<button class="btn btn-primary" @click="backStep" v-show="stepNumber != 1">Back</button>
<button class="btn btn-primary" @click="nextStep" v-show="stepNumber != 4">Next</button>

ブラウザで確認するとStep:1ではNextボタンのみ表示されます。

Step:1ではBackボタンは非表示
Step:1ではBackボタンは非表示
Step:2ではBack, Nextボタン表示
Step:2ではBack, Nextボタン表示
画像はありませんが、Step:4ではBackボタンしか表示されません。
fukidashi

ここまででステップの移動の実装は完了です。

通常はステップ移動の間に入力したデータのバリデーションが必要になります。

2つ目のフォーム作成(Step:2)

stepNumberが2の時に表示されるフォームを記述したコンポーネントを追加しますが、入力する項目(Email, Tel)以外は1つ目のコンポーネントと変わりません。


Vue.component('form-contact',{
	template: `<form @input="submit">
				<h2>Contact Info</h2>
				<div class="form-group">
				<label for="Email">Email</label>
				<input type="email" class="form-control" v-model="Email" placeholder="Enter email">
				</div>					
				<div class="form-group">
				<label for="tel">Tel</label>
				<input type="text" class="form-control" v-model="tel" placeholder="Enter phone number">
				</div>
				</form>`,
	data: function(){
		return {
			Email: null,
			tel: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				Email: this.Email,
				tel: this.tel
			});
		}
	}
});

追加したform-contactタグをhtmlに追加します。stepNumberが2の時のみform-contactの入力フォームが表示されるようにv-ifディレクティブを設定します。


<form-name v-if="stepNumber === 1" @update="updateForm"></form-name>
<form-contact v-if="stepNumber === 2" @update="updateForm"></form-contact>

これでform-cotactの追加処理は完了です。

ブラウザを開いて、Step:1, Step:2で表示されるフォームに入力を行なってください。

Step:2の画面でEmailとTelに入力すると右のformの値が入力した値になればform-contactの追加作業は正常に動作しています。

Emailと電話番号を入力
Emailと電話番号を入力
上記ではStep:1で入力したfirstName, lastNameとStep:2で入力したEmail, telが右側のformオブジェクトに入っていることを確認してください。
fukidashi

3つ目のフォーム作成(Step:3)

3つ目のフォームも2つ目の追加と全く同じです。3つ目のフォームではinput要素はbirthdayの1つのみです。


Vue.component('form-birthday',{
	template: `<form @input="submit">
				<h2>User Info</h2>
				<div class="form-group">
				<label for="Birthday">Birthday</label>
				<input type="date" class="form-control" v-model="birthday" placeholder="Enter Birthday">
				</div>
				</form>`,
	data: function(){
		return {
			birthday: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				birthday: this.birthday				
			});
		}
	}
});

追加したform-birthdayをhtml側に追加します。stepNumberが3の時のみform-birthdayが表示されるようv-ifディレクティブの設定を行います。


<form-name v-if="stepNumber === 1" @update="updateForm"></form-name>
<form-contact v-if="stepNumber === 2" @update="updateForm"></form-contact>
<form-birthday v-if="stepNumber === 3" @update="updateForm"></form-birthday>

Step:3の画面でBirthdayに入力すると右のformの値が入力した値になれば追加作業は正常に動作しています。

Step:3で誕生日を入力
Step:3で誕生日を入力

ここまでで3つのステップの入力フォームの作成は完了です。

4つ目の確認画面(Step:4)

4つ目のステップでは1〜3で入力した値を表示させる確認画面を作成します。

これまでは子コンポーネントから$emitで親コンポーネントに入力データを渡していましたが、最後は親コンポーネントから子コンポーネントにformデータを渡します。この場合はpropsを使います。


Vue.component('form-confirm',{
	template: `<div>
			<h2>Confirmation</h2>
		<p>Name: {{ form.firstName }} {{ form.lastName }}</p>
		<p>Email: {{ form.Email }}</p>
		<p>Tel: {{ form.tel }}</p>
		<p>Birthday: {{ form.birthday }}</p>
		</div>`,
	props:{
		form: Object
	},
});

追加したform-confirmでは、propsで受け取ったformを画面に表示させているだけです。

html側にもform-confirmタグを追加し、v-bindディレクティグでformを渡しています。

Step:1からStep:3まで入力して、Step:4を表示するとこれまで入力した値が表示されます。

確認画面が表示
確認画面が表示

次にBackボタンをクリックしてください。入力した値がすべて消えています。入力した値を保持するための設定が必要となります。

入力した値が消える
入力した値が消える

入力した値を保持するためにコンポーネントをkeep-aliveタグで閉じます。keep-aliveタグを使うことでBackボタンを押しても入力した値が保持された状態になります。

keep-aliveタグを設定した後にすべての項目を入力したStep:4の状態からStep:1に戻っても各ステップの入力フォームには値が保持されています。下記はStep:1に戻った時の画像ですが、input要素に値が保持され、右側のformオブジェクトにもStep:1からStep:3で入力した値が保持されています。

入力した値が保持される
入力した値が保持される

通常ではStep:4のConfirmation画面でConfirmボタンとクリックイベントを設定し入力した値をデータベース等に保存する処理が必要になります。

コンポーネント間でデータの受け渡しを行いながら、マルチステップのフォームを作成することができました。

作成したindex.htmlのコードは下記の通りです。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Multi Step Form</title>
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" >
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>

<div id="app" class="container mt-5">
	<div class="row">
		<div class="col-md-8">
		<h1>Step:{{ stepNumber }}</h1>
		<keep-alive>
		<form-name v-if="stepNumber === 1" @update="updateForm"></form-name>
		<form-contact v-if="stepNumber === 2" @update="updateForm"></form-contact>
		<form-birthday v-if="stepNumber === 3" @update="updateForm"></form-birthday>
		<form-confirm v-if="stepNumber === 4" v-bind:form="form"></form-confirm>
		</keep-alive>

		<button class="btn btn-primary" @click="backStep" v-show="stepNumber != 1">Back</button>
		<button class="btn btn-primary" @click="nextStep" v-show="stepNumber != 4">Next</button>

		</div>


		<pre><code>{{ form }}</code></pre>

	</div>
</div>
<script>

Vue.component('form-name',{
	template: `<form @input="submit">
				<h2>Name Info</h2>
				<div class="form-group">
				<label for="firstName">First Name</label>
				<input type="text" class="form-control" v-model="firstName" placeholder="Enter First Name">
				</div>					åç
				<div class="form-group">
				<label for="lastName">Last Name</label>
				<input type="text" class="form-control" v-model="lastName" placeholder="Enter Last Name">
				</div>
				</form>`,
	data: function(){
		return {
				firstName: null,
				lastName: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				firstName: this.firstName,
				lastName: this.lastName
			});
		}
	}
});

Vue.component('form-contact',{
	template: `<form @input="submit">
				<h2>Contact Info</h2>
				<div class="form-group">
				<label for="Email">Email</label>
				<input type="email" class="form-control" v-model="Email" placeholder="Enter email">
				</div>					
				<div class="form-group">
				<label for="tel">Tel</label>
				<input type="text" class="form-control" v-model="tel" placeholder="Enter phone">
				</div>
				</form>`,
	data: function(){
		return {
			Email: null,
			tel: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				Email: this.Email,
				tel: this.tel
			});
		}
	}
});

Vue.component('form-birthday',{
	template: `<form @input="submit">
				<h2>User Info</h2>
				<div class="form-group">
				<label for="Birthday">Birthday</label>
				<input type="date" class="form-control" v-model="birthday" placeholder="Enter Birthday">
				</div>
				</form>`,
	data: function(){
		return {
			birthday: null
		}
	},
	methods: {
		submit: function(){
			this.$emit('update',{
				birthday: this.birthday				
			});
		}
	}
});

Vue.component('form-confirm',{
	template: `<div>
			<h2>Confirmation</h2>
		<p>Name: {{ form.firstName }} {{ form.lastName }}</p>
		<p>Email: {{ form.Email }}</p>
		<p>Tel: {{ form.tel }}</p>
		<p>Birthday: {{ form.birthday }}</p>
		</div>`,
	props:{
		form: Object
	},
});

const app = new Vue({
	el: '#app',
	data: {
		stepNumber: 1,
		form: {
			firstName: null,
			lastName: null,
			Email: null,
			tel: null,
			birthday: null
		}
	},
	methods:{
		updateForm:function(formData){
			Object.assign(this.form, formData);
		},
		backStep:function(){
			this.stepNumber--;
		},			
		nextStep:function(){
			this.stepNumber++;
		},
	}
});
</script>
</body>
</html>