Task 3: Structured Programming
In this task, we will focus on structured programming. Structured programming is a programming paradigm that emphasizes the use of clear and concise code structures to make code more readable and maintainable. We will explore the principles of structured programming and apply them to our ToDo app code.
Step 1: Initialize the State
Before we start manipulating the DOM, we need to initialize the state of our application. For this version, we'll use an array to store our todos and a few variables to manage the app's state.
In main.js
:
// Array to store our todos
const todos = [
{ id: 1, text: 'Buy milk', completed: false },
{ id: 2, text: 'Buy bread', completed: false }
];
let nextTodoId = 3;
let filter = 'all'; // can be 'all', 'active', or 'completed'
Step 2: Render the Todos
We'll create a function to render the todos based on the current state.
// Function to render the todos based on the current filter
function renderTodos() {
const todoListElement = document.getElementById('todo-list');
todoListElement.innerHTML = ''; // clear the current list
let filteredTodos = [];
for (let i = 0; i < todos.length; i++) {
if (filter === 'all') {
filteredTodos.push(todos[i]);
} else if (filter === 'active' && !todos[i].completed) {
filteredTodos.push(todos[i]);
} else if (filter === 'completed' && todos[i].completed) {
filteredTodos.push(todos[i]);
}
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
const todoItem = document.createElement('div');
todoItem.classList.add('p-4', 'todo-item');
const todoText = document.createElement('div');
todoText.classList.add('todo-text');
todoText.textContent = filteredTodos[i].text;
if (filteredTodos[i].completed) {
todoText.style.textDecoration = 'line-through';
}
const todoEdit = document.createElement('input');
todoEdit.classList.add('hidden', 'todo-edit');
todoEdit.value = filteredTodos[i].text;
todoItem.appendChild(todoText);
todoItem.appendChild(todoEdit);
todoListElement.appendChild(todoItem);
}
}
In the following steps, we will add event listeners to handle user interactions
Step 3: Initialize the App on Page Load
Instead of directly calling the renderTodos
function, we'll bind it to an event listener that triggers when the entire content of the page is loaded.
// Event listener to initialize the app after the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', renderTodos);
This approach ensures that our app initializes only after all the DOM elements are available. This concept will be especially beneficial when you delve into frameworks like React, where understanding component lifecycle methods and their relation to the DOM is crucial.
Feel free to change the state and view the changes in the app!
// Array to store our todos
const todos = [
{ id: 1, text: "Buy milk", completed: false },
{ id: 2, text: "Buy bread", completed: false },
+ { id: 3, text: "Buy jam", completed: true },
];
- let nextTodoId = 3;
+ let nextTodoId = 4;
let filter = "all"; // can be 'all', 'active', or 'completed'
Step 4: Handle Adding a new todo
When the user types a new todo and presses Enter, we'll add it to the list.
// Function to handle adding a new todo
function handleNewTodoKeyDown(event) {
if (event.key === 'Enter' && this.value.trim() !== '') {
todos.push({ id: nextTodoId++, text: this.value, completed: false });
this.value = ''; // clear the input
renderTodos();
}
}
const newTodoElement = document.getElementById('new-todo');
newTodoElement.addEventListener('keydown', handleNewTodoKeyDown);
The event.key === 'Enter'
checks if the key pressed by the user is the 'Enter' key. The this.value.trim() !== ''
ensures that the input field is not empty or just whitespace. The trim()
method removes any leading or trailing whitespace from the string. So, if after trimming the string is still empty (''
), it means the user hasn't entered any meaningful content.
Aside: The event
object passed to the handleNewTodoKeyDown
function is an object that represents an event that occurred in the browser. In this case, it represents the user pressing a keyboard key while the newTodoElement
input field is in focus. The object contains various properties and methods that allow us to inspect and manipulate the event. In this function, we use the key
property of the event object to check if the key pressed was the 'Enter' key, and the value
property of the newTodoElement
input field to retrieve the text entered by the user.
💡 Note that every time we change the state of the app, we must call
renderTodos
to update the UI. One of the advantages of using a UI library like React is that it takes care of these changes automatically (and efficiently), allowing you to focus on managing the state.
Step 5: Marking a todo as completed
When a todo is clicked, we'll toggle its completed status.
// Function to handle marking a todo as completed
function handleTodoClick(event) {
if (event.target.classList.contains('todo-text')) {
for (let i = 0; i < todos.length; i++) {
if (todos[i].text === event.target.textContent) {
todos[i].completed = !todos[i].completed;
break;
}
}
renderTodos();
}
}
const todoListElement = document.getElementById('todo-list');
todoListElement.addEventListener('click', handleTodoClick);
The event.target.classList.contains('todo-text')
checks if the clicked element (event.target
) has the class todo-text
. This ensures that the subsequent code only runs if the text of a todo item was clicked, and not any other part of the todo or the page.
The loop goes through each todo in our todos
array to find the one that matches the clicked text. Once found, it toggles the completed
status of that todo using !todos[i].completed
, which inverts the boolean value.
Step 6: Filtering todos
When a filter is clicked, we'll update the filter state and re-render the todos.
// Function to handle changing the filter
function handleFilterClick(event) {
if (event.target.tagName === 'A') {
const hrefValue = event.target.getAttribute('href').slice(2);
filter = hrefValue === '' ? 'all' : hrefValue;
renderTodos();
}
}
const todoNavElement = document.getElementById('todo-nav');
todoNavElement.addEventListener('click', handleFilterClick);
Let's break down this function step by step:
-
Event Parameter: The function accepts an
event
parameter, which is automatically passed by the browser when an event listener triggers this function. Thisevent
object contains information about the event, such as which element was clicked. -
Checking the Element Type:
if (event.target.tagName === 'A') {
We first check if the clicked element (
event.target
) is an anchor tag (<a>
). This ensures that the subsequent code only runs if one of our filter links was clicked. -
Extracting the Filter Value:
const hrefValue = event.target.getAttribute('href').slice(2);
We retrieve the
href
attribute of the clicked link usinggetAttribute('href')
. This gives us values like#/
,#/active
, or#/completed
. We then use theslice(2)
method to remove the first two characters (#/
). This leaves us with an empty string for "All",active
for "Active", andcompleted
for "Completed". -
Updating the Filter State:
filter = hrefValue === '' ? 'all' : hrefValue;
We then update the global
filter
variable based on the extractedhrefValue
. If thehrefValue
is an empty string (which corresponds to the "All" filter), we setfilter
to'all'
. Otherwise, we use thehrefValue
directly. -
Re-rendering the Todos:
renderTodos();
Finally, we call the
renderTodos
function to update the displayed todos based on the new filter state.By using this function in conjunction with event delegation on the parent
nav
element, we can efficiently handle filter changes and update the displayed todos accordingly.
Step 7: Putting all together
This is the main.js
file:
import "../style.css";
// Array to store our todos
const todos = [
{ id: 1, text: "Buy milk", completed: false },
{ id: 2, text: "Buy bread", completed: false },
{ id: 3, text: "Buy jam", completed: true },
];
let nextTodoId = 4;
let filter = "all"; // Initial filter setting
// Function to render the todos based on the current filter
function renderTodos() {
const todoListElement = document.getElementById('todo-list');
todoListElement.innerHTML = ''; // Clear the current list to avoid duplicates
let filteredTodos = [];
// Filter todos based on the current filter setting
for (let i = 0; i < todos.length; i++) {
if (filter === 'all') {
filteredTodos.push(todos[i]);
} else if (filter === 'active' && !todos[i].completed) {
filteredTodos.push(todos[i]);
} else if (filter === 'completed' && todos[i].completed) {
filteredTodos.push(todos[i]);
}
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
const todoItem = document.createElement('div');
todoItem.classList.add('p-4', 'todo-item');
const todoText = document.createElement('div');
todoText.classList.add('todo-text');
todoText.textContent = filteredTodos[i].text;
if (filteredTodos[i].completed) {
todoText.style.textDecoration = 'line-through';
}
const todoEdit = document.createElement('input');
todoEdit.classList.add('hidden', 'todo-edit');
todoEdit.value = filteredTodos[i].text;
todoItem.appendChild(todoText);
todoItem.appendChild(todoEdit);
todoListElement.appendChild(todoItem);
}
}
// Function to handle adding a new todo
function handleNewTodoKeyDown(event) {
// Check if the pressed key is 'Enter' and the input is not empty
if (event.key === 'Enter' && this.value.trim() !== '') {
// Add the new todo to the todos array
todos.push({ id: nextTodoId++, text: this.value, completed: false });
this.value = ''; // Clear the input field
renderTodos(); // Re-render the todos
}
}
const newTodoElement = document.getElementById('new-todo');
newTodoElement.addEventListener('keydown', handleNewTodoKeyDown);
// Function to handle marking a todo as completed
function handleTodoClick(event) {
// Check if the clicked element has the class 'todo-text'
if (event.target.classList.contains('todo-text')) {
// Find the clicked todo in the todos array and toggle its completed status
for (let i = 0; i < todos.length; i++) {
if (todos[i].text === event.target.textContent) {
todos[i].completed = !todos[i].completed;
break;
}
}
renderTodos(); // Re-render the todos
}
}
const todoListElement = document.getElementById('todo-list');
todoListElement.addEventListener('click', handleTodoClick);
// Function to handle changing the filter
function handleFilterClick(event) {
// Check if the clicked element is an anchor tag ('A')
if (event.target.tagName === 'A') {
// Extract the filter value from the href attribute
const hrefValue = event.target.getAttribute('href').slice(2);
filter = hrefValue === '' ? 'all' : hrefValue;
renderTodos(); // Re-render the todos based on the new filter
}
}
const todoNavElement = document.getElementById('todo-nav');
todoNavElement.addEventListener('click', handleFilterClick);
// Event listener to initialize the app after the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', renderTodos);
With this implementation, we've set up the basic functionality for our todo app using a structured programming approach. The todos can be added, marked as completed, and filtered. The state of the app is managed using arrays and variables, and the DOM is manipulated directly using methods like document.getElementById()
and element.innerHTML
.
It's worth noting that we haven't yet implemented the behavior of the elements in the todo-actions
section (as well as some of the other features). This was intentional, as we wanted to focus on the core functionality first. As we progress through different programming paradigms, we'll gradually enhance the app's features.
This structured approach is straightforward and easy to understand, especially for beginners. However, as the app grows in complexity, this method can become challenging to manage. In the upcoming versions, we'll refactor the code to make it more modular and maintainable.