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

  1. 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.

✓ Resposta: Para adicionar edição, você pode modificar a função 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();
        });
    }
});
  • Implemente um filtro para mostrar apenas tarefas pendentes ou concluídas.
  • ✓ Resposta: Adicione três botões: "Todas", "Ativas" e "Concluídas". Cada botão aplica uma classe no contêiner da lista ou esconde os itens que não correspondem. Exemplo:
    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'));
  • Adicione um contador que exiba quantas tarefas estão pendentes.
  • ✓ Resposta: Crie um elemento <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.
  • Modifique o código para que as tarefas sejam salvas com um timestamp e exiba a data de criação.
  • ✓ Resposta: Ao adicionar uma tarefa, armazene a data atual. No 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.
  • Crie uma função para limpar todas as tarefas concluídas de uma vez.
  • ✓ Resposta: Adicione um botão "Limpar concluídas" e um evento que remove todos os itens com a classe completed. Exemplo:
    document.getElementById('clear-completed').addEventListener('click', () => {
        document.querySelectorAll('.task-item.completed').forEach(li => li.remove());
        saveTasks();
    });