How to Build a CRUD Web Application with Vanilla JavaScript


In this tutorial, we’ll be building a CRUD application in the form of a note taking app. We’ll also explore how to use local storage to save notes in our browser.

What’s CRUD?

You may already be wondering: “what does CRUD mean?!” A CRUD application is the most common form of any software application. CRUD stands for Create, Read, Update and Delete and it applies to any software that’s capable of letting users create and view data on its interface, as well as make changes to, and delete existing data.

CRUD applications can range from a simple to-do list application to a more complex social media platform. It’s also common for developers to build CRUD applications to display their grasp of a particular framework or language.

1. Creating the Markup

For our markup, we’ll have two main elements: the new-note container that contains the input and text area field for entering the content for our notes, and the notes-wrapper container that’ll contain the created notes. We also have a form-error div for displaying error messages and a button that calls the addNote() function we’ll define later.

1
<main>
2
  <div class="new-note">
3
    <input type="text" name="title" id="title" placeholder="Title">
4
    <textarea name="content" id="content" placeholder="Start writing"></textarea>
5
    <div id="form-error"></div>
6
  </div>
7
  <div class="button-container">
8
    <button class="add-btn" onclick="addNote()">Add note</button>
9
  </div>
10
  <div id="notes-wrapper"></div>
11
</main>

2. Styling the Layout

This is the CSS for our initial layout:

1
.new-note {
2
  margin-bottom: 32px;
3
  background-color: white;
4
  border: 1px solid #dadada;
5
  border-radius: 6px;
6
  padding: 16px;
7
  position: relative;
8
}
9
10
.new-note input {
11
  width: 100%;
12
  background: transparent;
13
  border: none;
14
  outline: none;
15
  font-size: 1.5rem;
16
  font-weight: 700;
17
  padding: 0 0 12px;
18
}
19
20
.new-note textarea {
21
  display: block;
22
  width: 100%;
23
  min-height: 100px;
24
  border: none;
25
  resize: none;
26
  background: transparent;
27
  outline: none;
28
  padding: 0;
29
}
30
31
#form-error {
32
  color: #b33030;
33
  position: absolute;
34
  bottom: -32px;
35
  left: 0;
36
}
37
38
.button-container {
39
  display: flex;
40
  justify-content: center;
41
  margin-bottom: 32px;
42
}
43
44
.add-btn {
45
  border: 1px solid rgb(95, 95, 95);
46
  background-color: transparent;
47
  transition: background-color 250ms, color 250ms;
48
  border-radius: 16px;
49
  cursor: pointer;
50
  padding: 8px 12px;
51
}
52
53
.add-btn:hover {
54
  background-color: rgb(95, 95, 95);
55
  color: white;
56
}
57
58
#notes-wrapper {
59
  display: flex;
60
  flex-direction: column;
61
  gap: 32px;
62
}
63
64
.note {
65
  position: relative;
66
  overflow: hidden;
67
  background: white;
68
  border: 1px solid #dadada;
69
  border-radius: 6px;
70
  padding: 16px;
71
}
72
73
.note-title {
74
  font-size: 1.5em;
75
  font-weight: 700;
76
  margin-bottom: 12px;
77
  width: 100%;
78
  display: inline-block;
79
  overflow-wrap: break-word;
80
  white-space: pre-wrap;
81
}
82
83
.note-title:last-child {
84
  margin-bottom: 0;
85
}
86
87
.note-text {
88
  margin-bottom: 16px;
89
  background-color: white;
90
  width: 100%;
91
  overflow-wrap: break-word;
92
  white-space: pre-wrap;
93
  background-color: white;
94
  overflow: hidden;
95
  width: 100%;
96
}
97
98
.note-date {
99
  font-size: 0.75em;
100
  text-align: right;
101
  border-top: 1px solid #dadada;
102
  padding-top: 16px;
103
  width: 100%;
104
  margin-top: auto;
105
}
106
107
.note-controls {
108
  position: absolute;
109
  right: 0;
110
  top: 0;
111
  background-color: white;
112
  font-size: 0.75rem;
113
  column-gap: 8px;
114
  padding: 8px;
115
  display: flex;
116
  opacity: 0;
117
  transition: opacity 350ms;
118
}
119
120
.note:hover .note-controls,
121
.note-controls:focus-within {
122
  opacity: 1;
123
}
124
125
.note-controls button {
126
  padding: 0;
127
  border: none;
128
  background-color: transparent;
129
  cursor: pointer;
130
  padding: 0.5rem;
131
}
132
133
.note-controls button:hover {
134
  filter: brightness(0.85)
135
}
136
137
.note-delete {
138
  color: #bb0000;
139
}
140
141
.note-edit {
142
  color: #00bb00;
143
}
144
145
.note-save {
146
  color: #0000bb;
147
}
148
149
.note-save[disabled="true"] {
150
  color: #dfdfdf;
151
  pointer-events: none;
152
  user-select: none;
153
  cursor: not-allowed;
154
}

In this demo, we’ve removed the outline styling on our input and textarea fields to avoid interfering with the styling. However, if you wish, you can leave the outline styling for accessibility purposes.

This is what our layout looks like currently:

our note taking app demoour note taking app demoour note taking app demo

3. Creating and Reading Data

Now we can start working on the logic of our application!

Global Elements

First, let’s get all the global elements we’ll need.

1
const notesWrapper = document.getElementById("notes-wrapper");
2
const title = document.getElementById("title");
3
const content = document.getElementById("content");
4
const error = document.getElementById("form-error");

Then we’ll define a global variable to store our notes data.

Creating and Storing Data

In order to create the data, we’ll define an addNote() function to be called when the button is clicked. This is what will handle taking the data from the input field and putting them into a new note element in a readable format.

To begin with, we’ll include a condition to check if there’s any content to be added in a new note. If both the title and content field are empty, we’ll display an error message.

1
const addNote = () => {
2
  if (title.value.trim().length == 0 && content.value.trim().length == 0) {
3
    error.innerText = "Note cannot be empty";
4
    return;
5
  }
6
};

Next, we create an object to contain the date for a new note. In this tutorial, we’ll be generating a unique id (uid) value for each note using the date.getTime() method. This returns the exact millisecond the note was created and is used to make sure that no two notes have the same id.

We’ll also be including the date value of when the note was created using the date.toLocaleDateString() method. This is what our updated function looks like:

1
const noteObj = {
2
    uid: new Date().getTime().toString(),
3
    title: title.value,
4
    text: content.value,
5
    date: new Date().toLocaleDateString()
6
};

Now that we have the note object, we can store that data in the notesData array and also in the browser localStorage

1
notesData.push(noteObj);
2
localStorage.setItem("notes", JSON.stringify(notesData));

localStorage only supports data in string format so we use the JSON.stringify() method to convert our array into a suitable format.

Displaying Data

Let’s define a function createNote() to handle appending the new note to the notesWrapper container. The note element will display the title, content and date based on user input. It will also have an edit, save and delete button for carrying out the corresponding functions based on the note unique id.

1
const createNote = (uid, title, text, date) => {
2
  const note = document.createElement("div");
3
  note.className = "note";
4
  note.id = uid;
5
  note.innerHTML = `
6
    <div class="note-title">${title}</div>
7
    <div class="note-controls">
8
      <button class="note-edit" onclick="editNote(${uid})">
9
        Edit
10
      </button>
11
      <button class="note-save" style="display:none" onclick="saveNote(${uid})">
12
        Save
13
      </button>
14
      <button class="note-delete" onclick="deleteNote(${uid})">
15
        Delete
16
      </button>
17
    </div>
18
    <div class="note-text">${text}</div>
19
    <div class="note-date">${date}</div>
20
  `;
21
22
  notesWrapper.insertBefore(note, notesWrapper.firstChild);
23
};

In this function, we use the .insertBefore() method to make sure that the newest note in placed at the top of the notesWrapper container.

Reset the Title and Content for Next Note

Finally, we can update our addNote() function to create a new note and also reset the title, content and error elements when the button is clicked.

1
const addNote = () => {
2
  if (title.value.trim().length == 0 && content.value.trim().length == 0) {
3
    error.innerText = "Note cannot be empty";
4
    return;
5
  }
6
7
  const noteObj = {
8
    uid: new Date().getTime().toString(),
9
    title: title.value,
10
    text: content.value,
11
    date: new Date().toLocaleDateString()
12
  };
13
  
14
  createNote(noteObj.uid, noteObj.title, noteObj.text, noteObj.date);
15
16
  error.innerText = "";
17
  content.value = "";
18
  title.value = "";
19
};

Check for Existing Data

Since we’re using localStorage, we can also include a condition to check if there’s already existing data for our notes in localStorage and display that on the page once the page loads. The JSON.parse() method is used to convert our stringified data back to its original format.

1
window.addEventListener("load", () => {
2
  notesData = localStorage.getItem("notes")
3
    ? JSON.parse(localStorage.getItem("notes"))
4
    : [];
5
    
6
  notesData.forEach((note) => {
7
    createNote(note.uid, note.title, note.text, note.date);
8
  });
9
});

4. Updating Data

At this point, we’ve handled the “C” and “R” parts of CRUD, having managed to successfully create notes based on user input, and display existing notes. Now let’s turn our attention to “U” by defining a function that lets us edit and save (update) an existing note.

Editing..

When we defined out createNote() function, we included two button elements to handle editing and saving a note based on a unique id so now we can create the editNote() and saveNote() functions. When the edit button is clicked, we’ll hide the edit button and display the save button:

1
const editNote = (uid) => {
2
  const note = document.getElementById(uid);
3
4
  const noteTitle = note.querySelector(".note-title");
5
  const noteText = note.querySelector(".note-text");
6
  const noteSave = note.querySelector(".note-save");
7
  const noteEdit = note.querySelector(".note-edit");
8
9
  noteTitle.contentEditable = "true";
10
  noteText.contentEditable = "true";
11
  noteEdit.style.display = "none";
12
  noteSave.style.display = "block";
13
  noteText.focus();
14
};

In this function, we use the uid to find the note element in the DOM. Then we target the title and text elements inside the target note and use the contentEditable method to allow us make changes to the content. contentEditable is an inbuilt browser attribute that allows a user change the content of any element if set to true.

We can update our CSS to include styling on the note element when editing.

1
.note > *[contenteditable="true"] {
2
  color: #5f5f5f;
3
  width: 100%;
4
  outline: none;
5
}

..and Resaving

For our saveNote() function, we’ll need to update our notesData value and localStorage data. We can do this using the .forEach() method to find the note that has the corresponding uid and update the content. Then we’ll push our updated array into localStorage to replace the old value.

1
  notesData.forEach((note) => {
2
    if (note.uid == uid) {
3
      note.title = noteTitle.innerText;
4
      note.text = noteText.innerText;
5
    }
6
  });
7
8
  localStorage.setItem("notes", JSON.stringify(notesData));

Then we’ll use the same logic as the editNote() function, only this time we’ll be setting the contentEditable attributes to false and hiding the save button while displaying the edit button. We’ll also use the same condition we used in our addNote() function to make sure users can’t save a blank note.

1
const saveNote = (uid) => {
2
  const note = document.getElementById(uid);
3
4
  const noteTitle = note.querySelector(".note-title");
5
  const noteText = note.querySelector(".note-text");
6
  const noteSave = note.querySelector(".note-save");
7
  const noteEdit = note.querySelector(".note-edit");
8
9
  if (
10
    noteTitle.innerText.trim().length == 0 &&
11
    noteText.value.trim().length == 0
12
  ) {
13
    error.innerHTML = "Note cannot be empty";
14
    return;
15
  }
16
17
  notesData.forEach((note) => {
18
    if (note.uid == uid) {
19
      note.title = noteTitle.innerText;
20
      note.text = noteText.innerText;
21
    }
22
  });
23
24
  localStorage.setItem("notes", JSON.stringify(notesData));
25
26
  noteTitle.contentEditable = "false";
27
  noteText.contentEditable = "false";
28
  noteEdit.style.display = "block";
29
  noteSave.style.display = "none";
30
  error.innerText = "";
31
};

5. Deleting Data

And lastly, the “D” part of CRUD.

For the final implementation of our application, we’ll be handling deleting data. For this function, we’ll remove the note element from the DOM and also delete the note object from our notesData array. We can handle removing the object from the array by using the .filter() method.

1
const deleteNote = (uid) => {
2
  let confirmDelete = confirm("Are you sure you want to delete this note?");
3
  if (!confirmDelete) {
4
    return;
5
  }
6
7
  const note = document.getElementById(uid);
8
  note.parentNode.removeChild(note);
9
  
10
  notesData = notesData.filter((note) => {
11
    return note.uid != uid;
12
  });
13
  localStorage.setItem("notes", JSON.stringify(notesData));
14
};

One of the benefits of building a web application is that we can take advantage of existing browser features. This function uses the confirm() method to display an inbuilt modal that can handle confirming user input, without the need to built a custom modal and detect a response ourselves.

Conclusion

And that’s that! What we’ve just coded was complex enough to be challenging, yet simple enough to understand, and the perfect demonstration of CRUD. We’ve implemented the basic operations of a software application using vanilla JavaScript (well done).



Source link