Welcome back to our JavaScript series! In this 81st installment, we're diving into a fundamental web development project: building a fully functional ToDo application. But we won't just build a simple one; we'll equip it with persistence using localStorage, ensuring your tasks stick around even after you close your browser tab.
A ToDo app is an excellent project for solidifying your understanding of DOM manipulation, event handling, and client-side data storage. By the end of this guide, you'll have a clean, interactive ToDo list that remembers your entries.
What is localStorage and Why Use It?
In web development, you often need a way to store data on the client side. While cookies are one option, localStorage offers a simpler, more modern approach for storing larger amounts of data (typically 5-10MB, depending on the browser) without expiration.
- Persistence: Data stored in
localStoragepersists even when the browser is closed and reopened. Unlike session storage, it doesn't clear when the session ends. - Simple API: It provides a straightforward key-value storage mechanism.
- Client-Side Only: Data is stored on the user's browser, not on a server. This makes it ideal for user preferences, offline data, or, in our case, a ToDo list.
The primary methods we'll use are:
localStorage.setItem(key, value): Stores a key-value pair. Both key and value must be strings.localStorage.getItem(key): Retrieves the value associated with a given key. Returnsnullif the key doesn't exist.localStorage.removeItem(key): Removes the key-value pair.localStorage.clear(): Removes all key-value pairs for that domain.
Crucially, localStorage only stores strings. To store arrays or objects, we'll need to use JSON.stringify() to convert them to a string before saving, and JSON.parse() to convert them back when retrieving.
Project Setup: HTML, CSS, and JavaScript Files
Let's begin by setting up the basic file structure. Create three files in a new folder:
index.html: Our main HTML structure.style.css: For basic styling.script.js: Where all our JavaScript logic will live.
1. HTML Structure (index.html)
Our HTML will be minimal. We need an input field for new tasks, a button to add them, and an unordered list to display the ToDos.
For your index.html file, place the following content inside the <body> tags, ensuring you link to your style.css in the <head> and your script.js just before the closing </body> tag.
<div class="container">
<h1>My ToDo List</h1>
<div class="input-section">
<input type="text" id="todo-input" placeholder="Add a new task...">
<button id="add-todo-btn">Add ToDo</button>
</div>
<ul id="todo-list">
<!-- ToDos will be dynamically added here -->
</ul>
</div>
Remember to include the standard HTML boilerplate (<!DOCTYPE html>, <html>, <head>, <body>) and link your CSS (`<link rel="stylesheet" href="style.css">`) and JavaScript (`<script src="script.js"></script>`) files in your actual index.html file.
2. Basic Styling (style.css)
Add some basic CSS to make our ToDo app look decent and readable.
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f7f6;
display: flex;
justify-content: center;
align-items: flex-start; /* Changed to flex-start to allow content to grow downwards */
min-height: 100vh;
margin: 0;
padding-top: 50px; /* Add some padding from the top */
}
.container {
background-color: #ffffff;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 25px;
}
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todo-input {
flex-grow: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
#add-todo-btn {
padding: 12px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
#add-todo-btn:hover {
background-color: #0056b3;
}
#todo-list {
list-style: none;
padding: 0;
}
#todo-list li {
background-color: #f9f9f9;
border: 1px solid #eee;
padding: 12px 15px;
margin-bottom: 8px;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s ease;
}
#todo-list li:hover {
background-color: #f0f0f0;
}
#todo-list li.completed {
text-decoration: line-through;
color: #888;
background-color: #e0ffe0; /* Light green for completed tasks */
}
.delete-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background-color 0.3s ease;
}
.delete-btn:hover {
background-color: #c82333;
}
JavaScript Logic (script.js): The Core of Our App
Now for the main event! We'll write the JavaScript to handle adding, displaying, and deleting ToDos, all while integrating localStorage for data persistence.
1. Get DOM Elements and Initialize ToDos Array
First, we select the necessary DOM elements. We'll also define an array to hold our ToDo items and load any existing ToDos from localStorage when the page loads.
// Get DOM elements
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');
const todoList = document.getElementById('todo-list');
// Initialize todos array from localStorage or an empty array
// We store objects like { text: "Task 1", completed: false }
let todos = JSON.parse(localStorage.getItem('todos')) || [];
// Function to save todos to localStorage
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// Function to render todos to the DOM
function renderTodos() {
todoList.innerHTML = ''; // Clear current list
todos.forEach((todoItem, index) => { // todoItem is now an object
const li = document.createElement('li');
li.dataset.index = index;
if (todoItem.completed) {
li.classList.add('completed');
}
const span = document.createElement('span');
span.textContent = todoItem.text;
span.addEventListener('click', () => toggleCompleteTodo(index)); // Toggle completion
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.classList.add('delete-btn');
deleteBtn.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent toggling completion when clicking delete
deleteTodo(index);
});
li.appendChild(span);
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
// Initial render when the page loads
document.addEventListener('DOMContentLoaded', renderTodos);
Notice the use of JSON.parse(localStorage.getItem('todos')) || []. This attempts to retrieve our todos array from localStorage. If it's the first time and nothing is there (getItem returns null), it defaults to an empty array. Also, we've structured our `todos` array to store objects like { text: "Task 1", completed: false } to easily manage the completion status of each task.
2. Adding New ToDos
We'll add an event listener to our "Add ToDo" button. When clicked, it will take the input value, add it to our `todos` array (as an object with completed: false), save the array, and then re-render the list.
// ... (previous code) ...
// Function to add a new todo
function addTodo() {
const todoText = todoInput.value.trim(); // Get value and remove whitespace
if (todoText !== '') {
todos.push({ text: todoText, completed: false }); // Add as an object
saveTodos();
renderTodos();
todoInput.value = ''; // Clear input field
} else {
alert('Please enter a task!');
}
}
// Event listener for the Add ToDo button
addTodoBtn.addEventListener('click', addTodo);
// Allow adding todos with 'Enter' key
todoInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
addTodo();
}
});
// ... (rest of the code) ...
3. Deleting ToDos
For deletion, we add a "Delete" button next to each ToDo item within the renderTodos function. When clicked, we'll find its index, remove it from the `todos` array using splice(), update `localStorage`, and re-render the list.
// ... (previous code) ...
// Function to delete a todo
function deleteTodo(index) {
todos.splice(index, 1); // Remove 1 item at the given index
saveTodos();
renderTodos();
}
// ... (rest of the code) ...
4. Toggling ToDo Completion
To make our ToDo app more functional, we'll add the ability to mark tasks as complete. Clicking on the task text (the <span> element) will toggle its completion status.
// ... (previous code) ...
// Function to toggle todo completion
function toggleCompleteTodo(index) {
todos[index].completed = !todos[index].completed; // Toggle the boolean
saveTodos();
renderTodos();
}
// ... (rest of the code) ...
This function updates the `completed` property of the ToDo object at the specified index. The `renderTodos` function then applies the `completed` CSS class based on this property, triggering our CSS for a strikethrough effect.
Putting It All Together (script.js Full Code)
Here's the complete JavaScript code for your script.js file:
// Get DOM elements
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');
const todoList = document.getElementById('todo-list');
// Initialize todos array from localStorage or an empty array
// We store objects like { text: "Task 1", completed: false }
let todos = JSON.parse(localStorage.getItem('todos')) || [];
// Function to save todos to localStorage
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// Function to render todos to the DOM
function renderTodos() {
todoList.innerHTML = ''; // Clear current list
todos.forEach((todoItem, index) => { // todoItem is now an object
const li = document.createElement('li');
li.dataset.index = index;
if (todoItem.completed) {
li.classList.add('completed');
}
const span = document.createElement('span');
span.textContent = todoItem.text;
span.addEventListener('click', () => toggleCompleteTodo(index)); // Toggle completion
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.classList.add('delete-btn');
deleteBtn.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent toggling completion when clicking delete
deleteTodo(index);
});
li.appendChild(span);
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
// Function to add a new todo
function addTodo() {
const todoText = todoInput.value.trim();
if (todoText !== '') {
todos.push({ text: todoText, completed: false }); // Add as an object
saveTodos();
renderTodos();
todoInput.value = '';
} else {
alert('Please enter a task!');
}
}
// Function to delete a todo
function deleteTodo(index) {
todos.splice(index, 1);
saveTodos();
renderTodos();
}
// Function to toggle todo completion
function toggleCompleteTodo(index) {
todos[index].completed = !todos[index].completed;
saveTodos();
renderTodos();
}
// Event listener for the Add ToDo button
addTodoBtn.addEventListener('click', addTodo);
// Allow adding todos with 'Enter' key
todoInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
addTodo();
}
});
// Initial render when the page loads
document.addEventListener('DOMContentLoaded', renderTodos);
Conclusion
Congratulations! You've successfully built a persistent ToDo application using HTML, CSS, and vanilla JavaScript with localStorage. You now have a solid understanding of:
- Retrieving and manipulating DOM elements.
- Handling user events (clicks, keypresses).
- Storing and retrieving data using
localStorage. - Using
JSON.stringify()andJSON.parse()to work with complex data types inlocalStorage. - Dynamically rendering and updating UI elements.
This project serves as a fantastic foundation. From here, you could explore enhancements like:
- Adding drag-and-drop reordering for tasks.
- Implementing filtering (e.g., "All", "Active", "Completed" tasks).
- Allowing users to edit existing tasks.
- Integrating with a backend API for multi-user support.
Keep experimenting and happy coding!