1. Comece com os ativos necessários
Para tornar o layout um pouco mais exclusivo, usaremos algumas ilustrações SVG feitas à mão e uma fonte personalizada tirada de ga-analytics#sendElementsClickEvent”>Envato Elements.
Vale a pena notar que a maioria desses ativos vem de um tutorial anterior. Na verdade, também usaremos muitas das técnicas de posicionamento que aprendemos neste tutorial, então vale a pena lê-lo.
2. Continuar com a marcação da página
Começaremos com um SVG e um div
recipiente:
...
Sprites SVG
Como fizemos muitas vezes no passado, como uma boa prática, armazenaremos todos os SVGs como symbol
s em um contêiner sprite SVG. Então, vamos renderizá-los na tela sempre que precisarmos chamando o use
elemento.
Aqui está a marcação para o sprite SVG:
Observe o preserveAspectRatio="none"
atributo que anexamos à maioria das ilustrações. Fizemos isso porque, como veremos mais adiante, nossos ícones serão dimensionados e perderão suas dimensões iniciais.
Recipiente
O contêiner incluirá um formulário, um div
elemento e uma lista ordenada vazia:
...
Dentro do formulário, teremos uma entrada e um botão de envio junto com seus SVGs associados:
Observe o name
atributo que adicionamos ao campo de entrada. Posteriormente usaremos este atributo para acessar o valor de entrada após o envio do formulário.
Observação: Em nossa demonstração, o autofocus
atributo do campo de texto não funcionará. Na verdade, ele lançará o seguinte erro que você pode ver se abrir o console do navegador:
No entanto, se você executar este aplicativo localmente (não como um projeto Codepen), esse problema não existirá. Como alternativa, você pode definir o foco via JavaScript.
Dentro de div
colocaremos três aninhados div
s e o SVG associado. Nesta seção, acompanharemos o número total de tarefas (remanescentes e concluídas):
Total Tasks: 0Completed Tasks: 0Remaining Tasks: 0
Finalmente, os itens da lista ordenada serão adicionados dinamicamente através do JavaScript.
3. Definir alguns estilos básicos
Com a marcação pronta, vamos continuar com alguns estilos de redefinição:
@font-face { font-family: "Summer"; src: url(SummerFont-Regular.woff); } @font-face { font-family: "Summer Bold"; src: url(SummerFont-Bold.woff); } :root { --white: #fff; } * { padding: 0; margin: 0; border: none; outline: none; box-sizing: border-box; } input, button { font-family: inherit; font-size: 100%; background: none; } [type="checkbox"] { position: absolute; left: -9999px; } button, label { cursor: pointer; } ol { list-style: none; } body { font: 28px/1.2 "Summer"; margin: 1.5rem 0; }
4. Defina os estilos principais
Vamos agora discutir os principais estilos de nosso aplicativo TODO.
Estilos de contêiner
O contêiner terá uma largura máxima com conteúdo centralizado horizontalmente:
.container { max-width: 700px; padding: 0 10px; margin: 0 auto; }
Estilos de formulário
Em telas pequenas, todos os elementos do formulário serão empilhados:
No entanto, em viewports de 600 pixels de largura e acima, o layout do formulário será alterado da seguinte forma:
Atentemos para duas coisas:
- Em viewports amplos, a entrada terá o dobro do tamanho do botão.
- Os SVGs serão elementos absolutamente posicionados e ficarão abaixo de seu controle de formulário adjacente. Novamente, para uma explicação mais detalhada, dê uma olhada neste tutorial anterior.
Aqui estão os estilos para esta seção:
/*CUSTOM VARIABLES HERE*/ .todo-form .form-wrapper { position: relative; } .todo-form input, .todo-form button { position: relative; width: 100%; z-index: 1; padding: 15px; } .todo-form svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .todo-form button { color: var(--white); text-transform: uppercase; } @media screen and (min-width: 600px) { .todo-form { display: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 5px; } }
Estilos de estatísticas
Em seguida, vamos olhar para a barra de status que nos dará um relatório rápido sobre o número total de tarefas.
Em telas pequenas terá a seguinte aparência empilhada:
No entanto, em viewports de 600 pixels de largura e acima, deve mudar da seguinte forma:
Atentemos para duas coisas:
- Em viewports amplas, todas as crianças
div
elementos terão larguras iguais. - Da mesma forma que os SVGs anteriores, este também será absolutamente posicionado e atuará como uma imagem de fundo que cobre toda a seção.
Os estilos relacionados:
/*CUSTOM VARIABLES HERE*/ .todo-stats { position: relative; text-align: center; padding: 5px 10px; margin: 10px 0; color: var(--white); } .todo-stats > div { position: relative; z-index: 1; } .todo-stats svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } @media screen and (min-width: 600px) { .todo-stats { display: grid; grid-template-columns: repeat(3, 1fr); } }
Estilos de tarefas
O layout das tarefas, que geraremos dinamicamente na próxima seção, ficará assim:
Cada tarefa que será representada por um li
terá duas partes.
Na primeira parte, você verá uma caixa de seleção junto com o nome da tarefa. Na segunda parte, você notará um botão de exclusão para remover a tarefa.
Aqui estão os estilos relacionados:
.todo-list li { display: grid; align-items: baseline; grid-template-columns: auto 20px; grid-column-gap: 10px; padding: 0 10px; } .todo-list li + li { margin-top: 10px; }
Quando uma tarefa é incompleto, uma caixa de seleção vazia aparecerá. Por outro lado, se uma tarefa for marcada como concluído, uma marca de seleção aparecerá. Além disso, seu nome receberá 50% de opacidade, bem como uma linha através dele.
Aqui estão os estilos responsáveis por esse comportamento:
.todo-list .checkbox-wrapper { display: flex; align-items: baseline; } .todo-list .checkbox-wrapper label { display: grid; margin-right: 10px; } .todo-list .checkbox-wrapper svg { grid-column: 1; grid-row: 1; width: 20px; height: 20px; } .todo-list .checkbox-wrapper .checkmark { display: none; } .todo-list [type="checkbox"]:checked + label .checkmark { display: block; } .todo-list [type="checkbox"]:checked ~ span { text-decoration: line-through; opacity: 0.5; }
Finalmente, abaixo estão os estilos para o botão de exclusão:
.todo-list .remove-task { display: flex; padding: 2px; } .todo-list .remove-task svg { width: 16px; height: 16px; }
5. Adicione o JavaScript
Neste ponto, estamos prontos para construir a funcionalidade principal de nosso aplicativo de lista TODO. Vamos fazer isso!
No envio do formulário
Cada vez que um usuário envia o formulário pressionando o botão Digitar chave ou o Enviar botão, faremos o seguinte:
- Interrompa o envio do formulário, evitando assim o recarregamento da página.
- Pegue o valor que está contido no campo de entrada.
- Supondo que o campo de entrada não esteja vazio, criaremos um novo objeto literal que representará a tarefa. Cada tarefa terá um ID exclusivo, um nome e estará ativa (não concluída) por padrão.
- Adicione esta tarefa ao
tasks
variedade. - Armazene a matriz no armazenamento local. O armazenamento local suporta apenas strings, então para fazer isso, temos que usar o
JSON.stringify()
método para converter os objetos dentro da matriz em strings. - Ligar para
createTask()
função para representar visualmente a tarefa na tela. - Limpe o formulário.
- Dê foco ao campo de entrada.
Aqui está o código relevante:
const todoForm = document.querySelector(".todo-form"); let tasks = []; todoForm.addEventListener("submit", function(e) { // 1 e.preventDefault(); // 2 const input = this.name; const inputValue = input.value; if (inputValue != "") { // 3 const task = { id: new Date().getTime(), name: inputValue, isCompleted: false }; // 4 tasks.push(task); // 5 localStorage.setItem("tasks", JSON.stringify(tasks)); // 6 createTask(task); // 7 todoForm.reset(); } // 8 input.focus(); });
Criar uma tarefa
o createTask()
A função será responsável por criar a marcação da tarefa.
Por exemplo, aqui está a estrutura para a tarefa “Dar um passeio”:
Duas coisas são importantes aqui:
- Se a tarefa for concluída, a marca de seleção aparecerá.
- Se a tarefa não for concluída, é
span
elemento receberá ocontenteditable
atributo. Este atributo nos dará a capacidade de editar/atualizar seu nome.
Segue abaixo a sintaxe desta função:
function createTask(task) { const taskEl = document.createElement("li"); taskEl.setAttribute("id", task.id); const taskElMarkup = `${task.name}`; taskEl.innerHTML = taskElMarkup; todoList.appendChild(taskEl); countTasks(); }
Atualizar uma tarefa
Uma tarefa pode ser atualizada de duas maneiras diferentes:
- Ao alterar seu status de “incompleto” para “concluído” e vice-versa.
- Modificando seu nome caso a tarefa esteja incompleta. Lembre-se que, neste caso, o
span
elemento tem ocontenteditable
atributo.
Para acompanhar essas mudanças, aproveitaremos a input
evento. Este é um evento aceitável para nós porque se aplica tanto a input
elementos e elementos com contenteditable
ativado.
O complicado é que não podemos anexar este evento diretamente aos elementos de destino (caixa de seleção, span
) porque são criados dinamicamente e não fazem parte do DOM no carregamento da página.
Graças à delegação do evento, anexaremos o input
evento para a lista pai que faz parte da marcação inicial. Em seguida, através do target
propriedade desse evento, vamos verificar os elementos nos quais o evento ocorreu e chamar o updateTask()
função:
todoList.addEventListener("input", (e) => { const taskId = e.target.closest("li").id; updateTask(taskId, e.target); });
Dentro de updateTask()
função, faremos o seguinte:
- Pegue a tarefa que precisa ser atualizada.
- Verifique o elemento que acionou o evento. Se o elemento tiver o
contenteditable
atributo (ou seja, é ospan
elemento), vamos definir o nome da tarefa igual aospan
conteúdo de texto de. - Caso contrário (ou seja, é a caixa de seleção), alternaremos o status da tarefa e sua
checked
atributo. Além disso, também alternaremos ocontenteditable
atributo do adjacentespan
. - Atualize o valor do
tasks
chave no armazenamento local. - Ligar para
countTasks()
função.
Aqui está a sintaxe para esta função:
function updateTask(taskId, el) { // 1 const task = tasks.find((task) => task.id === parseInt(taskId)); if (el.hasAttribute("contentEditable")) { // 2 task.name = el.textContent; } else { // 3 const span = el.nextElementSibling.nextElementSibling; task.isCompleted = !task.isCompleted; if (task.isCompleted) { span.removeAttribute("contenteditable"); el.setAttribute("checked", ""); } else { el.removeAttribute("checked"); span.setAttribute("contenteditable", ""); } } // 4 localStorage.setItem("tasks", JSON.stringify(tasks)); // 5 countTasks(); }
Remover uma tarefa
Podemos remover uma tarefa através do botão “fechar”.
Semelhante à operação de atualização, não podemos anexar diretamente um evento a este botão porque ele não está no DOM quando a página é carregada.
Mais uma vez obrigado à delegação do evento, anexaremos um click
event à lista pai e execute as seguintes ações:
- Verifique se o elemento clicado é o botão “fechar” ou seu filho SVG.
- Se isso acontecer, vamos pegar o
id
do item da lista pai. - Passe isso
id
para oremoveTask()
função.
Aqui está o código relevante:
const todoList = document.querySelector(".todo-list"); todoList.addEventListener("click", (e) => { // 1 if ( e.target.classList.contains("remove-task") || e.target.parentElement.classList.contains("remove-task") ) { // 2 const taskId = e.target.closest("li").id; // 3 removeTask(taskId); } });
Dentro de removeTask()
função, faremos o seguinte:
- Remover do
tasks
array a tarefa associada. - Atualize o valor do
tasks
chave no armazenamento local. - Remova o item de lista associado.
- Ligar para
countTasks()
função.
Aqui está a sintaxe para esta função:
function removeTask(taskId) { // 1 tasks = tasks.filter((task) => task.id !== parseInt(taskId)); // 2 localStorage.setItem("tasks", JSON.stringify(tasks)); // 3 document.getElementById(taskId).remove(); // 4 countTasks(); }
Tarefas de Contagem
Como já discutimos, muitas das funções acima incluem o countTask()
função.
Sua função é monitorar as tarefas quanto a alterações (adições, atualizações, exclusões) e atualizar o conteúdo dos elementos relacionados.
Aqui está sua assinatura:
const totalTasks = document.querySelector(".total-tasks span"); const completedTasks = document.querySelector(".completed-tasks span"); const remainingTasks = document.querySelector(".remaining-tasks span"); function countTasks() { totalTasks.textContent = tasks.length; const completedTasksArray = tasks.filter((task) => task.isCompleted === true); completedTasks.textContent = completedTasksArray.length; remainingTasks.textContent = tasks.length - completedTasksArray.length; }
Impedir a adição de novas linhas
Cada vez que um usuário atualiza o nome de uma tarefa, ele não deve ser capaz de criar novas linhas pressionando o botão Digitar chave.
Para desabilitar esta funcionalidade, mais uma vez vamos aproveitar a delegação do evento e anexar o keydown
evento à lista, assim:
todoList.addEventListener("keydown", function (e) { if (e.keyCode === 13) { e.preventDefault(); } });
Note que neste cenário apenas o span
elementos podem acionar esse evento, então não há necessidade de fazer uma verificação adicional como esta:
if (e.target.hasAttribute("contenteditable") && e.keyCode === 13) { e.preventDefault(); }
Dados persistentes no carregamento da página
Até agora, se fecharmos o navegador e navegarmos para o projeto de demonstração, nossas tarefas desaparecerão.
Mas, espere, isso não é 100% verdade! Lembre-se que cada vez que fazemos uma manipulação de tarefa, também armazenamos o tasks
matriz no armazenamento local. Por exemplo, no Chrome, para ver as chaves e valores de armazenamento local, clique no botão Inscrição guia em seguida, expanda o Armazenamento Local menu e, finalmente, clique em um domínio para visualizar seus pares chave-valor.
No meu caso, aqui estão os valores para o tasks
chave:
Portanto, para exibir essas tarefas, primeiro precisamos recuperá-las do armazenamento local. Para fazer isso, usaremos o JSON.parse()
método que converterá as strings de volta em objetos JavaScript.
Em seguida, armazenaremos todas as tarefas no familiar tasks
variedade. Lembre-se de que, se não houver dados no armazenamento local (por exemplo, na primeira vez que visitamos o aplicativo), essa matriz estará vazia. Em seguida, temos que percorrer o array e, para cada tarefa, chamar o método createTask()
função. E isso é tudo!
O trecho de código correspondente:
let tasks = JSON.parse(localStorage.getItem("tasks")) || []; if (localStorage.getItem("tasks")) { tasks.map((task) => { createTask(task); }); }
Conclusão
Ufa! Obrigado por acompanhar nesta longa jornada pessoal. Espero que você tenha adquirido algum conhecimento novo hoje, que poderá aplicar em seus próprios projetos.
Vamos nos lembrar do que construímos:
Sem dúvida, construir tal aplicativo com uma estrutura JavaScript pode ser mais estável, fácil e eficiente (repintar o DOM é caro). No entanto, saber resolver esse tipo de exercício com JavaScript simples ajudará você a obter uma compreensão sólida de seus fundamentos e a torná-lo um desenvolvedor de JavaScript melhor.
Antes de encerrar, deixe-me propor duas ideias para estender este exercício:
- Use a API HTML Drag and Drop ou uma biblioteca JavaScript como Sortable.js para reordenar as tarefas.
- Armazene dados (tarefas) na nuvem em vez do navegador. Por exemplo, substitua o armazenamento local por um banco de dados em tempo real como o Firebase.
Como sempre, muito obrigado pela leitura!
Mais aplicativos Vanilla JavaScript
Se você quiser aprender a criar pequenos aplicativos com JavaScript simples, confira os seguintes tutoriais: