今回は、ドットインストールのJavaScript講座で紹介されているスロットマシーンをVue.jsで作ってみました!
本家はこちら
(twitter上では機能追加したversionを紹介しましたが、説明がしづらいので、機能はドットインストールで紹介されているものと同じにしました。)
今回は初めてコンポーネントやテンプレートを使ったので結構苦戦しました(^^;)
スロットマシーンを実装する上で苦労したのが以下の2つでした。
- SPINボタンを押した時にスロットをスタートさせる方法
- STOPボタンを押した時にそれぞれのスロットを止めて、メイン側で押された枚数をカウントする方法
JavaScriptで書く場合のソースコード はドットインストールをみてもらいたいですが、全てPanelクラスのなかに記述してあって、特に難しいこともなく実装できています。
Vue.jsで実装する場合はPanelクラスをコンポーネントとして実装するわけですが、メイン側からコンポーネントで定義されているメソッドを実行したり、コンポーネント側で発生したイベントをメイン側で感知するということが必要で、ここの処理の知識がなくて苦労しました。
自分一人では解決できなかったですが、teratailで質問して、親切に回答してくれたエンジニアの方がいたので、なんとか作ることができました。
それでは、私が苦戦した処理のところを説明してみますね!ご参考にしてください!
SPINボタンを押した時にスロットをスタートさせる方法
HTML <div id="app"> <main> <slot-component ref="component1" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component2" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component3" v-on:decrement="decrementPanel"></slot-component> </main> <div class="spin" v-on:click="spin()" v-bind:class={inactive:isRunning}>SPIN</div> </div>
HTML側のソースコード はこの部分になりますね。
v-on:click=”spin()” でメイン側のメソッドで定義されているspin()を実行するようになっていますが、この時にコンポーネントで定義されているspin()を実行しないといけません。
メイン el: '#app', ~~~~~~~~~~~~~~~~~~~~~~~ methods: { spin() { if (this.isRunning) { return; } this.isRunning = true; this.$refs.component1.activate(); this.$refs.component2.activate(); this.$refs.component3.activate(); this.$refs.component1.spin(); this.$refs.component2.spin(); this.$refs.component3.spin(); },
これがメイン側のソースコード です。
コンポーネント var slotComponent = Vue.extend({ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ methods: { ~~~~~~~~~~~~~~~~~~~~~~~~~~~ spin() { this.timeoutId = setTimeout(() => { this.getRandomImage(); this.spin(); }, 10); },
こちらがコンポーネント側のソースコードですね。
このソースコード 通りなんですが、メイン側からコンポーネントのメソッドにアクセスするには、HTML側でコンポーネントにref属性を使って、リファんレンスID(今回だと、component1,component2,component3)を割り当てることで、コンポーネントに定義されているメソッドにアクセスすることが出来ます。
公式のドキュメントはこちらです。
知らないとできないですし、ドットインストールや私が受講したテックアカデミーの資料にもこんな方法は書いてありませんでした。
公式ドキュメントは1回見たんですが、よくわからず。。。
teratailで質問することで解決しました(^^;
STOPボタンを押した時にそれぞれのスロットを止めて、メイン側で押された枚数をカウントする方法
これは、コンポーネント側で発生したイベントをメイン側で感知して処理するというのが必要になってきます。
HTML <main> <slot-component ref="component1" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component2" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component3" v-on:decrement="decrementPanel"></slot-component> </main>
HTML側のソースコード はこの部分ですね。
“v-on:decrement=”decrementPanel”
のところがポイントになりますね。
メイン decrementPanel: function () { this.panelLeft--; if (this.panelLeft == 0) { this.isRunning = false; this.panelLeft = 3; } },
こちらがメイン側ののソースコード です。この”decrementPanel”を実行して、クリックされたパネルの枚数を数えてます。
コンポーネント var slotComponent = Vue.extend({ data() { }, template: `<section class="panel"> <img v-bind:src= image v-bind:class={inactive:isUnmatched}> <div class="stop" v-on:click="stop()" v-bind:class={inactive:isSelected}>STOP</div> </section>`, methods: { stop() { if (this.isSelected) { return; } this.isSelected = true; clearTimeout(this.timeoutId); this.$emit('decrement'); }, }, });
こちらがコンポーネント側のソースコード です。
それぞれのコンポーネントには、v-on:click=”stop()”でstopメソッドを呼び出すことができるようになっています。
ポイントは、stopメソッドのなかに書いてある、”this.$emit.(‘decrement’)”ですね。
$emitに関する公式ドキュメントはこちらです。
プログラム的には、こんな順番で動いてます。
- STOPボタンを押すと、v-on:click=”stop” でstopメソッドが呼ばれる
- stopメソッドのなかにある”this.$emit(“decrement”)”で v-on:decrement=”decrementPanel”のイベントが発生する
- メイン側のdecrementPanelメソッドが呼ばれて、クリックされたパネル枚数をカウントする
どうでしたか?わかればなんてことない処理かもしれませんが、やったことないと、そんなこと出来るんだな〜って感じで難しいですよね(^^;
ググるにしてもなんてググればいいのかよくわからなかったりするので、プログラミングって難しいな〜って思っちゃったりします(^^;
それでは最後に全体のソースコード を載せておくので、参考にしてください!
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SlotMachine</title> <style> body { background: #bdc3c7; font-size: 16px; font-weight: bold; font-family: Arial, Helvetica, sans-serif; } main { width: 300px; background: #ecf0f1; padding: 20px; border: 4px solid #fff; border-radius: 12px; ; margin: 16px auto; display: flex; justify-content: space-between; } .panel img { width: 90px; height: 110px; margin-bottom: 4px; } .stop { cursor: pointer; width: 90px; height: 32px; background: #ef454a; box-shadow: 0 4px 0 #d1483e; border-radius: 16px; line-height: 32px; text-align: center; font-size: 14px; color: #fff; user-select: none; } .spin { cursor: pointer; width: 280px; height: 36px; background: #3498db; box-shadow: 0 4px 0 #2880b9; border-radius: 18px; line-height: 36px; text-align: center; font-size: 14px; color: #fff; user-select: none; margin: 0 auto; } .unmatched { opacity: 0.5; } .inactive { opacity: 0.5; } </style> </head> <body> <div id="app"> <main> <slot-component ref="component1" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component2" v-on:decrement="decrementPanel"></slot-component> <slot-component ref="component3" v-on:decrement="decrementPanel"></slot-component> </main> <div class="spin" v-on:click="spin()" v-bind:class={inactive:isRunning}>SPIN</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var slotComponent = Vue.extend({ data() { return { images: [ 'img/seven.png', 'img/bell.png', 'img/cherry.png', ], image: 'img/seven.png', timeoutId: '', isSelected: true, isUnmatched: false, } }, template: `<section class="panel"> <img v-bind:src= image v-bind:class={inactive:isUnmatched}> <div class="stop" v-on:click="stop()" v-bind:class={inactive:isSelected}>STOP</div> </section>`, methods: { getRandomImage() { this.image = this.images[Math.floor(Math.random() * this.images.length)]; }, spin() { this.timeoutId = setTimeout(() => { this.getRandomImage(); this.spin(); }, 10); }, stop() { if (this.isSelected) { return; } this.isSelected = true; clearTimeout(this.timeoutId); this.$emit('decrement'); }, activate() { this.isSelected = false; this.isUnmatched = false; } }, }); var vue = new Vue({ el: '#app', data() { return { panelLeft: 3, isRunning: false, isNotAllCorrect: true, } }, components: { 'slot-component': slotComponent }, methods: { spin() { if (this.isRunning) { return; } this.isRunning = true; this.$refs.component1.activate(); this.$refs.component2.activate(); this.$refs.component3.activate(); this.$refs.component1.spin(); this.$refs.component2.spin(); this.$refs.component3.spin(); }, decrementPanel: function () { this.panelLeft--; if (this.panelLeft == 0) { this.isRunning = false; this.panelLeft = 3; if (this.$refs.component1.image !== this.$refs.component2.image && this.$refs.component1.image !== this.$refs.component3.image) { this.$refs.component1.isUnmatched = true; } if (this.$refs.component2.image !== this.$refs.component1.image && this.$refs.component2.image !== this.$refs.component3.image) { this.$refs.component2.isUnmatched = true; } if (this.$refs.component3.image !== this.$refs.component1.image && this.$refs.component3.image !== this.$refs.component2.image) { this.$refs.component3.isUnmatched = true; } if (this.$refs.component1.image == this.$refs.component2.image && this.$refs.component1.image == this.$refs.component3.image) { } } }, } }); </script> </body> </html>