Vue.jsでカウンター機能を実装【親子間のデータ受け渡し】

カウンター機能プログラミング備忘録

どうも、ちゃんカマでございます。

もしかして、Vue.jsでカウンター機能を実装する方法についてお調べですか?
カウンター機能を実装するポイントは、「親子間のデータ受け渡し」です。

私は点数計算を行うWebアプリを制作する過程で、Vue.jsで親子間のデータ受け渡しを行って、カウンター機能を実装することが出来ました。

今回はVue.jsで親子間のデータ受け渡しを行って、カウンター機能を実装する方法について解説します。

ちなみに私が作ったアプリはこんな感じです。( ↓ GIF動画参照)

点数計算アプリの動作の様子
点数計算アプリの動作の様子

「+」や「ー」のボタンを押すと数値が増減する、カウンター機能を実装しています。

この記事ではこんな疑問や悩みにお答えします
  • Vue.jsで数字のカウンター機能を実装したい
この記事の内容
  • Vue.jsでカウンター機能を実装する方法
  • Vue.jsのprops、methods、$emitの使い方
  • 完成コードも記載しています
スポンサーリンク

カウンター機能を実装していないコードを記載

カウンター機能を実装していない状態のコードを下記に記載します。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>

	<!-- Normalize CSS -->
	<link rel="stylesheet" href="normalize.css">

	<!-- Bootstrap CSS -->
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css">

	<!-- Style CSS -->
	<link rel="stylesheet" href="style.css">

	<!-- Vue.js(本番用) -->
	<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> -->

	<!-- Vue.js(開発用) -->
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
	<table class="table table-bordered table-striped">
		<thead class="thead-light">
			<tr>
				<th class="col-sm"></th>
				<th class="col-lg">ゲート1</th>
				<th class="col-lg">ゲート2</th>
				<th class="col-lg">ゲート3</th>
				<th class="col-lg">ゲート4</th>
				<th class="col-lg">ゴール</th>
				<th class="col-md">総打数</th>
				<th class="col-md">加算点</th>
				<th class="col-md">Total</th>
			</tr>
		</thead>
		<tbody class="row-item">
			<tr>
				<th>Aさん</th>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in>ネットイン</hoop-in>
				</td>
				<td>総打数</td>
				<td>加算点</td>
				<td>Total</td>
			</tr>
			<tr>
				<th>Bさん</th>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in></hoop-in>
				</td>
				<td>
					<counter-hit></counter-hit>
					<hoop-in>ネットイン</hoop-in>
				</td>
				<td>総打数</td>
				<td>加算点</td>
				<td>Total</td>
			</tr>
		</tbody>
	</table>

	<!-- Bootstrap JavaScript -->
	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"></script>
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>

	<!-- Main.js -->
	<script src="main.js"></script>
</body>
</html>
main.js
const counterHit = {
	template: `
		<form>
			打数
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-primary btn-up">+</button>
				</div>
				<input type="number" min="0" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-down">-</button>
				</div>
			</div>
		</form>
	`,
};

const hoopIn = {
	template: `
		<form>
			<slot>フープイン</slot>
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-info btn-add">○</button>
				</div>
				<input type="text" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-remove">☓</button>
				</div>
			</div>
		</form>
	`,
};

new Vue({
	el: '.row-item',
	components: {
		'counter-hit': counterHit,
		'hoop-in': hoopIn,
	},
});
style.css
@charset "UTF-8";

body {
	min-width: 1024px;
	font-size: 18px;
	padding: 5px;
}

.col-sm {
	width: 8%;
}

.col-lg {
	width: 14%;
}

.col-md {
	width: 8%;
}

th {
	text-align: center;
}

.form-control {
	padding: 0;
	text-align: center;
}

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

input[type="number"][disabled],
input[type="text"][disabled]{
	background-color: #fff;
}

.btn-up,
.btn-down,
.btn-add,
.btn-remove {
	width: 37px;
}

アプリの外観は整っていますが、ボタンを押しても何も反応しない状態です。

反応しないボタン
反応しないボタン
スポンサーリンク

Vue.jsの親と子とは

カウンター機能を実装するためには、親子間でデータをやり取りする必要があります。
Vue.jsの親と子については、下記の記事が参考になると思います。

Vue.jsでコンポーネントの親子間でデータのやり取りを行う方法 - Qiita
最近Vue.jsを使ったプロジェクトに手を出しています。 そこでコンポーネント間で値をやり取りする方法を調べていましたが、 こうするべきでしょ、というのを私なりに探っていった結果をまとめました。 こちらのコードの例で使用しているVu...

今回は「counterHit」と「hoopIn」のコンポーネントが子、「new Vue」で作られているVueインスタンスが親になります。

コンポーネントにprops: [‘value’]を渡す

子であるコンポーネントに、親からデータを渡します。
そのためには、main.jsに下記のとおりにコードを書きます。

main.js
const counterHit = {
	props: ['value'],
	template: `
		<form>
			打数
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-primary btn-up">+</button>
				</div>
				<input type="number" min="0" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-down">-</button>
				</div>
			</div>
		</form>
	`,
};

const hoopIn = {
	props: ['value'],
	template: `
		<form>
			<slot>フープイン</slot>
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-info btn-add">○</button>
				</div>
				<input type="text" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-remove">☓</button>
				</div>
			</div>
		</form>
	`,
};

<!-- 以下は省略 -->

「counterHit」と「hoopIn」コンポーネントのpropsオプションに[‘value’]を指定します。

props: ['value'],

これで、子であるコンポーネントに、valueという名前のデータが親から渡されました。

methodsにボタンを押した時の処理を書く

ボタンを押したら、valueの値が変更される処理を書きます。
処理の内容は、コンポーネントのmethodsオプションに書きます。

main.jsに下記のとおりに書きます。

main.js
const counterHit = {
	props: ['value'],
	methods: {
		countUp() {
			this.$emit('input', this.value + 1);
		},
		countDown() {
			if ( this.value > 0 ) {
				this.$emit('input', this.value - 1);
			}
		},
	},
	template: `
		<form>
			打数
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-primary btn-up">+</button>
				</div>
				<input type="number" min="0" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-down">-</button>
				</div>
			</div>
		</form>
	`,
};

const hoopIn = {
	props: ['value'],
	methods: {
		add() {
			this.$emit('input', "○");	
		},
		remove() {
			this.$emit('input', "");
		},
	},
	template: `
		<form>
			<slot>フープイン</slot>
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" class="btn btn-info btn-add">○</button>
				</div>
				<input type="text" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" class="btn btn-secondary btn-remove">☓</button>
				</div>
			</div>
		</form>
	`,
};

<!-- 以下は省略 -->

methodsオプションの中に、ボタンを押した時の処理を書いています。

counterHitコンポーネントでは、+ボタンを押したらvalueの値が+1(countUp)、ーボタンを押したらvalueの値がー1(countDown)となる処理です。

hoopInコンポーネントでは、◯ボタンを押したら入力フォーム(input)に◯を入力(add)、✕ボタンを押したら入力フォーム(input)が空欄(remove)となる処理です。

	methods: {
		add() {
			this.$emit('input', "○");	
		},
		remove() {
			this.$emit('input', "");
		},
	},
	methods: {
		add() {
			this.$emit('input', "○");	
		},
		remove() {
			this.$emit('input', "");
		},
	},

親から渡されたデータを子で変更してはならない【$emitを使う】

先程のmethodsのコードの中に、$emitというものがありました。

Vue.jsには「親から渡されたpropsのデータを、子で変更してはならない」というルールがあります。
そこで、この$emitを使います。

$emitはイベントを親に向けて発行するのみで、実際にデータは変更していません。
データの変更は、イベントの発行を受けた親が行います。

例えば、先程のコードで+ボタンを押すと、「inputの値が+1されるイベントが起きたよー」、「だから親のほうでinputの値を+1にしといてねー」といったやりとりが行われているんですね。

button要素とinput要素にイベントを指定する

各要素にイベントを指定します。

下記のとおりにコードを書きます。

main.js
const counterHit = {
	props: ['value'],
	methods: {
		countUp() {
			this.$emit('input', this.value + 1);
		},
		countDown() {
			if ( this.value > 0 ) {
				this.$emit('input', this.value - 1);
			}
		},
	},
	template: `
		<form>
			打数
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" @click="countUp" class="btn btn-primary btn-up">+</button>
				</div>
				<input type="number" v-model="value" min="0" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" @click="countDown" class="btn btn-secondary btn-down">-</button>
				</div>
			</div>
		</form>
	`,
};

const hoopIn = {
	props: ['value'],
	methods: {
		add() {
			this.$emit('input', "○");	
		},
		remove() {
			this.$emit('input', "");
		},
	},
	template: `
		<form>
			<slot>フープイン</slot>
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" @click="add" class="btn btn-info btn-add">○</button>
				</div>
				<input type="text" v-model="value" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" @click="remove" class="btn btn-secondary btn-remove">☓</button>
				</div>
			</div>
		</form>
	`,
};

<!-- 以下は省略 -->

テンプレート内のbutton要素に、@clickでクリックイベントを指定しています。

<button type="button" @click="countUp" class="btn btn-primary btn-up">+</button>

<button type="button" @click="countDown" class="btn btn-secondary btn-down">-</button>

<button type="button" @click="add" class="btn btn-info btn-add">○</button>

<button type="button" @click="remove" class="btn btn-secondary btn-remove">☓</button>

これでボタンが押されると、各ボタンに対応したmethods(処理)が実行されます。

また、親で変更されたデータ(value)が入力フォーム(input)に表示されるように、input要素にv-model=”value”を指定しています。

<input type="number" v-model="value" min="0" disabled class="form-control">

<input type="text" v-model="value" disabled class="form-control">

親にデータを指定する

子(コンポーネント)のコードは完成したので、次は親のコードを書いていきます。

親には、各カウンターのデータを指定します。
打数のカウンターには「countGate(Goal)」という名前のデータを、フープインのカウンターには「inGate(Goal)」という名前のデータを指定します。

main.jsに、下記のとおりにコードを書きます。

main.js
<!-- 以上は省略 -->

new Vue({
	el: '.row-item',
	data() {
		return {
			countGate1: 0,
			countGate2: 0,
			countGate3: 0,
			countGate4: 0,
			countGoalA: 0,
			
			countGate5: 0,
			countGate6: 0,
			countGate7: 0,
			countGate8: 0,
			countGoalB: 0,
			
			inGate1: "",
			inGate2: "",
			inGate3: "",
			inGate4: "",
			inGoalA: "",
			
			inGate5: "",
			inGate6: "",
			inGate7: "",
			inGate8: "",
			inGoalB: "",
		};
	},
	components: {
		'counter-hit': counterHit,
		'hoop-in': hoopIn,
	},
});

new Vueのdataオプションに各データを指定しています。

	data() {
		return {
			countGate1: 0,
			countGate2: 0,
			countGate3: 0,
			countGate4: 0,
			countGoalA: 0,
			
			countGate5: 0,
			countGate6: 0,
			countGate7: 0,
			countGate8: 0,
			countGoalB: 0,
			
			inGate1: "",
			inGate2: "",
			inGate3: "",
			inGate4: "",
			inGoalA: "",
			
			inGate5: "",
			inGate6: "",
			inGate7: "",
			inGate8: "",
			inGoalB: "",
		};
	},

データ名はそれぞれ違う名前になる必要があります。
同じ名前のデータがあると「ボタンを押したのは1つなのに、同じ名前のデータを持つ複数のカウンターの値が変化する」という状態になります。

上記のコードでは、データ名の後に数字やアルファベットを入れることで、それぞれ違う名前にしています。

ちなみに、ここで指定したデータの値が、カウンターの初期値になります。
「countGate: 0,」なので打数カウンターの初期値が0、「inGate: “”,」なのでフープインカウンターの初期値は空欄になります。

親のデータを入力フォーム(input)に入力させる

では、親で指定したデータの値が、各カウンターに入力されるようにします。
index.htmlの各カウンターのHTMLタグに、v-modelを使ってデータを指定します。

index.htmlを、下記のとおりに書きます。

index.html
<!-- 以上は省略 -->

<tbody class="row-item">
	<tr>
		<th>Aさん</th>
		<td>
			<counter-hit v-model="countGate1"></counter-hit>
			<hoop-in v-model="inGate1"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate2"></counter-hit>
			<hoop-in v-model="inGate2"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate3"></counter-hit>
			<hoop-in v-model="inGate3"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate4"></counter-hit>
			<hoop-in v-model="inGate4"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGoalA"></counter-hit>
			<hoop-in v-model="inGoalA">ネットイン</hoop-in>
		</td>
		<td>総打数</td>
		<td>加算点</td>
		<td>Total</td>
	</tr>
	<tr>
		<th>Bさん</th>
		<td>
			<counter-hit v-model="countGate5"></counter-hit>
			<hoop-in v-model="inGate5"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate6"></counter-hit>
			<hoop-in v-model="inGate6"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate7"></counter-hit>
			<hoop-in v-model="inGate7"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGate8"></counter-hit>
			<hoop-in v-model="inGate8"></hoop-in>
		</td>
		<td>
			<counter-hit v-model="countGoalB"></counter-hit>
			<hoop-in v-model="inGoalB">ネットイン</hoop-in>
		</td>
		<td>総打数</td>
		<td>加算点</td>
		<td>Total</td>
	</tr>
</tbody>

<!-- 以下は省略 -->

<counter-hit>と<hoop-in>タグに、v-modelでデータを指定しています。
v-model=”データ名”で、指定したデータ名の値を入力フォームに表示することが出来ます。

最初は親のnew Vueのdataオプションで指定された値が表示されます。
その後、ボタンを押すたびに変更されたprops: [‘value’]の値が表示されるようになります。

完成コードを記載

カウンター機能を実装できたコードを、下記に記載します。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>

	<!-- Normalize CSS -->
	<link rel="stylesheet" href="normalize.css">

	<!-- Bootstrap CSS -->
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css">

	<!-- Style CSS -->
	<link rel="stylesheet" href="style.css">

	<!-- Vue.js(本番用) -->
	<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> -->

	<!-- Vue.js(開発用) -->
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
	<table class="table table-bordered table-striped">
		<thead class="thead-light">
			<tr>
				<th class="col-sm"></th>
				<th class="col-lg">ゲート1</th>
				<th class="col-lg">ゲート2</th>
				<th class="col-lg">ゲート3</th>
				<th class="col-lg">ゲート4</th>
				<th class="col-lg">ゴール</th>
				<th class="col-md">総打数</th>
				<th class="col-md">加算点</th>
				<th class="col-md">Total</th>
			</tr>
		</thead>
		<tbody class="row-item">
			<tr>
				<th>Aさん</th>
				<td>
					<counter-hit v-model="countGate1"></counter-hit>
					<hoop-in v-model="inGate1"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate2"></counter-hit>
					<hoop-in v-model="inGate2"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate3"></counter-hit>
					<hoop-in v-model="inGate3"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate4"></counter-hit>
					<hoop-in v-model="inGate4"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGoalA"></counter-hit>
					<hoop-in v-model="inGoalA">ネットイン</hoop-in>
				</td>
				<td>総打数</td>
				<td>加算点</td>
				<td>Total</td>
			</tr>
			<tr>
				<th>Bさん</th>
				<td>
					<counter-hit v-model="countGate5"></counter-hit>
					<hoop-in v-model="inGate5"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate6"></counter-hit>
					<hoop-in v-model="inGate6"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate7"></counter-hit>
					<hoop-in v-model="inGate7"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGate8"></counter-hit>
					<hoop-in v-model="inGate8"></hoop-in>
				</td>
				<td>
					<counter-hit v-model="countGoalB"></counter-hit>
					<hoop-in v-model="inGoalB">ネットイン</hoop-in>
				</td>
				<td>総打数</td>
				<td>加算点</td>
				<td>Total</td>
			</tr>
		</tbody>
	</table>

	<!-- Bootstrap JavaScript -->
	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"></script>
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>

	<!-- Main.js -->
	<script src="main.js"></script>
</body>
</html>
main.js
const counterHit = {
	props: ['value'],
	methods: {
		countUp() {
			this.$emit('input', this.value + 1);
		},
		countDown() {
			if ( this.value > 0 ) {
				this.$emit('input', this.value - 1);
			}
		},
	},
	template: `
		<form>
			打数
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" @click="countUp" class="btn btn-primary btn-up">+</button>
				</div>
				<input type="number" v-model="value" min="0" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" @click="countDown" class="btn btn-secondary btn-down">-</button>
				</div>
			</div>
		</form>
	`,
};

const hoopIn = {
	props: ['value'],
	methods: {
		add() {
			this.$emit('input', "○");	
		},
		remove() {
			this.$emit('input', "");
		},
	},
	template: `
		<form>
			<slot>フープイン</slot>
			<div class="form-group input-group">
				<div class="input-group-prepend">
					<button type="button" @click="add" class="btn btn-info btn-add">○</button>
				</div>
				<input type="text" v-model="value" disabled class="form-control">
				<div class="input-group-append">
					<button type="button" @click="remove" class="btn btn-secondary btn-remove">☓</button>
				</div>
			</div>
		</form>
	`,
};

new Vue({
	el: '.row-item',
	data() {
		return {
			countGate1: 0,
			countGate2: 0,
			countGate3: 0,
			countGate4: 0,
			countGoalA: 0,
			
			countGate5: 0,
			countGate6: 0,
			countGate7: 0,
			countGate8: 0,
			countGoalB: 0,
			
			inGate1: "",
			inGate2: "",
			inGate3: "",
			inGate4: "",
			inGoalA: "",
			
			inGate5: "",
			inGate6: "",
			inGate7: "",
			inGate8: "",
			inGoalB: "",
		};
	},
	components: {
		'counter-hit': counterHit,
		'hoop-in': hoopIn,
	},
});

style.cssに変更はありません。

上記のコードでページを表示させると、下記のようになります。( ↓ GIF動画参照)

カウンター機能の実装
カウンター機能の実装

意図通りに数値のカウンター機能を実装することが出来ました。

おわりに

Vue.jsで数値のカウンター機能を実装する方法について解説してみました。
propsやmethods、$emitを使って、機能を実装することが出来ました。

今回の記事が皆さんの参考になれば幸いです。
以上、ちゃんカマでした。

コメント

タイトルとURLをコピーしました