facebook youtube pinterest twitter reddit whatsapp instagram

[OOP] Music List Application With JavaScript

I recently cover creating objects in JavaScript, and as you might have been aware, there are two ways to do this, the prototypal-based approach in ES5, and the newly introduced class-based approach in ES6.

You can go through them if you like, but I'll be using the Class-based approach for this guide as it's simpler and not that different from the other method.

With that knowledge, we would create a music list application, where you can:

  • Add a music list; Artiste name and song title
  • You can delete an individual music list item
  • You can clear all the music list
  • Prevents adding empty details
  • and persist the music list even after closing or reloading the application

The pre-requisite for this guide is as follows (Skip if you know all this):

HTML Structure of the Music List App

Let’s get started, here is what my HTML structure looks like:


<div class="content-wrapper text-center" id="container">
  <div class="content-wrapper text-center">
    <div class="container-fluid">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-md-8">
            <div class="card bg-light">

              <div class="card-body">
                <h2 class="card-header">Music List Application</h2>
              </div>
              <div id ="form-container" class="container-fluid">
                <div class="row justify-content-lg-start">

                  <!-- Form Input -->

                  <form id="musiclist-form" class="form-group m-auto">
                    <label class="w-300" for="artiste-name"></label>
                    <input autofocus class="form-control " id="artiste-name" placeholder="Enter Artiste Name"
                           type="text" value="">

                    <label class="w-300" for="song-name"></label>
                    <input autofocus class="form-control " id="song-name" placeholder="Enter Song Name"
                           type="text" value="">

                    <!-- Submit Button -->

                    <div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
                      <input class="btn mt-10" id="add-music" type="submit" value="Submit">
                    </div>

                  </form>

                  <!-- Table To Hold The Music List -->

                  <!-- Inner bordered table -->
                  <table class="table table-inner-bordered">
                    <thead>
                    <tr>
                      <th>#</th>
                      <th>Artiste Name</th>
                      <th>Song Title</th>
                      <th></th>
                    </tr>
                    </thead>
                    <tbody id="music-list">
                    <!-- We would dynamically insert the tbody row data -->
                    </tbody>
                  </table>

                  <!-- Clear Button -->

                  <div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
                    <input class="btn mt-10" id="clear-all" type="button" value="Clear All">
                  </div>

                </div>
              </div>

            </div>
          </div>
        </div>
      </div>
    </div>

  </div>
</div>


The above structure is within the body element, and besides that, I am using Halfmoon CSS Framework for the styling, here is how it looks on the page:

1. HTML Structure of Music List Application

Creating The Classes

To kick off the project, we would create two classes, one would contain the music information, and the other would be for constructing the user interface like for adding items to the list, showing errors, and anything that pertains to the UI:


// Code Template or Class for the Music Object
class Musiclist_Info {
    constructor(name, song) { // the constructpr
        this.name = name;
        this.song = song;
    }
}

// Code Template or Class for our user Interface
class Musiclist_UI {
}


I love prefixing my class with the name of the project, which is why I have Musiclist_info for collecting the user input, and Musiclist_UI for creating the data, prefixing keeps things organized.

Adding Event Listeners

The next I am going to do is add some event listeners, and instantiate an object from the Musiclist_Info class:

Note: I am adding this below the Class, and not within the class.


/*
-----------------------
  Start Event Listener
----------------------
*/
//  Event Listeners To Actually Listen For Submit Events Type;)
document.getElementById('musiclist-form').addEventListener('submit', function(e) {
    // Get values entered into the input box
    const artisteName = document.getElementById('artiste-name').value;
    const songName = document.getElementById('song-name').value;

    e.preventDefault() // Prevent the default action of the submit button

    // Instantiate Musiclist_Info
    const info = new Musiclist_Info(artisteName , songName);

});
/*
-----------------------
  End Event Listener
----------------------
*/


So, the document.getElementById is used to select the form element that houses the input box, and the submit button.

We then added an event listener in our handler that listens to the submit event type and once a submit has been captured, the event listener passes the logic of how it should respond to the function which we then started by preventing the default behavior of the submit button, and lastly, we instantiated a new object by passing whatever the user types into the input box.

Whatever the user enters into the input box would be in the object, here is an illustration, try entering the following and examine your console.log:

 // Instantiate Musiclist_Info
const info = new Musiclist_Info(artisteName , songName);
console.log(info);

2. checking if data is passed to instance of

Cool, you can remove the console.log(info), we can see that whatever data that is typed in the input box are values of the properties in the object instance.

Adding Data To The Music Table

The next thing we would be doing is appending the data to our music table, this would be done in the Musiclist_UI class, we can do the following:


class Musiclist_UI {
    addMusicToList(info) {
        const list = document.getElementById('music-list');
        
        // Create tr element
        const row = document.createElement('tr');
       
        // We check if the td element is available, if it is, we increment the first td by one
        if(list.firstElementChild) {
            let test = document.getElementById('music-list').querySelectorAll('tr');
            let i = test.length + 1;

            // Insert cols
            row.innerHTML = `
          <td>${i}</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);

            // if td element isn't available, we create our first td
        } else {
            // Insert cols
            row.innerHTML = `
          <td>1</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);
        }
    }
}


Haha, I know this is damn confusing, ama break it down, but before I do, here is what the output would look like:

3. Output of the music list application with populated data with incremented number

Incrementing the number is a bit challenging, but not that difficult, so, here is how I am able to do that in the code:

I started by creating a method where we would build the logic of adding the data to our UI:


addMusicToList(info) {...}


In that method, I get the tbody element:


const list = document.getElementById('music-list');


It would get all this for us:


<tbody id="music-list">
    <!-- We would dynamically insert the tbody row data -->
</tbody>


We are getting the above element so we can dynamically insert the row data there...moving on...

We created tr element:


// Create tr element
const row = document.createElement('tr');


It would create: <tr></tr>and store it in the constant row, so, we gonna use that later.

Here is where things get a bit interesting, we need a way to add the number to the lefter-most (the # column) part of the table like you saw in the screenshot, to do that, we need to first check if there is any td data within our tbody element, the list.firstElementChild would return td element if there is one, so, if td element is actually created, we get the length of how many they are. and we increment it by 1, we then insert the data into the columns:


 if(list.firstElementChild) {
let itemLength = document.getElementById('music-list').querySelectorAll('tr');
let i = itemLength.length + 1;

// Insert cols
row.innerHTML = `
<td>${i}</td>
<td>${info.name}</td>
<td>${info.song}</td>
<td><button class="btn btn-danger" type="button">Delete</button></td>`;
list.appendChild(row);
}


If the td data isn't available, we create one ourselves:


 // if td element isn't available, we create our first td
} else {
// Insert cols
row.innerHTML = `
<td>1</td>
<td>${info.name}</td>
<td>${info.song}</td>
<td><button class="btn btn-danger" type="button">Delete</button></td>`;
list.appendChild(row);
}


To make use of this, you need to instantiate the Musiclist_UI class, and call the method by passing in the info (which contains the user input):


const addNewMusic = new Musiclist_UI();

addNewMusic.addMusicToList(info);


You might wonder why we are not passing the info data to the Musiclist_UI directly, we are not doing that because we are not using a constructor method, so, you need to directly call the function from the object you instantiated.

Lastly, let's clear the input box, whenever user hits the submit button:


// Clear the input box
document.getElementById('artiste-name').value = '';
document.getElementById('song-name').value = '';


So, all together, we have:


// Code Template or Class for the Music Object
class Musiclist_Info {
    constructor(name, song) { // the constructpr
        this.name = name;
        this.song = song;
    }
}

// Code Template or Class for our user Interface
class Musiclist_UI {
    addMusicToList(info) {
        const list = document.getElementById('music-list');
        // Create tr element
        const row = document.createElement('tr');

        // We check if the td element is available, if it is, we increment the first td by one
        if(list.firstElementChild) {
            let itemLength = document.getElementById('music-list').querySelectorAll('tr');
            let i = itemLength.length + 1;

            // Insert cols
            row.innerHTML = `
          <td>${i}</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);

            // if td element isn't available, we create our first td
        } else {
            // Insert cols
            row.innerHTML = `
          <td>1</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);
        }

    }
}

/*
-----------------------
  Start Event Listener
----------------------
*/
//  Event Listeners To Actually Listen For Events ;)
document.getElementById('musiclist-form').addEventListener('submit', function(e) {
    // Get values entered into the input box
    const artisteName = document.getElementById('artiste-name').value;
    const songName = document.getElementById('song-name').value;

    e.preventDefault() // Prevent the default action of the submit button

    // Instantiate Musiclist_Info
    const info = new Musiclist_Info(artisteName, songName);

    const addNewMusic = new Musiclist_UI();

    addNewMusic.addMusicToList(info);
    
    // Clear Input Box
    document.getElementById('artiste-name').value = '';
    document.getElementById('song-name').value = '';

});
/*
-----------------------
  End Event Listener
----------------------
*/


Here is a visual illustration that shows how that works:

Add music to the musiclist application

Cool.

Delete Method

There should be a way our user can delete a music item, luckily for us, we already have a delete button appended to our table, so, first, let's create another event listener for the delete button:


// Event Listener for delete
document.getElementById('music-list').addEventListener('click', function(e){

    
    e.preventDefault();
});


If you recall, the music-list is the tbody element, so, the event listener would listen to any click event, which would obviously be on the delete button,, so, let's create a method in our Musiclist_UI class for that, just append it below the addMusicToList() method:


class Musiclist_UI {
    addMusicToList(info) {
...........
..........
    }
// Add it below like so:
    deletefromMusicList(target) {
        if(target.classList.contains('btn-danger')) {
            target.parentElement.parentElement.remove();
        }
    }
}


Now, back in the event listener function, I would instantiate a new MusicList_UI, and pass in e.target:


// Event Listener for delete
document.getElementById('music-list').addEventListener('click', function(e){

    const deleteMusic = new Musiclist_UI();

    deleteMusic.deletefromMusicList(e.target);

    e.preventDefault();
});


e.target returns the button clicked e.g: <button class="btn btn-danger" type="button">Delete</button>

In the method we created in the UI class, we find the class that contains btn-danger, we then remove its parentElement of parentElement which is obviously the tr element, here is an illustration of how that works:

5. delete music from the musicList App

Cool, if you ask me ;) but we still have a couple of stuff to iron out...

Validation

The crazy thing about what we've been doing so far is that we can enter empty data just fine, I mean look at this:

6. Empty data in musicList app

OMG! Let's do some validation to prevent our users from entering empty data, but first let's add some CSS to make it more user friendly, and a bit attention grabbing, you propbaly know how to include CSS in your head tag, if you don't know, view the page source of this website, and you'll see how it's done, so, here is my styles:


.err-anim {
    -webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
    animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}

/* ----------------------------------------------
 * Generated by Animista on 2020-12-13 6:23:50
 * Licensed under FreeBSD License.
 * See http://animista.net/license for more info.
 * w: http://animista.net, t: @cssanimista
 * ---------------------------------------------- */

/**
 * ----------------------------------------
 * animation shake-horizontal
 * ----------------------------------------
 */
@-webkit-keyframes shake-horizontal {
    0%,
    100% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }
    10%,
    30%,
    50%,
    70% {
        -webkit-transform: translateX(-10px);
        transform: translateX(-10px);
    }
    20%,
    40%,
    60% {
        -webkit-transform: translateX(10px);
        transform: translateX(10px);
    }
    80% {
        -webkit-transform: translateX(8px);
        transform: translateX(8px);
    }
    90% {
        -webkit-transform: translateX(-8px);
        transform: translateX(-8px);
    }
}
@keyframes shake-horizontal {
    0%,
    100% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }
    10%,
    30%,
    50%,
    70% {
        -webkit-transform: translateX(-10px);
        transform: translateX(-10px);
    }
    20%,
    40%,
    60% {
        -webkit-transform: translateX(10px);
        transform: translateX(10px);
    }
    80% {
        -webkit-transform: translateX(8px);
        transform: translateX(8px);
    }
    90% {
        -webkit-transform: translateX(-8px);
        transform: translateX(-8px);
    }
}

/*Success and Error Styline*/

.success, .error {
    color: white;
    padding: 5px 0;
    margin: 5px 70px;
}

.success {
    background: #4CAF50;
}

.error {
    background: #ff4d4f;
}


You can see the link of where I copied the animation in the styles above, I also added a background color of red to the error class, and green to the success class.

In the Musiclist_UI class, I'll handle the following method to handle the notification message:


    notification(message, className) {
        // Create div
        const div = document.createElement('div');
        // Add classes
        div.className = `${className}`;
        // Add text
        div.appendChild(document.createTextNode(message));
        // Get parent
        const container = document.querySelector('#form-container');
        // Insert notification
        container.insertBefore(div, container.firstElementChild);

        // Timeout after 4 sec
        setTimeout(function(){
            document.querySelector('#form-container').firstElementChild.remove();
        }, 4000);
    }


It takes in two-argument, one for the message it would display depending on if the action is successful or not, the second argument takes in a class name. So, in the notification method, we created and store in the "div" constant, next up.. the class that we passed in would be appended to the div, here: div.className = `${className}`;.

Next up, we add the message passed into the method, which would be a textnode of the div, here: div.appendChild(document.createTextNode(message));. So, we then get the parent container, in my case, it a div with an id of #form-container, we then insert the notification like so:


// Insert notification
container.insertBefore(div, container.firstElementChild);


If for example, the structure is like so:


<div id="form-container" class="container-fluid">
    <div class="row justify-content-lg-start">

        <form id="musiclist-form" class="form-group m-auto">
        ........................
        </form>

    </div>
</div>


Then the insertbefore function would insert our notification div before the container firstElementchild, in our case, this would be before the div with a class of row justify-content-lg-startHere is an illustration if you are still curious about that:

7. Insertbefore container firstelemntchild


and lastly, we set a timer for when the firstElementchild would be removed:


// Timeout after 4 sec
setTimeout(function(){
    document.querySelector('#form-container').firstElementChild.remove();
}, 4000);


In the event listener that listen for submit button, we re-arrange the logic for adding new music to the musicList:


document.getElementById('musiclist-form').addEventListener('submit', function(e) {
    // Get values entered into the input box
    const artisteName = document.getElementById('artiste-name').value;
    const songName = document.getElementById('song-name').value;

    // Instantiate Musiclist_Info
    const info = new Musiclist_Info(artisteName, songName);

    // Instantiate the UI class
    const addNewMusic = new Musiclist_UI();

    // Validate
    if(artisteName.replace(/\s/g,"") === "" || songName.replace(/\s/g,"") === "" ) {
        // Error Notification
        addNewMusic.notification('Seems you are missing a field', 'err-anim error')
    } else {
        // Add music to musiclist
        addNewMusic.addMusicToList(info);

        // success Notification
        addNewMusic.notification('Music Added', 'success')

        // Clear the input box
        document.getElementById('artiste-name').value = '';
        document.getElementById('song-name').value = '';
    }

    e.preventDefault() // Prevent the default action of the submit button

});


The if condition checks if the input box is empty (empty spaces also won't be allowed) upon clicking the submit button, if it is, we pass in an error notice with a couple of classes to notify the user:


addNewMusic.notification('Seems you are missing a field', 'err-anim error')


These are the classes I added in the css file, if the input box isn't empty upon clicking the submit button, we add new music to the list, and show a success notification, here is an illustration of how that looks:

8. Validation of the musiclist

Clear All Function

We should also add a clear all function, this should be pretty easy, first add the following method to your Musiclist_UI:


clearAllFromMusicList() {
        const list = document.getElementById('music-list');
        while (list.firstChild) {
            list.removeChild(list.firstChild);
        }
}


the document.getElementByid gets the <tbody></tbody> element that contains our rows of data, we then use while loop to loop through all of its firstChild, which would be the <tr></tr> elements, we then remove all of that.

Next up, add a new click event listener to the clear all button like so:


// Event Listener for Clear All Btn
document.getElementById('clear-all').addEventListener('click', function (e){

    const clearall = new Musiclist_UI();

    clearall.clearAllFromMusicList();

    e.preventDefault();

});


and that should do the trick, here is an illustration:

9. Clear all button functionality to music list

Cool. We still have one more thing to do, and that is...

Persisting Music Lists To Local Storage

As crazy as it sounds, it only takes a browser refresh for your music list to be gone, and this is because we are not persisting it to any storage. We can persist our list in the browser local storage, this way, it won’t get deleted even if you close the browser or you refresh the page.

For that, we should create a new class for our local storage since it has nothing to the with UI or collecting user info, and in the class, we would be using a static method, we use a static method because we don't want the functionality of a method to depend on an object instance, in other words, if something is not gonna change often, use a static method, here is how the class looks with a couple of static method that does a certain task for the localStorage:


// Class for our localStorage
class StoreMusicListToLocalStorage {

    static getMusic() { // This gets the music from the localStorage if there is one, otherwise we add an empty array
        let values;
        if(localStorage.getItem('music') === null) {
            values = [];
        } else {
            values = JSON.parse(localStorage.getItem('music'));
        }
        return values;
    }

    static addMusic(info) {
        const list = document.getElementById('music-list');

        // We check if the td element is available, if it is
        if(list.firstElementChild) {
            let itemLength = document.getElementById('music-list').querySelectorAll('tr');
            let id = itemLength.length;

            let output = "";
            for (let item in info) {
                output = output + info[item] + ",";
            }
            let item = output.split(',')  // Splitting by comma delimiter

            let artisteName = item[0];
            let songName = item[1];

            // Reinstatiate Musiclist_Info
            const MusicInfo = new Musiclist_Info(artisteName, songName, id)

            const values = StoreMusicListToLocalStorage.getMusic();

            values.push(MusicInfo);

            localStorage.setItem('music', JSON.stringify(values));
        }
    }

    static removeMusic(musicID) {
        const music = StoreMusicListToLocalStorage.getMusic();
        music.forEach(function(item, index){
            let id  = item.id;
            if(id === musicID) {
               music.splice(index, 1);
            }
        });
        localStorage.setItem('music', JSON.stringify(music));
    }

    static displayMusic() {
        const music = StoreMusicListToLocalStorage.getMusic();
        music.forEach(function(item){
            const addNewMusic  =  new Musiclist_UI();

            // Add music to UI
            addNewMusic.addMusicToList(item);
        });
    }
}


Let me break it down the method, one after the order:

static getMusic() {...}-

  • First and foremost, the purpose of the function is to get whatever is in the localStorage if there is one, so, I started by declaring a variable that would that, we then use if condition to check if there is a null value (that is;empty) in the localStorage that has a key of music, if there is none, we add an empty array to the values variable.
  • If that is false, that is if there is data in the values variable, we parse it using JSON (JSON is the data stored in the localStorage, so you need to parse it first when retrieving), and we then store it in the values variable.
  • Lastly, we return the values variable, by default it doesn do anything, it just a variable for getting values in the localStorage if there is one, and then return it to whatever method you are calling it from.

Before we move on to the next method, modify the Musiclist_info class to contain the id property like so:


class Musiclist_Info {
    constructor(name, song, id) { // the constructpr
        this.name = name;
        this.song = song;
        this.id   = id;
    }
}


We would make use of it when re-instantiating the class in the following static method:

static addMusic(info) {...}-

  • In the addMusic method, I passed in the info as an argument, when calling the function in the submit event listener we would pass it along.
  • Next up, I declared a constant that holds the <tbody></tbody>, music-list is the id of the tbody element.
  • Now, we then check using if statement if the tbody firstElementChild exist (this would be the row of data), which it would likely does as in the submit event listener we would add this static method to the else block, so, it won't get called if no item has been submitted. So, back to the if block in the addMusic static method, if the firstElementChild exist we select the child element, and we get the length of the child element.
  • Next up, we use a for...in loop to loop over the info object we passed in, we are looping over each property of the info object, and each time through the loop, it returns the name of the property and assigns that to the item variable, so, we then use the property names using the square bracket notation to access it. I am using the output variable to append each data each time through the loop, and we add a comma to each one of them, e.g if the data is: name: Fela
    song: Colonial Mentality
    id: undefined
    it would get: Fela, Colonial Mentality, undefined
  • Outside of the loop, we use the split function to split each of whatever is in the output variable into an array, we are splitting by the comma delimeter
  • Then we re-assign the artisteName and songName to the array index value
  • We re-instantiate the Musiclist_info class, and pass the arguments, but this time we are passing in 3 argument, the last one is the id, which would get the length of the current firstElementchil of the tbody element.
  • We store the data that is in the getMusic() method into the values variable, and append whatever is in the MusicInfo object we just re-instantiated into the values variables, and lastly
  • we set the item: localStorage.setItem('music', JSON.stringify(values));

static removeMusic(musicID) {...}-

  • We pass in the musicID argument, this would be passed when an item is deleted, we would use event delegation to get that in the delete event handler. So, in the function, we get whatever is the getMusic() static method, and store that in the music constant,
  •  To get the value out from the localStorage, you can either use a forEach, which is an Array method that we can use to execute a function on each item in an array, or you can use for/of loop, you can’t use for loop or while loop because they won’t know how to get the amount of item in the list. we are using a forEach loop in our case.
  • The forEach loop takes a callback function. The function will be executed for every single element of the array. So, we loop through the values with the forEach array method by passing two parameters, one which is item (would be the one to store individual music data that is gotten from the array), and the other which is index would keep track of the index count. I store the id of the music data in the id variable
  • The if condition is checking if the id of that particular music item is strictly equal to the musicID (the one passed into the removeMusic method) (remember item stores individual music item as loop iterates), if that is true, we use splice function to remove whatever index the item is located. Note that, this isn’t actually storing anything yet, we just spliced an item identified by a certain index from the music array.
  • To then store the value to the localStorage, we use: localStorage.setItem('music', JSON.stringify(music));

static displayMusic(musicID) {...}-

  • As usual, we get the current data from the getMusic static method, and store that in our music constant
  • The forEach loop takes a callback function.  It must take at least one parameter which represents the elements or item of the array, in our case, as long as there are data in the item variable, we instantiate the Musiclist_UI, and we then add music to the table row.

So, let's re-adjust our event listeners to fit in with the static methods of the StoreMusicListToLocalStorage class. Add the following at the top of your event listeners:


// DOM Load Event
document.addEventListener('DOMContentLoaded', StoreMusicListToLocalStorage.displayMusic);


The above deals with rebuilding the UI when the DOM content reloads.

Next up, in the else block statement of the submit event listener, I'll adjust it like so:


else {
        // Add music to musiclist
        addNewMusic.addMusicToList(info);

        // Add to localStorage
        StoreMusicListToLocalStorage.addMusic(info);

        // success Notification
        addNewMusic.notification('Music Added', 'success')

        // Clear the input box
        document.getElementById('artiste-name').value = '';
        document.getElementById('song-name').value = '';
    }


I'll adjust the delete event handler like so:


// Event Listener for delete
document.getElementById('music-list').addEventListener('click', function(e){

    const deleteMusic = new Musiclist_UI();

    deleteMusic.deletefromMusicList(e.target);

    // Remove from localStorage
    let id = e.target.parentElement.parentElement.firstElementChild.textContent
    StoreMusicListToLocalStorage.removeMusic(parseInt(id));

    e.preventDefault();
});


I would also add a clear function in the clear-all event handler to also clear all the data in the localStorage once the clear all button is clicked:


// Event Listener for Clear All Btn
document.getElementById('clear-all').addEventListener('click', function (e){

    const clearall = new Musiclist_UI();

    clearall.clearAllFromMusicList();

    localStorage.clear(); // Clear All the data from localStorage

    e.preventDefault();

});


All together, I have:


// Code Template or Class for the Music Object
class Musiclist_Info {
    constructor(name, song, id) { // the constructpr
        this.name = name;
        this.song = song;
        this.id   = id;
    }
}

// Code Template or Class for our user Interface
class Musiclist_UI {

    addMusicToList(info) {
        const list = document.getElementById('music-list');
        // Create tr element
        const row = document.createElement('tr');

        // We check if the td element is available, if it is, we increment the first td by one
        if(list.firstElementChild) {
            let itemLength = document.getElementById('music-list').querySelectorAll('tr');
            let i = itemLength.length + 1;

            // Insert cols
            row.innerHTML = `
          <td>${i}</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);

            // if td element isn't available, we create our first td
        } else {
            // Insert cols
            row.innerHTML = `
          <td>1</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);
        }
    }

    deletefromMusicList(target) {
        if(target.classList.contains('btn-danger')) {
            target.parentElement.parentElement.remove();
        }
    }

    notification(message, className) {
        // Create div
        const div = document.createElement('div');
        // Add classes
        div.className = `${className}`;
        // Add text
        div.appendChild(document.createTextNode(message));
        // Get parent
        const container = document.querySelector('#form-container');
        // Insert notification
        container.insertBefore(div, container.firstElementChild);

        // Timeout after 4 sec
        setTimeout(function(){
            document.querySelector('#form-container').firstElementChild.remove();
        }, 4000);
    }

    clearAllFromMusicList() {
        const list = document.getElementById('music-list');
        while (list.firstChild) {
            list.removeChild(list.firstChild);
        }
    }
}

// Class for our localStorage
class StoreMusicListToLocalStorage {

    static getMusic() { // This gets the music from the localStorage if there is one, otherwise we add an empty array
        let values;
        if(localStorage.getItem('music') === null) {
            values = [];
        } else {
            values = JSON.parse(localStorage.getItem('music'));
        }

        return values;
    }

    static addMusic(info) {
        const list = document.getElementById('music-list');

        // We check if the td element is available, if it is
        if(list.firstElementChild) {
            let itemLength = document.getElementById('music-list').querySelectorAll('tr');
            let id = itemLength.length;

            let output = "";
            for (let item in info) {
                output = output + info[item] + ",";
            }
            let item = output.split(',')  // Splitting by comma delimiter

            let artisteName = item[0];
            let songName = item[1];

            // Reinstatiate Musiclist_Info
            const MusicInfo = new Musiclist_Info(artisteName, songName, id)

            const values = StoreMusicListToLocalStorage.getMusic();

            values.push(MusicInfo);

            localStorage.setItem('music', JSON.stringify(values));

        }
    }

    static removeMusic(musicID) {
        const music = StoreMusicListToLocalStorage.getMusic();

        music.forEach(function(item, index){
            let id  = item.id;
            if(id === musicID) {
               music.splice(index, 1);
            }
        });

        localStorage.setItem('music', JSON.stringify(music));
    }

    static displayMusic() {
        const music = StoreMusicListToLocalStorage.getMusic();

        music.forEach(function(item){
            const addNewMusic  =  new Musiclist_UI();

            // Add music to UI
            addNewMusic.addMusicToList(item);
        });
    }
}

/*
-----------------------
  Start Event Listener
----------------------
*/
// DOM Load Event
document.addEventListener('DOMContentLoaded', StoreMusicListToLocalStorage.displayMusic);

//  Event Listeners To Actually Listen For Submit Events ;)
document.getElementById('musiclist-form').addEventListener('submit', function(e) {
    // Get values entered into the input box
    const artisteName = document.getElementById('artiste-name').value;
    const songName = document.getElementById('song-name').value;

    // Instantiate Musiclist_Info
    const info = new Musiclist_Info(artisteName, songName);

    // Instantiate the UI class
    const addNewMusic = new Musiclist_UI();

    // Validate
    if(artisteName.replace(/\s/g,"") === "" || songName.replace(/\s/g,"") === "" ) {
        // Error Notification
        addNewMusic.notification('Seems you are missing a field', 'err-anim error')
    } else {
        // Add music to musiclist
        addNewMusic.addMusicToList(info);

        // Add to localStorage
        StoreMusicListToLocalStorage.addMusic(info);

        // success Notification
        addNewMusic.notification('Music Added', 'success')

        // Clear the input box
        document.getElementById('artiste-name').value = '';
        document.getElementById('song-name').value = '';
    }

    e.preventDefault() // Prevent the default action of the submit button

});

// Event Listener for delete
document.getElementById('music-list').addEventListener('click', function(e){

    const deleteMusic = new Musiclist_UI();

    deleteMusic.deletefromMusicList(e.target);

    // Remove from localStorage
    let id = e.target.parentElement.parentElement.firstElementChild.textContent
    StoreMusicListToLocalStorage.removeMusic(parseInt(id));

    e.preventDefault();
});

// Event Listener for Clear All Btn
document.getElementById('clear-all').addEventListener('click', function (e){

    const clearall = new Musiclist_UI();

    clearall.clearAllFromMusicList();

    localStorage.clear(); // Clear All the data from localStorage

    e.preventDefault();

});
/*
-----------------------
  End Event Listener
----------------------
*/


and that is it.

Musiclist Application With JavaScript [Visual Illustration]



Bug 1: UI ID Not In Sync When The First Music Item or Anything Between The First and Last is Deleted

There is a fix for the aforementioned bug, but before I present the code, let see a visual illustration, and I then explain why the bug occurred:



As you can see if you delete anything between the first and the last item, it gets re-organize on fresh reload, if you also delete the first item among others, it gets re-arranged on browser reload.

The only thing that works is if the user starts deleting from the last item, which is absurd.

Why the bug occurred

The bug occurred because:

  • We are not separating the concern of rebuilding the UI from the localstorage and specifically adding a new music item

In the static displayMusic method in the StoreMusicListToLocalStorage class, we have this:


    static displayMusic() {
        const music = StoreMusicListToLocalStorage.getMusic();

        music.forEach(function(item){
            const addNewMusic  =  new Musiclist_UI();

            // Add music to UI
            addNewMusic.addMusicToList(item);
        });


Once it gets the data from the localstorage, we use the forEach array method to pass each and every of the music item got to the addMusicToList method in the Musiclist_UI class, and this is how that method looks in the Musiclist_UI class:


addMusicToList(info) {
    const list = document.getElementById('music-list');
    // Create tr element
    const row = document.createElement('tr');

    // We check if the td element is available, if it is, we increment the first td by one
    if(list.firstElementChild) {
        let itemLength = document.getElementById('music-list').querySelectorAll('tr');
        let i = itemLength.length + 1;

        // Insert cols
        row.innerHTML = `
      <td>${i}</td>
      <td>${info.name}</td>
      <td>${info.song}</td>
      <td><button class="btn btn-danger" type="button">Delete</button></td>`;
        list.appendChild(row);

        // if td element isn't available, we create our first td
    } else {
        // Insert cols
        row.innerHTML = `
      <td>1</td>
      <td>${info.name}</td>
      <td>${info.song}</td>
      <td><button class="btn btn-danger" type="button">Delete</button></td>`;
        list.appendChild(row);
    }
}


In the above code, look at the way we are incrementing the id, we are checking if the tr element exists, and we are adding a plus one to each of the ids as it goes through the loop, so, for the first time, it clearly doesn't exist, so, it would use the else statement to initialize the id, and the rest of the iteration would be true for the if statement.

This is clearly a wrong approach to re-build our music list UI, instead of relying on the addMusicToList method, we would create another function that specifically deals with building the UI for us.

Fix

The fix is adding a separate method to rebuild the music list from the localStorage instead of relying on the addMusicToList method.

So, in the static displayMusic() of the StoreMusicListToLocalStorage class, adjust it like so:


static displayMusic() {
const music = StoreMusicListToLocalStorage.getMusic();

music.forEach(function(item){
const addNewMusic = new Musiclist_UI();

// rebuild music to UI
addNewMusic.rebuildMusicList(item)
});
}


We pass each item we get from the localStorage to a new method instead (rebuildMusicList), so, I'll create a rebuildMusicList method in the Musiclist_UI like so:


rebuildMusicList(info) {
    const list = document.getElementById('music-list');
    // Create tr element
    const row = document.createElement('tr');

    // Insert cols
    row.innerHTML = `
      <td>${info.id}</td>
      <td>${info.name}</td>
      <td>${info.song}</td>
      <td><button class="btn btn-danger" type="button">Delete</button></td>`;
    list.appendChild(row);

}


Simple and elegant ;) So, now, we can maintain the id of the music items that are not deleted, ids are given for a reason, and that is to give uniqueness to each item, actually, I am trying to simulate how it works in SQL.

So, here is the visual representation of the fix:



Cool if you ask me, here is the code:


// Code Template or Class for the Music Object
class Musiclist_Info {
    constructor(name, song, id) { // the constructpr
        this.name = name;
        this.song = song;
        this.id   = id;
    }
}

// Code Template or Class for our user Interface
class Musiclist_UI {

    addMusicToList(info) {
        const list = document.getElementById('music-list');
        // Create tr element
        const row = document.createElement('tr');

        // We check if the td element is available, if it is, we increment the first td by one
        if(list.firstElementChild) {
            // We check the last item in the localStorage, and use that to increment the newly added one
            let ls = JSON.parse(localStorage.getItem('music'));
            let lastlslength = ls.length - 1
            let id = ls[lastlslength]['id'] + 1 // We then increment the id of what the newly added element would be at each time

            // Insert cols
            row.innerHTML = `
          <td>${id}</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);

            // if td element isn't available, we create our first td
        } else {
            // Insert cols
            row.innerHTML = `
          <td>1</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
            list.appendChild(row);
        }

    }

    rebuildMusicList(info) {
        const list = document.getElementById('music-list');
        // Create tr element
        const row = document.createElement('tr');

        // Insert cols
        row.innerHTML = `
          <td>${info.id}</td>
          <td>${info.name}</td>
          <td>${info.song}</td>
          <td><button class="btn btn-danger" type="button">Delete</button></td>`;
        list.appendChild(row);

    }

    deletefromMusicList(target) {
        if(target.classList.contains('btn-danger')) {
            target.parentElement.parentElement.remove();
        }
    }

    notification(message, className) {
        // Create div
        const div = document.createElement('div');
        // Add classes
        div.className = `${className}`;
        // Add text
        div.appendChild(document.createTextNode(message));
        // Get parent
        const container = document.querySelector('#form-container');
        // Insert notification
        container.insertBefore(div, container.firstElementChild);

        // Timeout after 4 sec
        setTimeout(function(){
            document.querySelector('#form-container').firstElementChild.remove();
        }, 4000);
    }

    clearAllFromMusicList() {
        const list = document.getElementById('music-list');
        while (list.firstChild) {
            list.removeChild(list.firstChild);
        }
    }
}


// Class for our localStorage
class StoreMusicListToLocalStorage {

    static getMusic() { // This gets the music from the localStorage if there is one, otherwise we add an empty array
        let values;
        if(localStorage.getItem('music') === null) {
            values = [];
        } else {
            values = JSON.parse(localStorage.getItem('music'));
        }

        return values;
    }

    static addMusic(info) {
        const list = document.getElementById('music-list');

        // We check the last item in the localStorage, and use that to increment the newly added one

        if(list.firstElementChild) { // We check if the td element is available, if it is do below
            let itemLength = document.getElementById('music-list').querySelectorAll('tr');
            let id = itemLength.length;

            let output = "";
            for (let item in info) {
                output = output + info[item] + ",";
            }
            let item = output.split(',')  // Splitting by comma delimiter

            let artisteName = item[0];
            let songName = item[1];

            // Reinstatiate Musiclist_Info
            const MusicInfo = new Musiclist_Info(artisteName, songName, id)

            const values = StoreMusicListToLocalStorage.getMusic();

            values.push(MusicInfo);

            localStorage.setItem('music', JSON.stringify(values));

        }
    }

    static removeMusic(musicID) {
        const music = StoreMusicListToLocalStorage.getMusic();

        music.forEach(function(item, index){
            let id  = item.id;
            if(id === musicID) {
                music.splice(index, 1);
            }
        });

        localStorage.setItem('music', JSON.stringify(music));
    }

    static displayMusic() {
        const music = StoreMusicListToLocalStorage.getMusic();

        music.forEach(function(item){
            const addNewMusic  =  new Musiclist_UI();

            // rebuild music to UI
            addNewMusic.rebuildMusicList(item)
        });
    }


}
/*
-----------------------
  Start Event Listener
----------------------
*/
// DOM Load Event
document.addEventListener('DOMContentLoaded', StoreMusicListToLocalStorage.displayMusic);

//  Event Listeners To Actually Listen For Submit Events ;)
document.getElementById('musiclist-form').addEventListener('submit', function(e) {
    // Get values entered into the input box
    const artisteName = document.getElementById('artiste-name').value;
    const songName = document.getElementById('song-name').value;

    // Instantiate Musiclist_Info
    const info = new Musiclist_Info(artisteName, songName);

    // Instantiate the UI class
    const addNewMusic = new Musiclist_UI();

    // Validate
    if(artisteName.replace(/\s/g,"") === "" || songName.replace(/\s/g,"") === "" ) {
        // Error Notification
        addNewMusic.notification('Seems you are missing a field', 'err-anim error')
    } else {
        // Add music to musiclist
        addNewMusic.addMusicToList(info);

        // Add to localStorage
        StoreMusicListToLocalStorage.addMusic(info);

        // success Notification
        addNewMusic.notification('Music Added', 'success')

        // Clear the input box
        document.getElementById('artiste-name').value = '';
        document.getElementById('song-name').value = '';
    }

    e.preventDefault() // Prevent the default action of the submit button

});

// Event Listener for delete
document.getElementById('music-list').addEventListener('click', function(e){

    const deleteMusic = new Musiclist_UI();

    deleteMusic.deletefromMusicList(e.target);

    // Remove from localStorage
    let id = e.target.parentElement.parentElement.firstElementChild.textContent
    StoreMusicListToLocalStorage.removeMusic(parseInt(id));

    e.preventDefault();
});

// Event Listener for Clear All Btn
document.getElementById('clear-all').addEventListener('click', function (e){

    const clearall = new Musiclist_UI();

    clearall.clearAllFromMusicList();

    localStorage.clear(); // Clear All the data from localStorage

    e.preventDefault();

});
/*
-----------------------
  End Event Listener
----------------------
*/


Related Post(s)

  • Getting Started With JavaScript (Introduction)

    This is my first JavaScript guide on this blog, and in this guide, we would go over the introduction of JavaScript, but first… What is JavaScript? JavaScript is a client-side scripting language su

  • Operator in Javascript

    Operators in javascript can either be used for logical expression where you connect two or more expressions and can be a comparison expression where you compare two values. You can also use an operat

  • Exploring Data Types, and Variables in JavaScript

    In JavaScript or any programming language, a data type is an attribute of data that tells the interpreter how the programs intend to use the given data. JavaScript support a couple of data type whic

  • Creating a Loader and Remove it After a Specific Time Using JavaScript/CSS/SVG

    In this guide, we would create a loader, and remove it after a specific time using CSS and JavaScript. The icon would be in SVG format, and the rotation would be done in CSS, while the removal after

  • Object Oriented Programming in JavaScript (The ES5 Way)

    In this guide, you'll learn and understand how to create and use Objects in JavaScript (ES5). While ES6 already supports creating classes (which is the way you create objects in Java, PHP, etc), they

  • Working With The Local & Session Storage In JavaScript

    We previously take a deep dive into the Windows Object, Properties, Methods, and even the Document Object itself, in this guide, we would explore working with the local and session storage in JavaScr