How to build a Todo app with Typescript
Tags: webdev, typescript, beginners
Published: 2024-07-09 12:47:20

![cover-picture](https://blogimagesbocket.s3.ap-southeast-2.amazonaws.com/Screenshot+2024-07-02+201900.png)

In this blog post, we will build a to-do app using typescript. The app allows users to add, delete, and update tasks, and users can mark completed tasks or unmark them. We will follow the Model, View, Controller (MVC) design pattern. Here is the link for the app [Todo app with typescript](https://6683cca0885c60255f7d5361--beautiful-madeleine-23be54.netlify.app/). We will use `vite` to quickly set up the typescript project. Let's get started!

## Setup typescript project using `vite`

Run the following command in the terminal
```
npm create vite@latest
```
It will ask you to enter the project name. we can say `Typescript-todo-app`. We will be using just typescript in this project so, in `Select a framework` prompt we need to choose `Vanilla`. In the next prompt, we need to choose `TypeScript`. Then, Vite will setup the project for us. To run the project run the following command in the terminal:
```
cd Typescript-todo-app
npm install
npm run dev
```

This will run the typescript-configured project. We can delete all unwanted files. I have used `bootstrap` and `uuid` to generate a unique `id` for each task item. So, let's install these packages and import them into the `main.ts` file
```
npm install bootstrap uuid
```
```
import "bootstrap/dist/css/bootstrap.min.css";
import { v4 as uuid } from "uuid";
```



 Our project files and folders tree looks like the following:
```
Typescript-todo-app
├── public
├── src
│   ├── controller
│   │   └── TaskListController.ts
│   ├── model
│   │   ├── TaskItem.ts
│   │   └── TaskList.ts
│   ├── view
│   │   └── TaskListView.ts
│   └── main.ts
├── index.html
└── tsconfig.json
```

## Creating model for TaskItem and TaskList
### TaskItem
In the `model` folder create a file `TaskItem.ts`. In this file, we will define a model for a single task. A single task item will have the following properties
* Unique `id` for that we will be using `uuid` library
*  `task` actual description of the task itself
*  `completed` a boolean value indicating whether the task is completed or not, which value will be `false` by default and can be changed latter 

```
export interface SingleTask {
  id: string;
  task: string;
  completed: boolean;
}

export default class TaskItem implements SingleTask {
  constructor(
    private _id: string = "",
    private _task: string = "",
    private _completed: boolean = false
  ) {}
  get id(): string {
    return this._id;
  }

  set id(id: string) {
    this._id = id;
  }

  get task(): string {
    return this._task;
  }
  set task(task: string) {
    this._task = task;
  }

  get completed(): boolean {
    return this._completed;
  }
  set completed(completed: boolean) {
    this._completed = completed;
  }
}

```
Here, first, we have defined an `interface` for a `SingleTask`. The interface provides the syntax for classes to follow. Any classes implementing a particular interface have to follow the structure provided by that interface.
Then, we have defined a `class` named `TaskItem` that implements the `SingleTask` interface. This means that the `TaskItem` class has to have all properties defined in the interface.
The constructor allows us to set the value for `id`, `task`, and `completed` when creating a new `TaskItem` instance. Plus, we've also defined getter and setter methods for each property. Getters allow us to retrieve the current value of the property, while setters ensure that any changes to the property are handled appropriately. We will see these in action while editing tasks and toggling task status.

### TaskList
 `TaskList.ts` will be responsible for managing the collection of TaskItem which includes retrieving tasks, adding, deleting, updating tasks, saving collection of tasks in `localStorage`, loading tasks from `localStorage`, and filtering tasks.

 ```
import TaskItem from "./TaskItem";

interface AllTask {
  tasks: TaskItem[];
  load(): void;
  save(): void;
  clearTask(): void;
  addTask(taskObj: TaskItem): void;
  removeTask(id: string): void;
  editTask(id: string, updatedTaskText: string): void;
  toggleTaskChange(id: string): void;
  getTaskToComplete(): TaskItem[];
  getCompletedTask(): TaskItem[];
}

export default class TaskList implements AllTask {
  private _tasks: TaskItem[] = [];

  get tasks(): TaskItem[] {
    return this._tasks;
  }

  load(): void {
    const storedTasks: string | null = localStorage.getItem("myTodo");
    if (!storedTasks) return;

    const parsedTaskList: {
      _id: string;
      _task: string;
      _completed: boolean;
    }[] = JSON.parse(storedTasks);
    parsedTaskList.forEach((taskObj) => {
      const newTaskList = new TaskItem(
        taskObj._id,
        taskObj._task,
        taskObj._completed
      );

      this.addTask(newTaskList);
    });
  }

  save(): void {
    localStorage.setItem("myTodo", JSON.stringify(this._tasks));
  }

  clearTask(): void {
    this._tasks = [];
    localStorage.removeItem("myTodo");
  }

  addTask(taskObj: TaskItem): void {
    this._tasks.push(taskObj);
    this.save();
  }

  removeTask(id: string): void {
    this._tasks = this._tasks.filter((task) => task.id !== id);
    this.save();
  }

  editTask(id: string, updatedTaskText: string): void {
    if (updatedTaskText.trim() === "") return;

    const taskToUpdate = this._tasks.find((task) => task.id === id);
    if (!taskToUpdate) return;
    taskToUpdate.task = updatedTaskText;
    this.save();
  }
  toggleTaskChange(id: string): void {
    const taskToUpdateChange = this._tasks.find((task) => task.id === id);
    if (!taskToUpdateChange) return;
    taskToUpdateChange.completed = !taskToUpdateChange.completed;
    this.save();
  }
  getCompletedTask(): TaskItem[] {
    const completedTask = this._tasks.filter((task) => task.completed);
    return completedTask;
  }

  getTaskToComplete(): TaskItem[] {
    const taskToComplete = this._tasks.filter((task) => !task.completed);
    return taskToComplete;
  }
}

 ```

 In the above code, first, we have imported the `TaskItem` class and then defined an interface `AllTask`. This interface outlines the methods a `TaskList` class should implement to manage tasks.
 Inside the `TaskList` class, we have private property `_tasks` which holds an array of `TaskItem`. 
The `load` method attempts to retrieve a serialized list of tasks from local storage with the key "`myTodo`".
If data is found, it parses the JSON string back into an array of objects with properties matching TaskItem's structure. It then iterates through the parsed data and creates new `TaskItem` objects, adding them to the internal `_tasks` array using the `addTask` method.
The `save` method serializes the current `_tasks` array into a JSON string and stores it in `localStorage` with the key "`myTodo`".

`clearTask`: This method removes all tasks from the internal list and clears the associated data from localStorage.

`addTask`: This method adds a new `TaskItem` object to the beginning of the internal `_tasks` array and calls the save method to persist the change.

`removeTask`: This method takes an `id` as input and filters the `_tasks` array, keeping only tasks where the id property doesn't match the provided id. It then calls save to persist the change.

`editTask`: This method takes an `id` and a `updatedTaskText` as input. It first checks if the `updatedTaskText` is empty (trimmed) and returns if so.
It then finds the task with the matching `id` using the find method. If no task is found, it returns. Finally, it updates the `task` property of the found `task` object with the provided `updatedTaskText` and calls `save` to persist the change.
`toggleTaskChange`: This method takes an id as input. It finds the task with the matching `id` and flips the value of its completed property (i.e., marks it as completed if pending or vice versa). Finally, it calls `save` to persist the change.

`getCompletedTask`: This method returns an array containing only the tasks where the completed property is set to true (completed tasks).

`getTaskToComplete`: This method returns an array containing only the tasks where the completed property is set to false (pending tasks).

## Creating a controller for TaskList
Now, let's create a file named `TaskListController.ts` inside the `controller` folder. This class acts as a bridge between `view` and `modal` as it interacts with `model` based on user interaction through the `view` component which we will create later.

First of all, let's import `TaskItem` and `TaskList` classes from the `model` and define an interface for `TaskListController`.

```
import TaskItem from "../model/TaskItem";
import TaskList from "../model/TaskList";

interface Controller {
  getTaskList(): TaskItem[];
  addTask(newTask: TaskItem): void;
  deleteTask(taskId: string): void;
  editTask(taskId: string, updatedTaskText: string): void;
  loadTask(): void;
  clearTask(): void;
  saveTask(): void;
  toggleTaskChange(taskId: string): void;
  getPendingTask(): TaskItem[];
  getCompletedTask(): TaskItem[];
}


```


Now, create a class `TaskListController` that implements the `Controller` interface. This ensures the class provides all the functionalities defined in the interface.

```

export default class TaskListController implements Controller {
  private _taskList: TaskList = new TaskList();

  constructor() {
    
    this.loadTask();
  }

  getTaskList(): TaskItem[] {
    return this._taskList.tasks;
  }

  addTask(newTask: TaskItem): void {
    this._taskList.addTask(newTask);
  }

  deleteTask(taskId: string): void {
    this._taskList.removeTask(taskId);
  }

  editTask(taskId: string, updatedTaskText: string): void {
    this._taskList.editTask(taskId, updatedTaskText);
  }

  getCompletedTask(): TaskItem[] {
    const completedTask = this._taskList.getCompletedTask();
    return completedTask;
  }

  getPendingTask(): TaskItem[] {
    const pendingTask = this._taskList.getTaskToComplete();
    return pendingTask;
  }

  clearTask(): void {
    this._taskList.clearTask();
  }
  loadTask(): void {
    this._taskList.load();
  }
  saveTask(): void {
    this._taskList.save();
  }

  toggleTaskChange(taskId: string): void {
    this._taskList.toggleTaskChange(taskId);
  }
}

```

Inside the class, a private property `_taskList` is declared and initialized with a new instance of the `TaskList` class. This `_taskList` object handles the actual storage and manipulation of tasks. 
The constructor gets called whenever a new `TaskListController` object is created.
Inside the constructor, it calls the `loadTask` method, to retrieve any previously saved tasks from persistent storage and populate the internal `_taskList` object.

The class defines several methods each of these methods simply calls the corresponding method on the internal `_taskList` object. For instance, `getTaskList` calls t`his._taskList`.tasks to retrieve the task list, `addTask` calls `this._taskList`.
By delegating the work to the `_taskList` object, the TaskListController acts as a facade, providing a convenient interface for interacting with the task management functionalities.

## Creating a view for TaskList
 Let's create a file named `TaskListView.ts` inside a folder `view`.  We will create a class `HTMLTaskListView`, which is responsible for rendering the tasks on the web page and managing user interactions. First, let's create an interface for this class.
 ```
import TaskItem from "../model/TaskItem";
import TaskListController from "../controller/TaskListController";

interface DOMList {
  clear(): void;
  render(allTask: TaskItem[]): void;
}
 ```
 In the above interface `DOMList`, we only have two methods. These two methods will be public and are responsible for rendering tasks and clearing all tasks.
Now let's look at `HTMLTaskListView` class.
 ```

export default class HTMLTaskListView implements DOMList {
  private ul: HTMLUListElement;
  private taskListController: TaskListController;
  constructor(taskListController: TaskListController) {
    this.ul = document.getElementById("taskList") as HTMLUListElement;
    this.taskListController = taskListController;

    if (!this.ul)
      throw new Error("Could not find html ul element in html document.");
  }

  clear(): void {
    this.ul.innerHTML = "";
  }

  private createTaskListElement(task: TaskItem): HTMLLIElement {
    const li = document.createElement("li") as HTMLLIElement;
    li.className = "list-group-item d-flex gap-3 align-items-center";
    li.dataset.taskId = task.id;

    const checkBox = this.createCheckBox(task);
    const label = this.createLabel(task);
    const editTaskInput = this.createEditTaskInput();
    const [saveButton, editButton] = this.createEditAndSaveButton(
      editTaskInput,
      label,
      task
    );
    const deleteButton = this.createDeleteButton(task);

    li.append(
      checkBox,
      editTaskInput,
      label,
      editButton,
      saveButton,
      deleteButton
    );
    return li;
  }

  private createCheckBox(task: TaskItem): HTMLInputElement {
    const checkBox = document.createElement("input") as HTMLInputElement;
    checkBox.type = "checkbox";
    checkBox.checked = task.completed;
    checkBox.addEventListener("change", () => {
      this.taskListController.toggleTaskChange(task.id);
    });
    return checkBox;
  }

  private createEditTaskInput(): HTMLInputElement {
    /// input field to edit task
    const editTaskInput = document.createElement("input") as HTMLInputElement;
    editTaskInput.hidden = true;
    editTaskInput.type = "text";
    editTaskInput.className = "form-control";
    return editTaskInput;
  }

  private createLabel(task: TaskItem): HTMLLabelElement {
    const label = document.createElement("label") as HTMLLabelElement;
    label.htmlFor = task.id;
    label.textContent = task.task;
    return label;
  }

  private createEditAndSaveButton(
    editTaskInput: HTMLInputElement,
    label: HTMLLabelElement,
    task: TaskItem
  ): HTMLButtonElement[] {
    const saveButton = document.createElement("button") as HTMLButtonElement;
    saveButton.hidden = true;
    saveButton.className = "btn btn-warning btn-sm";
    saveButton.textContent = "Save";

    const editButton = document.createElement("button") as HTMLButtonElement;
    editButton.className = "btn btn-success btn-sm";
    editButton.textContent = "Edit";

    saveButton.addEventListener("click", () => {
      const updatedTaskText = editTaskInput.value;
      task.task = updatedTaskText;
      this.taskListController.editTask(task.id, updatedTaskText);
      saveButton.hidden = true;
      editButton.hidden = false;
      editTaskInput.hidden = true;
      this.render(this.taskListController.getTaskList());
    });

    editButton.addEventListener("click", () => {
      saveButton.hidden = false;
      editTaskInput.hidden = false;
      editTaskInput.value = task.task;
      label.innerText = "";
      editButton.hidden = true;
    });

    return [saveButton, editButton];
  }

  private createDeleteButton(task: TaskItem): HTMLButtonElement {
    const deleteButton = document.createElement("button") as HTMLButtonElement;
    deleteButton.className = "btn btn-primary btn-sm";
    deleteButton.textContent = "Delete";

    deleteButton.addEventListener("click", () => {
      this.taskListController.deleteTask(task.id);
      this.render(this.taskListController.getTaskList());
    });
    return deleteButton;
  }

  render(allTask: TaskItem[]): void {
    this.clear();

    allTask.forEach((task) => {
      const li = this.createTaskListElement(task);
      this.ul.append(li);
    });
  }
}


 ```

### Constructor and Initialization
The constructor initializes the HTMLTaskListView class. It sets up essential properties and ensures that the necessary DOM elements are available.
```
constructor(taskListController: TaskListController) {
  this.ul = document.getElementById("taskList") as HTMLUListElement;
  this.taskListController = taskListController;

  if (!this.ul)
    throw new Error("Could not find html ul element in html document.");
}
```
The properties `taskListController` is an instance of `TaskListController` that manager the tasks and `ul` is a reference to the HTML unordered list element where tasks will be displayed.

### Clearing the TaskList
The `clear` method removes all child elements from the ul element, effectively clearing the task list displayed on the webpage.
```
clear(): void {
  this.ul.innerHTML = "";
}
```

### Creating Task List Elements


* `createTaskListElement(task: TaskItem): HTMLLIElement` creates an individual task item element (li) and appends various components such as checkboxes, labels, edit inputs, and buttons and we have methods for each of these components.

* `createEditTaskInput(): HTMLInputElement` generates an input field used for editing the task description, initially hides the input field, and will show when the user clicks on `Edit` button.
* `createLabel(task: TaskItem): HTMLLabelElement` creates a label element to display the task description.
* `createEditAndSaveButton(editTaskInput: HTMLInputElement, label: HTMLLabelElement, task: TaskItem): HTMLButtonElement[]` generates buttons for editing and saving task descriptions and manages their visibility and actions.
* `createDeleteButton(task: TaskItem): HTMLButtonElement` creates a delete button and when clicked removes tasks from the list.
  ### Rendering the Task List
`render` method takes an array of `TaskItem` representing all tasks to be displayed, clears the current task list, and populates it with the provided tasks.


## Creating an add task form
So far, we have created a Model, View, and Controller for our Todo app. Now, it's time to look into `index.html` page. Where we will create a form where users can write a task and add it to our to-do app plus we will create buttons to show completed tasks, tasks to complete, and clear all tasks.
```
<body>
    <div class="container" style="max-width: 800px;">
      
<h2>Todo list app with TypeScript</h2>
<form class="m-3" id="todo-form">
  <div class="form-group p-2 d-flex">
    <input name="new-todo" class="form-control" type="text" id="new-todo" placeholder="Add new todo"/>
    <button type="submit" class="btn btn-primary mx-2">Add</button>
  </div>
</form>


<section  style="max-width: 600px;">


  <div class="btn-group my-2" role="group" aria-label="Basic mixed styles example">
    <button type="button" id="all-task" class="btn btn-danger">All Tasks</button>
    <button type="button" id="completed-task" class="btn btn-warning">Completed Task</button>
    <button type="button" id="task-to-complete" class="btn btn-success">Task To Complete</button>
    <buttonc type="button" id="clear-btn" class="btn btn-secondary">Clear All</button>
  </div>

<ul id="taskList" class="list-group">

</ul>

</section>

    </div>
  
  </body>

```


![todo-app](https://blogimagesbocket.s3.ap-southeast-2.amazonaws.com/Screenshot+2024-07-09+211216.png)


## Connecting Model, View, Controller
In, MVC architecture, View captures user interaction such as new task is added, the delete button is clicked, and sends them to the Controller. Then, the Controller acts as an intermediary between the Model and the View. It updates the Model such as adding a new task to the task list, deleting a task from the list, or updating a single task. We have already created a Model, View, and Controller. Now let's connect them together.
In `main.ts` file let's import the required classes and initialize Controller and View.
### Importing and Initializing the Controller and View
```
import TaskItem from "./model/TaskItem";
import TaskListController from "./controller/TaskListController";
import HTMLTaskListView from "./view/TaskListView";

const taskListController = new TaskListController();
const taskListView = new HTMLTaskListView(taskListController);
```

* `taskListController`: An instance of `TaskListController` that manages the task data and business logic.

* `taskListView`: An instance of `HTMLTaskListView` that takes the taskListController as an argument. This view will handle rendering tasks on the webpage.

### Accessing DOM Elements

```
const todoForm = document.getElementById("todo-form") as HTMLFormElement;
const clearBtn = document.getElementById("clear-btn") as HTMLButtonElement;
const showCompletedTask = document.getElementById("completed-task") as HTMLButtonElement;
const showTaskToComplete = document.getElementById("task-to-complete") as HTMLButtonElement;
const showAllTask = document.getElementById("all-task") as HTMLButtonElement;
```
* `todoForm`: The form where new tasks are added.
* `clearBtn`: The button to clear all tasks.
* `showCompletedTask`: The button to filter and display completed tasks.
* `showTaskToComplete`: The button to filter and display pending tasks.
* `showAllTask`: The button to display all tasks.
  
### Initializing the Application
```
const initApp = () => {
  const allTask = taskListController.getTaskList();
  taskListView.render(allTask);
};

initApp()
```
* `initApp`: A function that initializes the application by fetching all tasks from the controller and rendering them using the view. We have called the `initApp` function right away to ensure that the task list is rendered when the application loads.

### Adding a New Task
```
if (todoForm) {
  todoForm.addEventListener("submit", (e) => {
    e.preventDefault();
    const formData = new FormData(todoForm);
    const todoValue = formData.get("new-todo") as string;
    if (todoValue === null || todoValue?.toString().trim() === "") return;
    const newTask = new TaskItem(uuid(), todoValue.trim());

    taskListController.addTask(newTask);

    initApp();

    todoForm.reset();
  });
}

```

On submission, `todoForm` will do the following:
1. Prevent the default form submission behavior.
2. Extract the new task description from the form.
3. Validate the task description (non-empty and non-null).
4. Create a new TaskItem with a unique ID and the trimmed task description.
5. Add the new task to the controller.
6. Reinitialize the application to render the updated task list.
7. Reset the form.

### Clearing All Tasks
```
clearBtn.addEventListener("click", () => {
  taskListController.clearTask();
  taskListView.clear();
});
```

### Showing Completed Tasks
```
showCompletedTask.addEventListener("click", () => {
  const completedTask = taskListController.getCompletedTask();
  taskListView.render(completedTask);
});
```

### Showing Tasks to Complete
```
showTaskToComplete.addEventListener("click", () => {
  const taskToComplete = taskListController.getPendingTask();
  taskListView.render(taskToComplete);
});

```

## Conclusion
In Conclusion, our todo application built using the MVC architecture demonstrates a robust and maintainable structure for managing tasks. By breaking down the responsibilities into distinct components—Model, View, and Controller—the application achieves a clear separation of concerns, which enhances both scalability and maintainability.