Lesson 8 - Improving the Object-Oriented Diary In JavaScript
In the previous lesson, Objects, JSON, And Enhancing the JavaScript Diary, we improved our diary and added saving
entries to localStorage
. Today, we're going to add sorting by date,
grouping entries with the same date, and deleting entries.
Listing Entries
First, we'll edit the listing of the entries. It'd be best to sort the entries by date and group the entries happening on the same day, which we'll list below each other in one block.
Sorting
First, we'll implement a method for sorting the entries. To do this, we'll
use the sort()
method on the array. It sorts the array by comparing
element pairs. As a parameter, it accepts a comparison function that defines how
to compare 2 elements from the array. Our method in the Diary
class
to sort the entries
array will be as follows:
sortEntries() { this.entries.sort(function (entry1, entry2) { return (new Date(entry1.date) - new Date(entry2.date)); }); }
The function compares the dates of two entries that we parse to dates using
the Date
object's constructor. Of course, we pass the
date
property from the entries. To compare two dates, we simply use
the -
operator which returns their difference in milliseconds. If
the first date is after the second, a negative number is returned. If they are
the same, 0
is returned. Otherwise, a positive number is returned.
It's a positive, negative, or zero number what the sort()
method
needs to determine whether the date is greater, smaller, or equal.
We'll call the method every time before printing the entries:
printEntries() { this.sortEntries(); this.printElement.innerHTML = ""; for (let i = 0; i < this.entries.length; i++) { const entry = this.entries[i]; this.printElement.innerHTML += `<h3>${entry.name}</h3>when: ${entry.date}<br>done: ${entry.done}`; } }
We could also call the sorting after adding an entry and after loading them so that we won't have to call it each time we list them. The disadvantage of this solution, however, would be that we could forget about it while manipulating the entries elsewhere.
If we now run the diary and have some data in localStorage
,
these will be ordered by date.
Grouping
Finally, we're going to finish listing the entries. We'll print the date and all the entries occurring on that day. We can easily modify our loop:
printEntries() { this.sortEntries(); this.printElement.innerHTML = ""; let lastDate = null; for (const entry of this.entries) { if (entry.date !== lastDate) { this.printElement.innerHTML += `<h3>${entry.date}</h3>` } lastDate = entry.date; this.printElement.innerHTML += `<strong>${entry.name}</strong><br>done: ${entry.done}<hr>`; } }
We assign the date
property from the previous entry to the
lastDate
variable. Since there's no last entry when the loop's
running for the first time, we initialize the variable to null
for
the start. We then print the date of the current entry only if it differs from
the last one. So the entries on the same day will be grouped. Now we have the
tasks occurring on the same date printed nicely under each other and sorted:
Formatting the date And done Properties
Since the date format and the way we print the done
property are
also not ideal, we need to edit the printing to make it more "human-friendly"
Remember the
language
property that we can set in the constructor? Now we're
going to use it to print the date better and at the same time we'll improve
printing of the done
property:
printEntries() { this.sortEntries(); this.printElement.innerHTML = ""; let lastDate = null; for (const entry of this.entries) { if (entry.date !== lastDate) { const date = new Date(entry.date).toLocaleDateString(this.language, { weekday: "long", day: "numeric", month: "short", year: "numeric" }); this.printElement.innerHTML += `<h3>${date}</h3>` } lastDate = entry.date; this.printElement.innerHTML += `<strong>${entry.name}</strong><br>task ${!entry.done ? "not " : ""}done<hr>`; } }
We create an instance of the Date
object based on our date and
use its toLocaleString()
method, to which we pass the
language
property of our class and as the second parameter a
formatting object whose properties specify how exactly the date should be
printed.
We use a simple ternary operator to print the done
property, we
add either "not "
or an empty string
.
The result now looks like this:
Deleting Entries
We'll continue with the basic interaction with our tasks, delete them, or mark them as done. Let's start with deleting.
Saving Entries
Since we'll need to save the entries again after deleting, let's move saving
the entries from the setEvents()
method to a separate
saveEntries()
method:
saveEntries() { localStorage.setItem("entries", JSON.stringify(this.entries)); }
We'll now call the saveEntries()
method in the
setEvents()
method:
setEvents() { this.confirmButton.onclick = () => { // this now stays this const entry = new Entry(this.nameInput.value, this.dateInput.value); this.entries.push(zaznam); this.saveEntries(); this.printEntreis(); }; }
The Button
We'll generate a button for each entry to remove it. We'll create it as a new
<button>
element using the
document.createElement()
method and put it into a
<div>
using appendChild()
. We'll also add a
click event handler to the button, removing the entry from the array, saving the
entries to localStorage
, and returning them. The
printEntries()
method after adding a delete button will look like
this:
printEntries() { this.sortEntries(); this.printElement.innerHTML = ""; let lastDate = null; for (const entry of this.entries) { if (entry.date !== lastDate) { const date = new Date(entry.date).toLocaleDateString(this.language, { weekday: "long", day: "numeric", month: "short", year: "numeric" }); this.printElement.innerHTML += `<h3>${date}</h3>` } lastDate = entry.date; this.printElement.innerHTML += `<strong>${entry.name}</strong> <br>task ${!entry.done ? "not " : ""}done`; const deleteButton = document.createElement("button"); deleteButton.onclick = () => { if (confirm("Are you sure you want to remove the task?")) { this.entries = this.entries.filter(e => e !== entry); // Keep everything not equal to the entry variable this.saveEntries(); this.printEntries(); } }; deleteButton.innerText = "Delete entry"; this.printElement.appendChild(deleteButton); this.printElement.innerHtml += "<br>"; } }
Note the use of the confirm()
confirmation dialog, removing the
record is certainly an action we don't want to make by mistake
You might want to insert the button directly into the HTML code
as text and assign it the array index of the entry to delete as a data
attribute. Such buttons would then all be selected and handled to delete an
element from the array below the given index. However, the problem would arise
if we opened the diary in multiple browser tabs at once. If we deleted an item
in one tab and did not refresh the other tab, that item would still be there,
but in localStorage
, there would be a totally different item under
that index. Like that, we could unintentionally delete some other task in a
non-refreshed tab. Therefore, we'll always do all of the item
manipulations directly using anonymous functions to which we will pass this one
particular item.
If you shake your head over the item removal code:
this.entries = this.entries.filter(e => e !== entry);
Unfortunately, this is currently the easiest way to delete an element from an array in JavaScript if we don't know it's index and don't wont to bother looking for it either. The code filters the array so that only entries that don't match the entry that we want to remove remain in it.
We'll continue next time, in the lesson Finishing an Object-Oriented Diary In JavaScript, when we'll add a button to make a given task done and complete the diary by adding simple CSS styles