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
});
};