reply View all Tutorials & Projects

Build a Notes App with JavaScript and Local Storage (No Frameworks)

In this tutorial you'll be creating a Notes App using only JavaScript and Local Storage. This project is perfect if you've got some JavaScript knowledge under your belt and want to learn new ways to structure an app like this.

Video Tutorial

Source Code

You can find the source code for this video below. Alternatively, browse it on GitHub.

README.md
# notes-app-javascript-localstorage
A Notes App built with vanilla JavaScript and Local Storage.

This is taken from my YouTube video tutorial at:
https://youtu.be/01YKQmia2Jw
css/main.css
html,
body {
    height: 100%;
    margin: 0;
}

.notes {
    display: flex;
    height: 100%;
}

.notes * {
    font-family: sans-serif;
}

.notes__sidebar {
    border-right: 2px solid #dddddd;
    flex-shrink: 0;
    overflow-y: auto;
    padding: 1em;
    width: 300px;
}

.notes__add {
    background: #009578;
    border: none;
    border-radius: 7px;
    color: #ffffff;
    cursor: pointer;
    font-size: 1.25em;
    font-weight: bold;
    margin-bottom: 1em;
    padding: 0.75em 0;
    width: 100%;
}

.notes__add:hover {
    background: #00af8c;
}

.notes__list-item {
    cursor: pointer;
}

.notes__list-item--selected {
    background: #eeeeee;
    border-radius: 7px;
    font-weight: bold;
}

.notes__small-title,
.notes__small-updated {
    padding: 10px;
}

.notes__small-title {
    font-size: 1.2em;
}

.notes__small-body {
    padding: 0 10px;
}

.notes__small-updated {
    color: #aaaaaa;
    font-style: italic;
    text-align: right;
}

.notes__preview {
    display: flex;
    flex-direction: column;
    padding: 2em 3em;
    flex-grow: 1;
}

.notes__title,
.notes__body {
    border: none;
    outline: none;
    width: 100%;
}

.notes__title {
    font-size: 3em;
    font-weight: bold;
}

.notes__body {
    flex-grow: 1;
    font-size: 1.2em;
    line-height: 1.5;
    margin-top: 2em;
    resize: none;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notes App</title>
    <link rel="stylesheet" href="./css/main.css">
</head>
<body>
    <div class="notes" id="app">
        <!-- <div class="notes__sidebar">
            <button class="notes__add" type="button">Add Note</button>
            <div class="notes__list">
                <div class="notes__list-item notes__list-item--selected">
                    <div class="notes__small-title">Lecture Notes</div>
                    <div class="notes__small-body">I learnt nothing today.</div>
                    <div class="notes__small-updated">Thursday 3:30pm</div>
                </div>
            </div>
        </div>
        <div class="notes__preview">
            <input class="notes__title" type="text" placeholder="Enter a title...">
            <textarea class="notes__body">I am the notes body...</textarea>
        </div> -->
    </div>
    <script src="./js/main.js" type="module"></script>
</body>
</html>
js/App.js
import NotesView from "./NotesView.js";
import NotesAPI from "./NotesAPI.js";

export default class App {
    constructor(root) {
        this.notes = [];
        this.activeNote = null;
        this.view = new NotesView(root, this._handlers());

        this._refreshNotes();
    }

    _refreshNotes() {
        const notes = NotesAPI.getAllNotes();

        this._setNotes(notes);

        if (notes.length > 0) {
            this._setActiveNote(notes[0]);
        }
    }

    _setNotes(notes) {
        this.notes = notes;
        this.view.updateNoteList(notes);
        this.view.updateNotePreviewVisibility(notes.length > 0);
    }

    _setActiveNote(note) {
        this.activeNote = note;
        this.view.updateActiveNote(note);
    }

    _handlers() {
        return {
            onNoteSelect: noteId => {
                const selectedNote = this.notes.find(note => note.id == noteId);
                this._setActiveNote(selectedNote);
            },
            onNoteAdd: () => {
                const newNote = {
                    title: "New Note",
                    body: "Take note..."
                };

                NotesAPI.saveNote(newNote);
                this._refreshNotes();
            },
            onNoteEdit: (title, body) => {
                NotesAPI.saveNote({
                    id: this.activeNote.id,
                    title,
                    body
                });

                this._refreshNotes();
            },
            onNoteDelete: noteId => {
                NotesAPI.deleteNote(noteId);
                this._refreshNotes();
            },
        };
    }
}
js/NotesAPI.js
export default class NotesAPI {
    static getAllNotes() {
        const notes = JSON.parse(localStorage.getItem("notesapp-notes") || "[]");

        return notes.sort((a, b) => {
            return new Date(a.updated) > new Date(b.updated) ? -1 : 1;
        });
    }

    static saveNote(noteToSave) {
        const notes = NotesAPI.getAllNotes();
        const existing = notes.find(note => note.id == noteToSave.id);

        // Edit/Update
        if (existing) {
            existing.title = noteToSave.title;
            existing.body = noteToSave.body;
            existing.updated = new Date().toISOString();
        } else {
            noteToSave.id = Math.floor(Math.random() * 1000000);
            noteToSave.updated = new Date().toISOString();
            notes.push(noteToSave);
        }

        localStorage.setItem("notesapp-notes", JSON.stringify(notes));
    }

    static deleteNote(id) {
        const notes = NotesAPI.getAllNotes();
        const newNotes = notes.filter(note => note.id != id);

        localStorage.setItem("notesapp-notes", JSON.stringify(newNotes));
    }
}
js/NotesView.js
export default class NotesView {
    constructor(root, { onNoteSelect, onNoteAdd, onNoteEdit, onNoteDelete } = {}) {
        this.root = root;
        this.onNoteSelect = onNoteSelect;
        this.onNoteAdd = onNoteAdd;
        this.onNoteEdit = onNoteEdit;
        this.onNoteDelete = onNoteDelete;
        this.root.innerHTML = `
            <div class="notes__sidebar">
                <button class="notes__add" type="button">Add Note</button>
                <div class="notes__list"></div>
            </div>
            <div class="notes__preview">
                <input class="notes__title" type="text" placeholder="New Note...">
                <textarea class="notes__body">Take Note...</textarea>
            </div>
        `;

        const btnAddNote = this.root.querySelector(".notes__add");
        const inpTitle = this.root.querySelector(".notes__title");
        const inpBody = this.root.querySelector(".notes__body");

        btnAddNote.addEventListener("click", () => {
            this.onNoteAdd();
        });

        [inpTitle, inpBody].forEach(inputField => {
            inputField.addEventListener("blur", () => {
                const updatedTitle = inpTitle.value.trim();
                const updatedBody = inpBody.value.trim();

                this.onNoteEdit(updatedTitle, updatedBody);
            });
        });

        this.updateNotePreviewVisibility(false);
    }

    _createListItemHTML(id, title, body, updated) {
        const MAX_BODY_LENGTH = 60;

        return `
            <div class="notes__list-item" data-note-id="${id}">
                <div class="notes__small-title">${title}</div>
                <div class="notes__small-body">
                    ${body.substring(0, MAX_BODY_LENGTH)}
                    ${body.length > MAX_BODY_LENGTH ? "..." : ""}
                </div>
                <div class="notes__small-updated">
                    ${updated.toLocaleString(undefined, { dateStyle: "full", timeStyle: "short" })}
                </div>
            </div>
        `;
    }

    updateNoteList(notes) {
        const notesListContainer = this.root.querySelector(".notes__list");

        // Empty list
        notesListContainer.innerHTML = "";

        for (const note of notes) {
            const html = this._createListItemHTML(note.id, note.title, note.body, new Date(note.updated));

            notesListContainer.insertAdjacentHTML("beforeend", html);
        }

        // Add select/delete events for each list item
        notesListContainer.querySelectorAll(".notes__list-item").forEach(noteListItem => {
            noteListItem.addEventListener("click", () => {
                this.onNoteSelect(noteListItem.dataset.noteId);
            });

            noteListItem.addEventListener("dblclick", () => {
                const doDelete = confirm("Are you sure you want to delete this note?");

                if (doDelete) {
                    this.onNoteDelete(noteListItem.dataset.noteId);
                }
            });
        });
    }

    updateActiveNote(note) {
        this.root.querySelector(".notes__title").value = note.title;
        this.root.querySelector(".notes__body").value = note.body;

        this.root.querySelectorAll(".notes__list-item").forEach(noteListItem => {
            noteListItem.classList.remove("notes__list-item--selected");
        });

        this.root.querySelector(`.notes__list-item[data-note-id="${note.id}"]`).classList.add("notes__list-item--selected");
    }

    updateNotePreviewVisibility(visible) {
        this.root.querySelector(".notes__preview").style.visibility = visible ? "visible" : "hidden";
    }
}
js/main.js
import App from "./App.js";

const root = document.getElementById("app");
const app = new App(root);

If you have any questions about this code, please leave a comment on the video.