A New Vue On JavaScript30 - 01 JavaScript Drum Kit

This article is part of the A New Vue On JavaScript30 series that explores re-implementing JavaScript30 projects using Vue. Before reading, its a good idea to have:

Key Vue Concepts

The following Vue concepts are discussed in this article:

  • v-for directive
  • Class binding
  • Event binding

01 - JavaScript Drum Kit

The first of the JavaScript30 projects walks through building a drum kit that binds key presses to playing drum sounds. This one is a lot fun and I ended up creating two different Vue implementations:

Lets take a look at the first version I created.

Vue Version 1

For the first approach, I decided to simply insert the code from the original project into their corresponding Vue locations:

  • The HTML section fit inside the root <div>
  • The functions went into the methods section
  • The JavaScript that executed on page load was placed into the mounted function
  • The data, computed, and watch sections were not needed so they were removed
index-VUE-V1.htmllink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Drum Kit</title>
<link rel="stylesheet" href="style.css">
<!-- Use Vue from a CDN -->
<script src="https://unpkg.com/vue"></script>
</head>
<body>

<!-- Vue Root DOM Element -->
<div id="app">
<div class="keys">
<div data-key="65" class="key">
<kbd>A</kbd>
<span class="sound">clap</span>
</div>
<div data-key="83" class="key">
<kbd>S</kbd>
<span class="sound">hihat</span>
</div>
<div data-key="68" class="key">
<kbd>D</kbd>
<span class="sound">kick</span>
</div>
<div data-key="70" class="key">
<kbd>F</kbd>
<span class="sound">openhat</span>
</div>
<div data-key="71" class="key">
<kbd>G</kbd>
<span class="sound">boom</span>
</div>
<div data-key="72" class="key">
<kbd>H</kbd>
<span class="sound">ride</span>
</div>
<div data-key="74" class="key">
<kbd>J</kbd>
<span class="sound">snare</span>
</div>
<div data-key="75" class="key">
<kbd>K</kbd>
<span class="sound">tom</span>
</div>
<div data-key="76" class="key">
<kbd>L</kbd>
<span class="sound">tink</span>
</div>
</div>

<audio data-key="65" src="sounds/clap.wav"></audio>
<audio data-key="83" src="sounds/hihat.wav"></audio>
<audio data-key="68" src="sounds/kick.wav"></audio>
<audio data-key="70" src="sounds/openhat.wav"></audio>
<audio data-key="71" src="sounds/boom.wav"></audio>
<audio data-key="72" src="sounds/ride.wav"></audio>
<audio data-key="74" src="sounds/snare.wav"></audio>
<audio data-key="75" src="sounds/tom.wav"></audio>
<audio data-key="76" src="sounds/tink.wav"></audio>
</div>

<!-- Vue Instance Section -->
<script>
var app = new Vue({
el: '#app',
methods: {
removeTransition: function (e) {
if (e.propertyName !== 'transform') { return }
e.target.classList.remove('playing')
},
playSound: function (e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`)
const key = document.querySelector(`div[data-key="${e.keyCode}"]`)
if (!audio) { return }

key.classList.add('playing')
audio.currentTime = 0
audio.play()
}
},
mounted: function () {
const keys = Array.from(document.querySelectorAll('.key'))
keys.forEach(key => key.addEventListener('transitionend', this.removeTransition))
window.addEventListener('keydown', this.playSound)
}
})
</script>

</body>
</html>

While this version works and was easy to make, I didn’t feel as if it took advantage of Vue’s features so I decided to iterate on it.

Vue Version 2

When looking at the HTML section of the JavaScript30 project, you can see a repetitive pattern for each key: keyCode, keyName, and soundName. By extracting those parts into a data structure, we can then make use of Vue’s v-for directive to loop it and create the same HTML. Lets first look at the data structure, an array of objects where each object represents a key:

index-VUE-V2.htmllink
28
29
30
31
32
33
34
35
36
37
38
39
40
data: {
keys: [
{keyCode: 65, keyName: "A", soundName: "clap", audio: new Audio("sounds/clap.wav"), isPlaying: false},
{keyCode: 83, keyName: "S", soundName: "hihat", audio: new Audio("sounds/hihat.wav"), isPlaying: false},
{keyCode: 68, keyName: "D", soundName: "kick", audio: new Audio("sounds/kick.wav"), isPlaying: false},
{keyCode: 70, keyName: "F", soundName: "openhat", audio: new Audio("sounds/openhat.wav"), isPlaying: false},
{keyCode: 71, keyName: "G", soundName: "boom", audio: new Audio("sounds/boom.wav"), isPlaying: false},
{keyCode: 72, keyName: "H", soundName: "ride", audio: new Audio("sounds/ride.wav"), isPlaying: false},
{keyCode: 74, keyName: "J", soundName: "snare", audio: new Audio("sounds/snare.wav"), isPlaying: false},
{keyCode: 75, keyName: "K", soundName: "tom", audio: new Audio("sounds/tom.wav"), isPlaying: false},
{keyCode: 76, keyName: "L", soundName: "tink", audio: new Audio("sounds/tink.wav"), isPlaying: false},
]
},

In addition to including the keyCode, keyName, and soundName, I also added an HTMLAudioElement for playing the audio and an isPlaying flag. This allows us to remove the <audio> elements and simplify the logic for adding and removing the playing class which gets applied to the key div elements. If you are new to Vue, this next section may look a bit complex but I’ll do my best to break it down.

index-VUE-V2.htmllink
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<div class="keys">
<div
v-for="key in keys"
:key="key.keyCode"
:class="[key.isPlaying ? 'playing' : '', 'key']"
@transitionend="removeTransition($event, key)"
>
<kbd>{{ key.keyName }}</kbd>
<span class="sound">{{ key.soundName }}</span>
</div>
</div>
</div>

As you can see the using the v-for directive has shortened the HTML section significantly. Now we are able to loop the keys array, placing its data values into their corresponding spots. Vue recommends setting the :key attribute when using v-for and even though its not necessary for this code I went ahead and did it anyway. For more information on the :key attribute you can read about it here.

On the next line down I’m binding classes to the <div> element. This will always apply the key class, but will only apply the playing class when key.isPlaying is truthy.

Lastly in the loop, we set the transitionend event to call the removeTransition() method passing the $event and key objects. Setting the transitionend event this way allows me to remove querySelectorAll call and the addEventListener loop from the mounted function.

Now all that is left are some small changes to the removeTransition and playSound methods control the key.isPlaying flag and to play the audio from the keys array.

Putting It All Together

index-VUE-V2.htmllink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Drum Kit</title>
<link rel="stylesheet" href="style.css">
<script src="https://unpkg.com/vue"></script>
</head>
<body>

<div id="app">
<div class="keys">
<div
v-for="key in keys"
:key="key.keyCode"
:class="[key.isPlaying ? 'playing' : '', 'key']"
@transitionend="removeTransition($event, key)"
>
<kbd>{{ key.keyName }}</kbd>
<span class="sound">{{ key.soundName }}</span>
</div>
</div>
</div>

<script>
var app = new Vue({
el: '#app',
data: {
keys: [
{keyCode: 65, keyName: "A", soundName: "clap", audio: new Audio("sounds/clap.wav"), isPlaying: false},
{keyCode: 83, keyName: "S", soundName: "hihat", audio: new Audio("sounds/hihat.wav"), isPlaying: false},
{keyCode: 68, keyName: "D", soundName: "kick", audio: new Audio("sounds/kick.wav"), isPlaying: false},
{keyCode: 70, keyName: "F", soundName: "openhat", audio: new Audio("sounds/openhat.wav"), isPlaying: false},
{keyCode: 71, keyName: "G", soundName: "boom", audio: new Audio("sounds/boom.wav"), isPlaying: false},
{keyCode: 72, keyName: "H", soundName: "ride", audio: new Audio("sounds/ride.wav"), isPlaying: false},
{keyCode: 74, keyName: "J", soundName: "snare", audio: new Audio("sounds/snare.wav"), isPlaying: false},
{keyCode: 75, keyName: "K", soundName: "tom", audio: new Audio("sounds/tom.wav"), isPlaying: false},
{keyCode: 76, keyName: "L", soundName: "tink", audio: new Audio("sounds/tink.wav"), isPlaying: false},
]
},
methods: {
removeTransition: function (e, key) {
if (e.propertyName !== 'transform') { return }
key.isPlaying = false
},
playSound: function (e) {
const key = this.keys.find( key => { return key.keyCode === e.keyCode })
if (!key) { return }

key.audio.currentTime = 0
key.isPlaying = true
key.audio.play()
}
},
mounted: function () {
window.addEventListener('keydown', this.playSound)
}
})
</script>

</body>
</html>

By taking advantage of Vue’s features I was able to make some nice improvements to the code. Looping the data structure instead of the repetitive HTML makes the code easier to maintain and expand in the future. New keys can be added by simply adding a new entry into the keys array. If the HTML needs to change, it can be updated in one spot instead of each of the nine key <div> elements. Some additional improvements were:

  • Removal of all direct JavaScript DOM queries & manipulations
  • Removal of all data attributes

I hope you enjoyed this article, feel free to message me with any questions, comments, or corrections. All code presented within this series is available in my fork of the official JavaScript30 GitHub repository which is located at:

Up Next

Next in the A New Vue On JavaScript30 series is A New Vue On JavaScript30 - 02 JS and CSS Clock.

Share