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, CSS, Basics of JavaScript
- [Window Objects, Methods, and Properties In JavaScript] + Document Object
- Working With The Local & Session Storage In JavaScript
- Object-Oriented Programming in JavaScript (The ES5 Way)
- Object-Oriented Programming (Classes) in JavaScript (The ES6 Way)
- HTML Structure of the Music List App
- Creating The Classes
- Adding Event Listeners
- Adding Data To The Music Table
- Delete Method
- Validation
- Clear All Function
- Persisting Music Lists To Local Storage
- 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
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:
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);
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:
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:
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:
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:
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-start
Here is an illustration if you are still curious about that:
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:
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:
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
it would get:
song: Colonial Mentality
id: undefinedFela, 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 ---------------------- */