Codementor Events

Let's write a piano chord visualization application - a vanilla JS beginner-friendly tutorial—part III.

Published Apr 15, 2024
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:

  1. Minimum music theory explanation for better topic understanding
  2. Setup
  3. [HTML/CSS] Model of a piano keyboard
    3.1 Complete code
  4. [HTML/CSS] Chord root and type selection
    4.1 Complete code
  5. [JS] Chord generation
    5.1 Complete code
  6. [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 tokeyStartIndex.If the statement is invalid, we clear the CSSactiveclass ofpianoKey.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:
chords_01.png
chords_02.png
chords_03.png
chords_04.png

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


Discover and read more posts from Andrzej Gorgoń
get started
post commentsBe the first to share your opinion
Show more replies