Projeto: lista de tarefas (To-Do)
LidoNesta aula, você construirá uma aplicação completa de lista de tarefas (To-Do) em JavaScript, aplicando conceitos de manipulação do DOM, eventos, e persistência com localStorage. O projeto será desenvolvido passo a passo, com explicações detalhadas e exemplos práticos.
Nesta aula, vamos construir uma aplicação de lista de tarefas (To-Do) totalmente funcional, utilizando JavaScript puro. Este projeto integra diversos conceitos que você aprendeu ao longo do curso: manipulação do DOM, eventos, armazenamento local e boas práticas de organização de código. Ao final, você terá uma aplicação que permite adicionar, marcar como concluída e remover tarefas, com a persistência dos dados no navegador.
O projeto será desenvolvido em etapas, começando pela estrutura HTML, passando pela lógica de manipulação do DOM, a implementação de eventos e, por fim, a integração com o localStorage. Cada subtópico abordará um aspecto crucial do desenvolvimento, com explicações claras e exemplos práticos.
Estrutura
A estrutura do projeto é composta por três arquivos principais: index.html, style.css e script.js. O HTML define a estrutura da página, o CSS estiliza os elementos e o JavaScript contém toda a lógica da aplicação. Vamos começar pelo HTML.
No arquivo HTML, teremos um contêiner principal com um título, um formulário para adicionar novas tarefas e uma lista não ordenada onde as tarefas serão exibidas. Cada tarefa terá um checkbox para marcar como concluída, um texto descritivo e um botão para excluir.
<!-- index.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lista de Tarefas</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Lista de Tarefas</h1>
<form id="task-form">
<input type="text" id="task-input" placeholder="Digite uma nova tarefa" required>
<button type="submit">Adicionar</button>
</form>
<ul id="task-list"></ul>
</div>
<script src="script.js"></script>
</body>
</html>
A estrutura é simples e semântica. O formulário contém um campo de texto e um botão de envio. A lista vazia será preenchida dinamicamente via JavaScript. No CSS, podemos definir estilos para os itens da lista, como o risco no texto quando a tarefa é concluída.
Manipulação do DOM
A manipulação do DOM é a base do nosso projeto. Precisamos criar elementos dinamicamente, atualizar o conteúdo da lista e remover itens quando necessário. Vamos escrever funções que encapsulam essas operações.
Primeiro, selecionamos os elementos do DOM que iremos manipular: o formulário, o input e a lista. Em seguida, criamos uma função para adicionar uma nova tarefa à lista. Essa função cria um elemento <li>, adiciona um checkbox, um texto e um botão de excluir. Também devemos garantir que o texto da tarefa seja inserido com segurança, usando textContent em vez de innerHTML para evitar problemas de segurança.
// script.js
const form = document.getElementById('task-form');
const input = document.getElementById('task-input');
const taskList = document.getElementById('task-list');
// Função para criar um novo item de tarefa
function addTask(text, completed = false) {
const li = document.createElement('li');
li.className = 'task-item';
if (completed) {
li.classList.add('completed');
}
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = completed;
checkbox.className = 'task-checkbox';
const span = document.createElement('span');
span.textContent = text;
span.className = 'task-text';
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Excluir';
deleteBtn.className = 'delete-btn';
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);
taskList.appendChild(li);
}
Essa função recebe o texto da tarefa e um parâmetro opcional completed para indicar se a tarefa já está concluída. Ela cria os elementos necessários, adiciona classes para estilização e anexa tudo à lista. No próximo subtópico, veremos como integrar essa função com o localStorage.
Persistência com localStorage
O localStorage permite que os dados persistam mesmo após o recarregamento da página. Vamos armazenar a lista de tarefas em um array de objetos, onde cada objeto tem as propriedades text e completed. A cada modificação na lista, salvamos o array no localStorage.
Para carregar as tarefas salvas, usamos uma função que lê o localStorage e, se houver dados, os converte de volta para um array e adiciona cada tarefa à lista. Também devemos remover as tarefas antigas da lista antes de carregar, para evitar duplicatas.
// Função para salvar tarefas no localStorage
function saveTasks() {
const tasks = [];
document.querySelectorAll('.task-item').forEach(item => {
const text = item.querySelector('.task-text').textContent;
const completed = item.querySelector('.task-checkbox').checked;
tasks.push({ text, completed });
});
localStorage.setItem('tasks', JSON.stringify(tasks));
}
// Função para carregar tarefas do localStorage
function loadTasks() {
const storedTasks = localStorage.getItem('tasks');
if (storedTasks) {
const tasks = JSON.parse(storedTasks);
tasks.forEach(task => addTask(task.text, task.completed));
}
}
// Chamar loadTasks ao iniciar
loadTasks();
No código acima, saveTasks percorre todos os itens da lista, extrai o texto e o estado do checkbox, e armazena o array serializado em JSON. loadTasks lê o localStorage e recria as tarefas na lista. É importante chamar loadTasks assim que o script for carregado para restaurar o estado anterior.
Eventos
Os eventos são fundamentais para tornar a aplicação interativa. Precisamos lidar com o envio do formulário (para adicionar tarefas), com a mudança do checkbox (para marcar como concluída) e com o clique no botão de excluir. Vamos utilizar a delegação de eventos para lidar com os cliques nos botões de excluir, pois isso é mais eficiente do que adicionar um listener em cada botão.
No envio do formulário, prevenimos o comportamento padrão de recarregar a página, adicionamos a nova tarefa, limpamos o campo de input e salvamos no localStorage. Para o checkbox, ouvimos o evento change e alternamos a classe completed no item pai. Para o botão de excluir, usamos a delegação de eventos no ul para capturar o clique e remover o item correspondente.
// Evento de envio do formulário
form.addEventListener('submit', function(e) {
e.preventDefault();
const text = input.value.trim();
if (text === '') return;
addTask(text);
input.value = '';
saveTasks();
});
// Delegação de eventos para a lista
// Mudança do checkbox
taskList.addEventListener('change', function(e) {
if (e.target.classList.contains('task-checkbox')) {
const li = e.target.closest('.task-item');
li.classList.toggle('completed', e.target.checked);
saveTasks();
}
});
// Clique no botão de excluir
taskList.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-btn')) {
const li = e.target.closest('.task-item');
li.remove();
saveTasks();
}
});
Com esses eventos, a aplicação se torna totalmente funcional. O formulário adiciona novas tarefas, o checkbox alterna o estado de conclusão e o botão excluir remove a tarefa. Todas as alterações são salvas no localStorage, garantindo a persistência.
Boas Práticas e Observações
Ao desenvolver este projeto, observe algumas boas práticas importantes: usar textContent para inserir texto no DOM, evitar o uso de innerHTML com dados não sanitizados, e sempre chamar saveTasks() após qualquer modificação na lista. Além disso, a delegação de eventos é uma técnica eficiente para lidar com elementos dinâmicos.
Você pode melhorar o projeto adicionando filtros (todas, ativas, concluídas), contador de tarefas pendentes, ou até mesmo animações. A lógica básica permanece a mesma, mas essas funcionalidades extras deixam a aplicação mais completa e profissional.
Referências
Exercícios
- Adicione uma funcionalidade para editar o texto de uma tarefa existente. Dica: use um botão "Editar" que transforma o texto em um campo de input.
addTask para incluir um botão "Editar". Ao clicar, o span é substituído por um input com o valor atual. Ao pressionar Enter ou ao sair do campo, o novo texto é salvo e o span é restaurado. Exemplo:taskList.addEventListener('click', function(e) {
if (e.target.classList.contains('edit-btn')) {
const li = e.target.closest('.task-item');
const span = li.querySelector('.task-text');
const input = document.createElement('input');
input.type = 'text';
input.value = span.textContent;
li.replaceChild(input, span);
input.focus();
input.addEventListener('blur', () => {
const newText = input.value.trim();
if (newText) {
span.textContent = newText;
}
li.replaceChild(span, input);
saveTasks();
});
}
});const filterAll = document.getElementById('filter-all');
const filterActive = document.getElementById('filter-active');
const filterCompleted = document.getElementById('filter-completed');
function applyFilter(filter) {
document.querySelectorAll('.task-item').forEach(li => {
const completed = li.classList.contains('completed');
if (filter === 'all') li.style.display = '';
else if (filter === 'active' && completed) li.style.display = 'none';
else if (filter === 'active') li.style.display = '';
else if (filter === 'completed' && !completed) li.style.display = 'none';
else if (filter === 'completed') li.style.display = '';
});
}
filterAll.addEventListener('click', () => applyFilter('all'));
filterActive.addEventListener('click', () => applyFilter('active'));
filterCompleted.addEventListener('click', () => applyFilter('completed'));<span id="task-count"></span> e atualize-o sempre que a lista mudar. Use uma função updateCount() que conta os itens sem a classe completed. Exemplo:function updateCount() {
const pending = document.querySelectorAll('.task-item:not(.completed)').length;
document.getElementById('task-count').textContent = pending;
}
// Chame updateCount() em todos os lugares onde saveTasks() é chamado.addTask, aceite um parâmetro createdAt e exiba-o no item. Exemplo:function addTask(text, completed = false, createdAt = new Date()) {
// ...
const dateSpan = document.createElement('span');
dateSpan.className = 'task-date';
dateSpan.textContent = createdAt.toLocaleDateString();
li.appendChild(dateSpan);
// ...
}
// Ao salvar, inclua createdAt no objeto.
// Ao carregar, passe createdAt para addTask.completed. Exemplo:document.getElementById('clear-completed').addEventListener('click', () => {
document.querySelectorAll('.task-item.completed').forEach(li => li.remove());
saveTasks();
});