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:

  1. Event Parameter: The function accepts an event parameter, which is automatically passed by the browser when an event listener triggers this function. This event object contains information about the event, such as which element was clicked.

  2. 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.

  3. Extracting the Filter Value:

    const hrefValue = event.target.getAttribute('href').slice(2);
    

    We retrieve the href attribute of the clicked link using getAttribute('href'). This gives us values like #/, #/active, or #/completed. We then use the slice(2) method to remove the first two characters (#/). This leaves us with an empty string for "All", active for "Active", and completed for "Completed".

  4. Updating the Filter State:

    filter = hrefValue === '' ? 'all' : hrefValue;
    

    We then update the global filter variable based on the extracted hrefValue. If the hrefValue is an empty string (which corresponds to the "All" filter), we set filter to 'all'. Otherwise, we use the hrefValue directly.

  5. 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.