Let's write a piano chord visualization application - a vanilla JS beginner-friendly tutorial—part III.
In part II, we have learned and implemented the JavaScript code that generates a chord's notes. In this part, we will put together pieces written so far.
Big apologies to everyone who waited till the final part was published. I was dragged away from the writing for a little while then it should be. But here I am now with the last part
Contents:
- Minimum music theory explanation for better topic understanding
- Setup
- [HTML/CSS] Model of a piano keyboard
3.1 Complete code - [HTML/CSS] Chord root and type selection
4.1 Complete code - [JS] Chord generation
5.1 Complete code - [HTML/JS] Putting all together
[HTML/JS] Putting all together
It is time to glue JS core and HTML/CSS visuals together.
This part will be JavaScript only and touch the topic of object getters and setters. All code below has to be put in index.js file.
Render state object
Let's start with the code responsible for our application state: meaning, information of root note, and chord to show.
const renderState = {
_rootNote: "",
_chordType: "",
set rootNote(note) {
this._rootNote = note;
renderChanges({ rootNote: this.rootNote, chordType: this.chordType });
},
get rootNote() {
return this._rootNote;
},
set chordType(type) {
this._chordType = type;
renderChanges({ rootNote: this.rootNote, chordType: this.chordType });
},
get chordType() {
return this._chordType;
},
};
In the code above, we declare a variable called renderState
. The variable stores two fields: "_rootNote" and "_chordType." Each of those is defined as an empty string.
As mentioned before, setters and getters simplify how we call our application to update the CSS classes. Object field setters give us more control over assigning a value to the field.
The above code can be understood by observing that anytime we assign a value to the fields "rootNote" or "chordType," the function renderChanges
is called.
Next, we will add interactivity to our notes and chords selection buttons.
Adding on click handlers to buttons
// collect all buttons for selecting root note
const rootSelectBtns = document.querySelectorAll("#root-notes-group button");
// create onClick handler function
const onRootBtnClick = (event) => {
renderState.rootNote = event.target.dataset.note;
};
// assign onClick handler to each button
for (let i = 0; i < rootSelectBtns.length; i++) {
rootSelectBtns[i].addEventListener("click", onRootBtnClick);
}
// collect all buttons for selecting chord type
const chordTypeSelectBtns = document.querySelectorAll("#chord-types-group button");
// create onClick handler function
const onChordTypeBtnClick = (event) => {
renderState.chordType = event.target.dataset.type;
};
// assign onClick handler to each button
for (let i = 0; i < chordTypeSelectBtns.length; i++) {
chordTypeSelectBtns[i].addEventListener("click", onChordTypeBtnClick);
}
If you look closely, you will see that the second part of the code above repeats the first part but targets different buttons.
Let's review the code. First, we create the rootSelectBtns
variable, assigning the results from the function document.querySelectorAll("#root-notes-group button")
call.
The querySelectorAll
function returns a collection of HTML elements matching the CSS selector. For example, selector #root-notes-group button
matches all the buttons in div with id root-notes-group.
Next, we create the onRootBtnClick
variable and assign it a function that takes an argument called "event."
Inside the function body, we put the code that sets the "rootNote" field of the renderState object. We assign the value event.target.dataset.note
.
In the final step, we assign the function created before to every click handler of the buttons we selected via querySelectorAll.
To do this, we iterate collection from rootSelectBtns
using a for loop.
In each iteration, we assign an event listener with the addEventListener
method call to which we pass the "click" event type name and the function that must execute on each button click.
event.target.dataset.note
is the value of the HTML data-note
attribute on the root note selection button.
The rest of the code is a copy-paste of the first half with changed variable names and query selectors to match the chord type selection buttons. The logic of work stays precisely the same.
Making root note and chord type selection visible
// function to set the specific root note selection button's classes
const setActiveRootBtn = (rootNote = "") => {
for (let i = 0; i < rootSelectBtns.length; i++) {
const rootSelectBtn = rootSelectBtns[i];
if (rootSelectBtn.dataset.note === rootNote) {
// add "active" class
rootSelectBtn.classList.add("active");
} else {
// remove "active" class
rootSelectBtn.classList.remove("active");
}
}
};
// function to set the specific chord type selection button's classes
const setActiveChordTypeBtn = (chordType = "") => {
for (let i = 0; i < chordTypeSelectBtns.length; i++) {
const chordTypeSelectBtn = chordTypeSelectBtns[i];
if (chordTypeSelectBtn.dataset.type === chordType) {
// add "active" class
chordTypeSelectBtn.classList.add("active");
} else {
// remove "active" class
chordTypeSelectBtn.classList.remove("active");
}
}
};
In this part of the code, we are making our changes visible. We have two almost identical functions. Each function iterates through the collection of buttons and sets its classes to reflect the current state.
In the first function, we check if the dataset.note
value equals the rootNote
argument.
If the statement is true, we add an "active" class to the button using the HTML element's classList.add
method. When the statement returns a false value, we remove the "active" class with the classList.remove
function.
The following function realizes the same logic but for chord-type selection buttons.
Presenting the chord notes on piano keyboard
// function to set the div (piano key representation) elements "active" classes
const setActivePianoKeys = (chordNotes = []) => {
const pianoKeys = document.querySelectorAll("#piano-keyboard .key"); // get html elements of piano keys
const pianoKeyNotes = Array.prototype.map.call(
pianoKeys,
(pKey) => pKey.dataset.note
); // create the array of the piano keys notes values
const keyStartIndex = pianoKeyNotes.indexOf(chordNotes[0]); // get the key from which the chord starts (we do not set every matching key as "active")
const tmpChordNotes = [...chordNotes]; // temporary array of chord notes used when setting CSS classes
// iteration through piano key divs
for (let i = 0; i < pianoKeys.length; i++) {
const pianoKey = pianoKeys[i];
const pianoKeyNote = pianoKeyNotes[i];
// set key to active if the piano note is in chord notes and it's index is greater than or equal starting index
if (tmpChordNotes.indexOf(pianoKeyNote) > -1 && i >= keyStartIndex) {
pianoKey.classList.add("active");
tmpChordNotes.shift(); // remove the piano note that was already rendered
} else {
// remove the "active" class
pianoKey.classList.remove("active");
}
}
};
In this part of the code, we are making our changes visible. We have two almost identical functions. Each function iterates through the collection of buttons and sets its classes to reflect the current state.
In the first function, we check if the dataset.note
value equals the rootNote
argument.
If the statement is true, we add an "active" class to the button using the HTML element's classList.add
method. When the statement returns a false value, we remove the "active" class with the classList.remove
function.
The following function realizes the same logic but for chord-type selection buttons.
Now, we create the setActivePianoKeys
method. We'll use it to make our chord notes visible on the piano. I'll now describe each step we're taking here.
The pianoKeys
variable stores a collection of HTML elements with the CSS class key
placed in a div with the id piano-keyboard.
The pianoKeyNotes
variable is an array of strings. The value of the array is the result of the pianoKeys
map method execution. We have passed a method to the map function that returns the value of every pianoKeys
element data-note
attribute. This way, we know precisely in what order our piano keyboard keys occur.
The keyStartIndex
variable stores the array index of the first chord note. It's our starting point for marking active keys.
The tmpChordNotes
is an array of copied chord notes later used to indicate when to stop setting active piano keys.
Next, with a for-loop, we iterate through pianoKeyNotes
to mark the correct keys active. For every iteration, we check if the current pianoKeyNote
is included in tmpChordNotes
and if the index i' is greater or equal to
keyStartIndex.If the statement is invalid, we clear the CSS
activeclass of
pianoKey.For a valid statement, we add the CSS 'active
class of pianoKey
and remove the tmpChordNotes
array's first element. This way, we clear every unnecessary key and mark only those containing a chord note. We are setting piano keys active until the tmpChordNotes
array still has elements.
Rendering changes
const renderChanges = ({ rootNote, chordType }) => {
if (rootNote) {
setActiveRootBtn(rootNote);
}
if (chordType) {
setActiveChordTypeBtn(chordType);
}
if (rootNote && chordType) {
const chordNotes = getChord(rootNote, chordType);
setActivePianoKeys(chordNotes);
}
};
renderChanges
is simply a function we call to render changes in button and piano key selection. Based on the arguments we have passed, we render our changes. We call the setActiveRootBtn
method when rootNote
is not empty. We call the setActiveChordTypeBtn
method when chordType
is not empty. If rootNote
and chordTypes
are selected, we get the selected chord notes and pass them to the setActivePianoKeys
method to show our changes on the piano keyboard.
dataset attributes in HTML
For our application to work, we made the last change, which is adding the dataset attributes. To piano key elements and chords' root note select buttons, we added data-note
HTML attributes with corresponding values. To chords' type select buttons, we added data-type
attributes with corresponding values.
HTML code should looks as following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf8" />
<title>Piano chord visualizer</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.2/font/bootstrap-icons.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="piano-keyboard.css" />
</head>
<body class="d-flex flex-column h-100 min-vh-100">
<body class="d-flex flex-column h-100 min-vh-100">
<main class="bg-secondary text-white flex-grow-1">
<div class="container">
<div class="row mt-3">
<div class="col-md-12">
<h1>Piano chord visualizer</h1>
</div>
</div>
<div class="row mt-5">
<div class="col-md-6">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<label for="root-notes-group" class="form-label"
><i class="bi bi-music-note"></i> Root note</label
>
<div
class="btn-toolbar mb-3"
role="toolbar"
aria-label="Toolbar with button groups"
>
<div
id="root-notes-group"
class="btn-group me-2"
role="group"
aria-label="Root notes group"
>
<button type="button" class="btn btn-outline-light" data-note="c">
C
</button>
<button type="button" class="btn btn-outline-light" data-note="c#">
C#
</button>
<button type="button" class="btn btn-outline-light" data-note="d">
D
</button>
<button type="button" class="btn btn-outline-light" data-note="d#">
D#
</button>
<button type="button" class="btn btn-outline-light" data-note="e">
E
</button>
<button type="button" class="btn btn-outline-light" data-note="f">
F
</button>
<button type="button" class="btn btn-outline-light" data-note="f#">
F#
</button>
<button type="button" class="btn btn-outline-light" data-note="g">
G
</button>
<button type="button" class="btn btn-outline-light" data-note="g#">
G#
</button>
<button type="button" class="btn btn-outline-light" data-note="a">
A
</button>
<button type="button" class="btn btn-outline-light" data-note="a#">
A#
</button>
<button type="button" class="btn btn-outline-light" data-note="b">
B
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="chord-types-group" class="form-label"
><i class="bi bi-music-note-beamed"></i> Chord type</label
>
<div
class="btn-toolbar mb-3"
role="toolbar2"
aria-label="Toolbar with button groups"
>
<div
id="chord-types-group"
class="btn-group me-2"
role="group2"
aria-label="Chord types group"
>
<button type="button" class="btn btn-outline-light" data-type="maj">
Major
</button>
<button type="button" class="btn btn-outline-light" data-type="min">
Minor
</button>
<button type="button" class="btn btn-outline-light" data-type="dim">
Diminished
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 d-flex justify-content-center flex-wrap">
<div id="piano-keyboard">
<div class="key key-natural" data-note="c"></div>
<div class="key key-sharp" data-note="c#"></div>
<div class="key key-natural" data-note="d"></div>
<div class="key key-sharp" data-note="d#"></div>
<div class="key key-natural" data-note="e"></div>
<div class="key key-natural" data-note="f"></div>
<div class="key key-sharp" data-note="f#"></div>
<div class="key key-natural" data-note="g"></div>
<div class="key key-sharp" data-note="g#"></div>
<div class="key key-natural" data-note="a"></div>
<div class="key key-sharp" data-note="a#"></div>
<div class="key key-natural" data-note="b"></div>
<div class="key key-natural" data-note="c"></div>
<div class="key key-sharp" data-note="c#"></div>
<div class="key key-natural" data-note="d"></div>
<div class="key key-sharp" data-note="d#"></div>
<div class="key key-natural" data-note="e"></div>
<div class="key key-natural" data-note="f"></div>
<div class="key key-sharp" data-note="f#"></div>
<div class="key key-natural" data-note="g"></div>
<div class="key key-sharp" data-note="g#"></div>
<div class="key key-natural" data-note="a"></div>
<div class="key key-sharp" data-note="a#"></div>
<div class="key key-natural" data-note="b"></div>
</div>
</div>
</div>
</div>
</main>
</body>
</body>
</html>
Result
You can check the live version on my CodePen here. Feel free to play around, add or rework some functions.
Sneak-peak of results below:
Thank you all for reading and for the all comments
In case of any questions I'm happy to help, discuss or assist.
Section knowledge base
Here you will find links to helpful articles on the topic I don't cover thoroughly in this section.
JavaScript Object Accessors by w3schools.com - W3School's article on JavaScript Accessors (Getters and Setters)
HTML DOM Document querySelectorAll() by w3schools.com - W3School's article on querySelectAll function
HTMLElement.dataset by developer.mozilla.org - Mozilla article on HTML datasets
HTML DOM Element classList by w3schools.com - W3School's article on classList property
Thanks for sharing this tutorial! It looks like a great resource for beginners interested in building a piano chord visualization application using vanilla JS. I appreciate the clear explanations and step-by-step approach. Looking forward to diving into it and exploring more. Keep up the good work!