[Simple Web Apps] 3. Building a simple todo list

In this article, let's see how we can implement a basic version of a ToDo list using HTML, CSS, and Typescript.

This is going to be something like the below:

<html>
  <head>
    <title>Basic To Do application</title>
    <meta charset="UTF-8" />
    <link href="styles/styles.css" rel="stylesheet" type="text/css" />
    <script src="scripts/index.js" async></script>
  </head>
  <body>
    <div id="todoContainer">
      <input
        type="text"
        placeholder="eg.) Pickup Milk"
        value=""
        id="todoSearch"
        class="todo-search"
      />
      <div id="itemsContainer"></div>
    </div>
  </body>
</html>
html,
body {
  font-size: 16px;
}

#todoContainer {
  display: flex;
  flex: 1;
  flex-direction: column;
}

#todoContainer #todoSearch {
  display: flex;
  flex: 1;
  padding: 0.75rem;
  font-size: 2rem;
  outline: none;
}

#todoContainer #itemsContainer {
  display: flex;
  flex: 1;
  margin-top: 0.5rem;
  flex-direction: column;
}

.todo-checkbox {
  width: 2rem;
}

.todo-item {
  display: flex;
  padding: 1rem;
  background: grey;
  margin-bottom: 0.5rem;
}

.todo-title {
  font-size: 2rem;
  display: inline-flex;
}

.todo-title.done {
  text-decoration: line-through;
}
export enum TodoItemStatus {
  NotStarted,
  Done
}

export interface ITodoItem {
  title: string;
}

export interface ITodoItemInternal extends ITodoItem {
  id?: string;
  dateAdded: number; // For simplicity, we are not considering TimeZones
  status?: TodoItemStatus;
}

class TodoList {
  container: HTMLElement;
  itemsMap: Record<string, ITodoItemInternal> = {};
  todoItemsContainer: HTMLDivElement | undefined;
  inputElement: HTMLInputElement;
  constructor(inputElement: HTMLInputElement, container: HTMLElement) {
    this.inputElement = inputElement;
    this.container = container;
    this.init();
  }

  async init() {
    this.handleEvents();
    this.itemsMap = await this.getExistingItems();
  }

  handleEvents() {
    this.inputElement.addEventListener("keyup", (event) => {
      if (event.key === "Enter") {
        const inputBox: HTMLInputElement = event.target as HTMLInputElement;
        this.addItem({
          title: inputBox.value
        });
        inputBox.value = ""; // resets the input box.
      }
    });
    this.container.addEventListener("change", (event: any) => {
      if (event.target.classList.contains("todo-checkbox")) {
        this.completeItem(event.target.parentNode.id);
      }
    });
  }

  async getExistingItems(): Promise<Record<string, ITodoItemInternal>> {
    // This is where:
    // 1. you can make server calls to get the stored items
    // 2. (or) you can get from local storage,
    // 3. (or) from any other memory layer
    return Promise.resolve({}); // Since, I am using in-memory, this is going to be empty
    // on every load.
  }

  getUniqueIdentifier() {
    // Ideally use a uuid library to return the unique identifier
    // For simplicity, using Date.now() as the id.
    return Date.now();
  }

  addItem(item: ITodoItem): void {
    const id = this.getUniqueIdentifier();
    const internalItem = {
      ...item,
      status: TodoItemStatus.NotStarted,
      dateAdded: id,
      id: id.toString()
    };
    this.itemsMap[id] = internalItem;
    this.renderInView(internalItem);
  }

  renderInView(internalItem: ITodoItemInternal) {
    this.container.appendChild(this.getTodoItemElement(internalItem));
  }

  getCheckboxItem(checked: boolean = false): HTMLInputElement {
    const todoCheckbox: HTMLInputElement = document.createElement("input");
    todoCheckbox.type = "checkbox";
    todoCheckbox.checked = checked;
    todoCheckbox.classList.add("todo-checkbox");
    return todoCheckbox;
  }

  getTitleItem(title: string, checked: boolean = false): HTMLSpanElement {
    const todoTitle: HTMLSpanElement = document.createElement("span");
    todoTitle.classList.add("todo-title");
    todoTitle.innerHTML = title;
    if (checked) {
      todoTitle.classList.add("done");
    }
    return todoTitle;
  }

  getTodoItemElement(internalItem: ITodoItemInternal): HTMLDivElement {
    console.log(internalItem);
    const todoItem: HTMLDivElement = document.createElement("div");
    const checked = internalItem.status === TodoItemStatus.Done;
    todoItem.id = internalItem.id as string;
    todoItem.classList.add("todo-item");
    todoItem.appendChild(this.getCheckboxItem(checked));
    todoItem.appendChild(this.getTitleItem(internalItem.title, checked));
    return todoItem;
  }

  completeItem(id: string) {
    console.log("Here", id);
    this.itemsMap[id].status =
      this.itemsMap[id].status === TodoItemStatus.Done
        ? TodoItemStatus.NotStarted
        : TodoItemStatus.Done;
    this.refreshView(id);
  }

  refreshView(id: string) {
    (document.getElementById(id) as HTMLDivElement).replaceWith(
      this.getTodoItemElement(this.itemsMap[id])
    );
  }
}

new TodoList(
  document.getElementById("todoSearch") as HTMLInputElement,
  document.getElementById("itemsContainer") as HTMLDivElement
);

You can run the application live in this codesandbox.

If you like this article, make sure to check out my other articles here. I make sure to post a new article every day. Also, make sure to sign up for the newsletter to directly receive the new articles in your inbox.

Cheers,

Arunkumar Sri Sailapathi.

#2Articles1Week