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
id
for that we will be usinguuid
library -
task
actual description of the task itself -
completed
a boolean value indicating whether the task is completed or not, which value will befalse
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 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): 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 onEdit
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 ofTaskItem
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>
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 ofTaskListController
that manages the task data and business logic.taskListView
: An instance ofHTMLTaskListView
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 theinitApp
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:
- 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)