Skip to content

Práctica TODO APP

⭐ Videos Premium ⭐

Esta sección es parte del curso en Udemy. Si quieres acceder a ella, puedes comprar el curso en Udemy: Curso de TypeScript.

Conceptos

  • crypto.randomUUID() es un método incorporado en Node.js que genera un UUID (Universally Unique Identifier) versión 4 de forma segura utilizando el generador criptográfico de números aleatorios del sistema.

Práctica

Vamos a crear una aplicación de tareas pendientes (Todo App) utilizando TypeScript, Vite y Bootstrap. Esta aplicación permitirá a los usuarios añadir y eliminar tareas.

index.html

html
<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <link
      rel="icon"
      type="image/svg+xml"
      href="/vite.svg"
    />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>Todo App</title>
  </head>
  <body>
    <div class="container mx-auto mt-5">
      <div class="row justify-content-center">
        <header class="col-12 col-md-8">
          <h1 class="text-center mb-4">Todo App</h1>
        </header>
        <section class="card">
          <div class="card-body">
            <!-- Formulario para añadir tareas -->
            <form
              id="todo-form"
              class="mt-3"
            >
              <div class="input-group mb-3">
                <input
                  type="text"
                  class="form-control"
                  id="todo-input"
                  placeholder="Añadir nueva tarea"
                  required
                />
                <button
                  class="btn btn-primary"
                  type="submit"
                >
                  Añadir
                </button>
              </div>
            </form>

            <!-- Listado de tareas: -->
            <ul
              id="todo-list"
              class="list-group mt-3"
            >
              <!-- Las tareas se añadirán aquí dinámicamente -->
              <template id="todo-item-template">
                <li
                  class="list-group-item d-flex justify-content-between align-items-center"
                  role="button"
                >
                  <span class="todo-text">Ejemplo de tarea</span>
                  <button class="btn btn-danger btn-sm">Eliminar</button>
                </li>
              </template>
            </ul>
          </div>
        </section>
      </div>
    </div>
    <script
      type="module"
      src="/src/main.ts"
    ></script>
  </body>
</html>

src\main.ts

ts
import "bootstrap/dist/css/bootstrap.min.css";
import "./style.css";

document.addEventListener("DOMContentLoaded", () => {
  // Referencia a los elementos del DOM
  const $todoForm = document.querySelector<HTMLFormElement>("#todo-form");
  const $todoInput = document.querySelector<HTMLInputElement>("#todo-input");
  const $todoList = document.querySelector<HTMLUListElement>("#todo-list");

  // Verificar si los elementos existen
  if (!$todoForm || !$todoInput || !$todoList) {
    console.error("Uno o más elementos del DOM no se encontraron");
    return;
  }
}

todo.service.ts

src\services\todo.service.ts

ts
export interface Todo {
  id: string;
  description: string;
  done: boolean;
}

// estado será un array de objetos Todo
let state: Todo[] = [];

// Inicializamos el estado cargando los todos desde el localStorage
const initStore = () => {
  loadTodosFromLocalStorage();
};

// Cargar los todos desde el localStorage al iniciar la aplicación
const loadTodosFromLocalStorage = () => {
  const todos = localStorage.getItem("todos");
  if (todos) {
    state = (JSON.parse(todos) as Todo[]) || [];
  }
};

// Guardar los todos en el localStorage cada vez que se actualice el estado
const saveTodosToLocalStorage = () => {
  localStorage.setItem("todos", JSON.stringify(state));
};

const getTodos = (): Todo[] => {
  // devolvemos una copia del estado actual para evitar mutaciones accidentalmente del estado.
  return [...state];
};

const addTodo = (description: string) => {
  if (!description.trim()) {
    throw new Error("La descripción no puede estar vacía");
  }

  const newTodo: Todo = {
    id: crypto.randomUUID(), // API web estándar window.crypto.randomUUID()
    description,
    done: false,
  };

  // Añadimos el nuevo todo al estado
  state = [...state, newTodo];

  // Guardamos los cambios en el localStorage
  saveTodosToLocalStorage();
};

// Marcar una tarea como completada o pendiente
const toggleTodo = (id: string) => {
  state = state.map((todo) => {
    if (todo.id === id) {
      return { ...todo, done: !todo.done };
    }
    return todo;
  });

  // Guardamos los cambios en el localStorage
  saveTodosToLocalStorage();
};

// Eliminar una tarea
const deleteTodo = (id: string) => {
  state = state.filter((todo) => todo.id !== id);
  // Guardamos los cambios en el localStorage
  saveTodosToLocalStorage();
};

export const todoService = {
  initStore,
  getTodos,
  addTodo,
  toggleTodo,
  deleteTodo,
};

UI

src\ui\render-todos.ts

ts
import type { Todo } from "../services/todo.service";

const $todoTemplate = document.querySelector<HTMLTemplateElement>(
  "#todo-item-template"
);

// Función para crear el HTML de un todo utilizando un template
const createTodoHTML = (todo: Todo) => {
  if (!$todoTemplate) {
    throw new Error("Template not found");
  }

  const $todoElement = $todoTemplate.content.cloneNode(true) as HTMLElement;
  const $li = $todoElement.querySelector("li");
  const $span = $todoElement.querySelector("span");

  if (!$li || !$span) {
    throw new Error("Template structure is incorrect");
  }

  $li.setAttribute("data-id", todo.id);
  $span.textContent = todo.description;

  if (todo.done) {
    $span.classList.add("text-decoration-line-through");
  }

  // retornar el elemento li con el todo
  return $li;
};

// Función para renderizar la lista de todos
export const renderTodos = ($todoList: HTMLUListElement, todos: Todo[]) => {
  // Limpiar la lista actual
  $todoList.innerHTML = "";

  // Iterar sobre los todos y crear el HTML para cada uno
  todos.forEach((todo) => {
    const $todoItem = createTodoHTML(todo);
    $todoList.appendChild($todoItem);
  });
};

src\main.ts

ts
import "bootstrap/dist/css/bootstrap.min.css";
import { todoService } from "./services/todo.service";
import "./style.css";
import { renderTodos } from "./ui/render-todos";

document.addEventListener("DOMContentLoaded", () => {
  // Referencia a los elementos del DOM
  const $todoForm = document.querySelector<HTMLFormElement>("#todo-form");
  const $todoInput = document.querySelector<HTMLInputElement>("#todo-input");
  const $todoList = document.querySelector<HTMLUListElement>("#todo-list");

  // Verificar si los elementos existen
  if (!$todoForm || !$todoInput || !$todoList) {
    console.error("Uno o más elementos del DOM no se encontraron");
    return;
  }

  // Inicializar el servicio de todos
  todoService.initStore();

  // función para mostrar las tareas en el DOM
  const displayTodos = () => {
    const todos = todoService.getTodos();
    renderTodos($todoList, todos);
  };

  // Mostrar los todos al cargar la página
  displayTodos();

  // Manejar el envío del formulario
  $todoForm.addEventListener("submit", (event) => {
    event.preventDefault();
    const description = $todoInput.value.trim() || "";

    if (description) {
      try {
        todoService.addTodo(description);
        $todoInput.value = ""; // Limpiar el input
        displayTodos(); // Actualizar la lista de todos
      } catch (error) {
        console.error("Error al añadir el todo:", error);
      }
    }
  });

  // Manejar los clics en la lista de todos (delegación de eventos)
  $todoList.addEventListener("click", (event) => {
    const target = event.target as HTMLElement;
    const todoId = target.closest("li")?.getAttribute("data-id");

    if (!todoId) {
      return; // Si no se hizo clic en un elemento de todo, salir
    }

    // Si se hizo clic en el botón de borrar
    if (target.tagName === "BUTTON") {
      todoService.deleteTodo(todoId);
      displayTodos(); // Actualizar la lista de todos
      return;
    }

    // Si se hizo clic en cualquier parte del todo, alternar su estado
    todoService.toggleTodo(todoId);
    displayTodos(); // Actualizar la lista de todos
  });
});

init-todo-app.ts

Puedes crear un archivo init-todo-app.ts para inicializar la aplicación. Esto te permitirá separar la lógica de inicialización de la lógica de la aplicación, lo que es una buena práctica.

ts
import { todoService } from "../services/todo.service";
import { renderTodos } from "../ui/render-todos";

export const initTodoApp = () => {
  // Referencia a los elementos del DOM
  const $todoForm = document.querySelector<HTMLFormElement>("#todo-form");
  const $todoInput = document.querySelector<HTMLInputElement>("#todo-input");
  const $todoList = document.querySelector<HTMLUListElement>("#todo-list");

  // Verificar si los elementos existen
  if (!$todoForm || !$todoInput || !$todoList) {
    console.error("Uno o más elementos del DOM no se encontraron");
    return;
  }

  // Inicializar el servicio de todos
  todoService.initStore();

  // función para mostrar las tareas en el DOM
  const displayTodos = () => {
    const todos = todoService.getTodos();
    renderTodos($todoList, todos);
  };

  // Mostrar los todos al cargar la página
  displayTodos();

  // Manejar el envío del formulario
  $todoForm.addEventListener("submit", (event) => {
    event.preventDefault();
    const description = $todoInput.value.trim() || "";

    if (description) {
      try {
        todoService.addTodo(description);
        $todoInput.value = ""; // Limpiar el input
        displayTodos(); // Actualizar la lista de todos
      } catch (error) {
        console.error("Error al añadir el todo:", error);
      }
    }
  });

  // Manejar los clics en la lista de todos (delegación de eventos)
  $todoList.addEventListener("click", (event) => {
    const target = event.target as HTMLElement;
    const todoId = target.closest("li")?.getAttribute("data-id");

    if (!todoId) {
      return; // Si no se hizo clic en un elemento de todo, salir
    }

    // Si se hizo clic en el botón de borrar
    if (target.tagName === "BUTTON") {
      todoService.deleteTodo(todoId);
      displayTodos(); // Actualizar la lista de todos
      return;
    }

    // Si se hizo clic en cualquier parte del todo, alternar su estado
    todoService.toggleTodo(todoId);
    displayTodos(); // Actualizar la lista de todos
  });
};