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. 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
idfor that we will be usinguuidlibrary -
taskactual description of the task itself -
completeda boolean value indicating whether the task is completed or not, which value will befalseby 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 this._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): HTMLLIElementcreates 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(): HTMLInputElementgenerates an input field used for editing the task description, initially hides the input field, and will show when the user clicks onEditbutton.createLabel(task: TaskItem): HTMLLabelElementcreates 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): HTMLButtonElementcreates a delete button and when clicked removes tasks from the list.Rendering the Task List
rendermethod takes an array ofTaskItemrepresenting 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>
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 ofTaskListControllerthat manages the task data and business logic.taskListView: An instance ofHTMLTaskListViewthat 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 theinitAppfunction 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:
- Prevent the default form submission behavior.
- Extract the new task description from the form.
- Validate the task description (non-empty and non-null).
- Create a new TaskItem with a unique ID and the trimmed task description.
- Add the new task to the controller.
- Reinitialize the application to render the updated task list.
- 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.


Top comments (0)