To-do List localStorage használatával

Mi a cél?

A localStorage-ot mint helyi adatbázist használjuk most a feladataink tartós (persist) eltárolására. Ide kívánkozik ugyanakkor, hogy a böngészők privát módjánál (így a Firefoxnál alapból) a localStorage is úgy működik, mint a sessionStorage, azaz nem tárol csak az adott munkamenet alatt. Ettől függetlenül a közösség érdekében mindenki használjon Firefox-ot.

DEMO (a képre kattintva indul)

Todo list localStorage használatával

HTML kód

Behúzzuk a font-awesome-ot a kuka ikon miatt. Az oldal 4 nagyobb különálló div-re tagozódik. A todo__header-ben a dátumok mellett lesz egy form, hogy egybe fogja a beviteli inputot és a beviteli button-t. Azért jó form-ot használni mert ekkor input ENTER-rel és a plusz gombbal is el tud majd menni a bevitt feladat egyetlen eseményfigyelőn keresztül. A pending, azaz elvégzendő feladatok azért vannak kikommentelve, mert ezeket a feladatokat majd mindig a JS hajtja végre, minden esetben újra és újra elő lesznek állítva (renderelve) a feladat divek.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>To do list</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous"> <link rel="stylesheet" href="style.css"> </head> <body> <h1>Daily To-Do list manager</h1> <div class="todo-container"> <div class="todo__header"> <h2 class="day"></h2> <h2 class="date"></h2> <form class="input-box"> <input class="input" type="text" placeholder="Take the garbage out"> <button type="submit" class="add">+</button> </form> </div> <p>You have <span class="pending-count"></span> pending items</p> <div class="chill"></div> <div class="pending-todos-box"> <!-- <div class="todo" data-id="123456789"> <div class="input-label-box"> <input type="checkbox"> <label>Task for today</label> </div> <span class="trash"></span> </div> --> </div> <div class="completed-todos-box hide"> <!-- <p>Completed tasks: 50%</p> --> </div> <div class="todo__footer"> <button class="hide-completed">Show Complete</button> <button class="clear">Clear All</button> </div> </div> <script src="main.js"></script> </body> </html>

CSS kód

Ezt az alap beállítást majd minden esetben használom, a CSS guruk azt mondják, hogy pseudo elemek esetén ez a korrekt box-sizing beállítás. A body-t is jó általában kifeszíteni, mert önmagában nincs magassága, csak a gyerek elemek miatt.

* { margin: 0; padding: 0; } *, *::before, *::after { box-sizing: inherit; } html { box-sizing: border-box; } body { min-height: 100vh; font-family: sans-serif; background: linear-gradient(violet, green); color: rgba(0, 0, 0,.4); }

Ezek layout beállítások.

h1 { padding: 2rem; text-align: center; color: #fff; } h2 { color: rgba(255,0,0, .5); font-weight: 500; } p { margin: 0.5rem 0; } .day { margin-top: 1rem; }

A fő containert a 'margin: 0 auto' állítja középre és a flex miatt tudjuk majd a benne lévő elemeket könnyedén mozgatni.

.todo-container { width: 30rem; height: 82vh; border-radius: 3px; background-color: #ececec; display: flex; justify-content: space-between; flex-direction: column; margin: 0 auto; padding: 1.5rem; }

A form (.input-box) flex tulajdonsága miatt tudjuk a gyerek beviteli input mezőt a 'flex:1'-el kifeszíteni és megformázzuk benne a placeholder-t.

.input-box { display: flex; margin-top: 1rem; } .input { flex: 1; margin-right: 0.5rem; padding-left: 0.5rem; border: 1px solid rgba(0, 0, 0,.3); font-style: italic; color: rgba(0, 0, 0,.3); border-radius: 3px; font-size: 1rem; }

Az elvégzendő feladatok div felveszi a rendelkezésére álló összes helyet (flex:1) és az egyes feladatok div-jében pedig azért használunk position:relativ-et, hogy a pseudo elemet majd tudjuk mozgatni hozzá képest. A show-in class majd a legelső feladat elemre vonatkozik, mert az majd ezzel az animációval fog megérkezni.

.pending-todos-box { flex: 1; } .show-in { animation-name: show-in; animation-duration: 1s; } .todo { background-color: rgba(0, 0, 0,.1); padding: 0.5rem; display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; position: relative; } .input-label-box { display: flex; align-items: center; } @keyframes show-in { 0%{ opacity: 0; width: 0 } 100% { opacity: 1; width: 100%; } }

A kukát most egy pseudo elemmel hozzuk létre, ez egy font-awesome-elem, ezért a content unicode mellett most a FontAwesome font-family-t is használni kell. Az input végi beigazításhoz kell a position:absolut és a right:0 és a line-height-al állítjuk a kukát függőleges irányban középre, mégpedig úgy hogy a szülő elem magasságát adjuk neki, ilyenkor a karakter (jelen esetben ikon) középre kerül.

.todo:hover.todo::after { position: absolute; content: "\f014"; font-family: FontAwesome; right: 0; background-color: red; height: 100%; width: 2.5rem; cursor: pointer; text-align: center; line-height: 2.5rem; color: #fff; animation-name: fade-in; animation-duration: 1s; } @keyframes fade-in { 0% { width: 0; content:''; } 100% { width: 2.5rem; content: "\f014"; } }

Mivel a pseudo elemet nem tudjuk kattintani, ezért a kuka felé rakunk egy span elemet, amire majd felírjuk az eseménykezelőt.

.trash { width: 2.5rem; height: 1.5rem; z-index: 10; cursor: pointer; }

A következő elemek a feladat pipa négyzetek és a bepipálás design-olására szolgálnak. A bepipálás is egy pseudo elemmel történik, mégpedig úgy, hogy amikor megtörténik a bepipálás, akkor az eredeti design eltűnik és rajzolunk egy újat a \2713-as unicode-al.

input[type='checkbox'] { width: 1.5rem; height: 1.5rem; cursor: pointer; } input[type='checkbox']:checked { display: none; } input[type='checkbox']:checked + label::before { content: '\2713'; color: red; background-color: rgba(0, 0, 0,.01); text-align: center; display: inline-block; width: 1.5rem; margin-right: .5rem; border: 1px solid rgba(0, 0, 0,.3); font-weight: 900; } label { margin-left: 0.5rem; } input[type='checkbox']:checked + label { margin-left: 0; text-decoration: line-through; }

A gombokkal kapcsolatos design.

button { border: none; background: none; font-weight: 700; font-size: 1rem; cursor: pointer; padding: 0.5rem 1.5rem; border-radius: 3px; } button:not([type=submit]) { margin-top: 0.3rem; } .add { background-color: rgba(0, 0, 0,.1); color: #fff; font-size: 1.5rem; } .add:hover { background-color: royalblue; } .hide-completed:hover, .clear:hover { background-color: rgba(0, 0, 0,.15); color: #fff; } .todo__footer { display: flex; justify-content: center; }

Néhány JS mozgatással kapcsolatos CSS.

.hide { display: none; } .checked { background-color: #fff; }

A pihenővel (.chill), üres feladatokkal kapcsolatos osztályok.

.chilldesign { flex: 1; text-align: center; font-weight: 900; color: #000000; display: flex; flex-direction: column; justify-content: center; align-items: center; } .emoji { font-size: 5rem; margin: 1rem; }

JS kód

Írjuk fel először a DOM elemeket, melyekre hivatkozni fogunk, melyekkel feladatokat fogunk végezni, és beállíjuk az elvégzendő feladatok számlálójót 0-ra, valamint az összes feladatot tartalmazó tömböt is üres tömbre.

const day = document.querySelector('.day'); const date = document.querySelector('.date'); const input = document.querySelector('.input'); const inputBox = document.querySelector('.input-box'); const pendingCount = document.querySelector('.pending-count'); const pendingTodosBox = document.querySelector('.pending-todos-box'); const completedTodosBox = document.querySelector('.completed-todos-box'); const clearButton = document.querySelector('.clear'); const hideButton = document.querySelector('.hide-completed'); const chill = document.querySelector('.chill'); let count = 0; let todos = [];

A dátumokra a toLocaleDateString-gel találunk megoldást, egy objektumban elkérhetjük a csak minket érdeklő dolgokat, jelen esetben a nap hosszú leírását és a másiknál pedig az amerikai formátum kis átalakítással megfelelő lesz.

day.textContent = new Date().toLocaleDateString('en', { weekday: 'long' }); date.textContent = new Date().toLocaleDateString('en-US').replaceAll('/', '-');

Kezdhetjük a feladatot azzal, hogy a formból, illetve annak inputjából érkező feladatot először beírjuk a localStorage-ba. Itt sokféle ellenőrzést végezhetnénk regex-ekkel, trim-mellésel, hogy ne legyen benn space vagy akármi nem kívánatos, most csak egy olyan feltételt írtam, hogy ne legyen üres, vagyis ha üresen nyomtuk meg az ENTERT (vagy a plusz gombot), akkor az ne menjen az adatbázisba. Az adatok, feladatok egyediségét sokféleképpen lehetne meghatározni, különféle véletlenszám előállításokkal, itt most az 1970-től számított ms-okat használjuk. A feladat (todo) objektumunk az egyedi id mellett tartalmazza még a feladat nevét(a bevitt szöveget) valamint, hogy elvégzendő vagy már elvégzett. Az adatbázisból aztán létrehozzuk az elemeket, kitöröljük az inputmezőt. Először tehát tegyünk a formra egy eseményfigyelőt, majd aztán adatbázis és utána elem megjelenítés.

inputBox.addEventListener('submit', (event) => { event.preventDefault(); addTodos(input.value); }) function addTodos(inputdata) { if (inputdata !== '') { const todo = { id: Date.now(), name: inputdata, completed: false } todos.push(todo); handleCount(todos); addToLocalStorage(todos); input.value = ''; pendingTodosBox.firstChild.classList.add('show-in'); } } function addToLocalStorage(todos) { localStorage.setItem('todos', JSON.stringify(todos)); createTodosElement(todos); }

Az elem megjelenítés folyamata először a meglévő elemek kiradírozása, aztán az adatbázisból megkapott adatok alapján a HTML elemek megrajzolása, kiválasztva közben, hogy a feladat már elvégzett vagy elvégzendő és a végén kiszámoljuk és megjelenítjük, hogy mennyi feladat van már elvégezve. A feladatok HTML ragasztásánál most az append helyett a prepend-et használjuk, hogy felülre kerüljön mindig a bevitt új elem.

function createTodosElement(todosArray) { pendingTodosBox.innerHTML = ''; completedTodosBox.innerHTML = ''; todosArray.forEach(todo => { const checked = todo.completed ? 'checked' : null; const TodosBoxElement = document.createElement("div"); TodosBoxElement.classList.add('todo'); TodosBoxElement.setAttribute('data-id', todo.id); if (todo.completed) { TodosBoxElement.classList.add('checked') } TodosBoxElement.innerHTML = ` <div class="input-label-box"> <input type="checkbox" ${checked}> <label>${todo.name}</label> </div> <span class="trash"></span> `; todo.completed ? completedTodosBox.prepend(TodosBoxElement) : pendingTodosBox.prepend(TodosBoxElement); }); createCompletedTitle(); } function createCompletedTitle() { const completedTitle = document.createElement('p'); completedTitle.textContent = `Completed tasks: ${Math.round((todos.length - count) * 100 / todos.length)}%` completedTodosBox.prepend(completedTitle); }

A clear gombra teszünk egy eseményfigyelőt, melynek a belső függvényében az elvégzett feladatokat válogatjuk le és tesszük a localStorage-ba, ezzel átételesen kitöröljük a még nem elvégzett feladatokat.

clearButton.addEventListener('click', () => { clearPendingElements(); }) function clearPendingElements() { todos = todos.filter(item => { return item.completed === true; }); handleCount(todos); addToLocalStorage(todos); }

A show/hide gomra is rakunk egy eseményfigyelőt, ez a szokásos hide class mozgatás, mivel click eseményről van szó, a remove/add páros helyett használható a classList.toggle is.

hideButton.addEventListener('click', () => { completedTodosBox.classList.toggle('hide'); completedTodosBox.classList.contains('hide') ? hideButton.textContent = 'Show Complete' : hideButton.textContent = 'Hide Complete'; })

A checkboxra és a kukára egy eseményfigyelőt teszünk és pedig az elvégzendő feladatok div-re, mert a feladatra nem tehetünk, mert amennyiben nincs semmilyen feladat, akkor az ahhoz kapcsolódó DOM elemet nem tudná felírni a JS és megakadna. A kuka törlést felírtam a már elvégzett feladatokra is. A checbox bepipálás esetén megváltoztatjuk a feladat státuszát elvégzettre, a kuka esetén pedig töröljük. A megjelölt feladatot a data-id-n keresztül párosítjuk az adatbázisban található párjához.

pendingTodosBox.addEventListener('click', (event) => { if (event.target.type === 'checkbox') { changeCompleted(parseInt(event.target.parentElement.parentElement.getAttribute('data-id'))) } if (event.target.classList.contains('trash')) { deleteTodo(parseInt(event.target.parentElement.getAttribute('data-id'))); } }) completedTodosBox.addEventListener('click', (event) => { if (event.target.classList.contains('trash')) { deleteTodo(parseInt(event.target.parentElement.getAttribute('data-id'))); } })

A két belső függvény.

function changeCompleted(id) { todos.forEach(item => { if (item.id == id) { item.completed = !item.completed; } }); handleCount(todos); addToLocalStorage(todos); } function deleteTodo(id) { todos = todos.filter(item => { return item.id !== id; }) handleCount(todos); addToLocalStorage(todos); }

A handleCount() figyeli, hogy az elvégzendő feladatok nem ürültek-e ki, mert akkor mehetünk chill állapotba. Ha viszont van elvégzendő feladat, akkor az esetleges korábbi chill állapotot töröljük.

function handleCount(todos) { count = todos.filter(item => item.completed === false).length; if (count === 0) { timeToChill(); pendingCount.parentElement.classList.add('hide'); } else { pendingCount.parentElement.classList.remove('hide'); chill.textContent = ''; chill.classList.remove('chilldesign'); pendingCount.textContent = count; clearButton.classList.remove('hide'); hideButton.classList.remove('hide'); } }

A timeToChill() elrejti az elvégzett feladatokat, berakunk egy emojit és töröljük alulról a két gombot is.

function timeToChill() { completedTodosBox.classList.add('hide'); hideButton.textContent = 'Show Complete'; chill.innerHTML = `<span class="emoji">🍻</span> Time to chill! You have no todos.`; chill.classList.add('chilldesign'); clearButton.classList.add('hide'); hideButton.classList.add('hide'); }

A legvégén megoldjuk az első indítás kérdését, amikor is ha már van valami a localStorage-ban, akkor azzal feltöltjük a todo list-ünket. Ezt érdemes rögtön a dátumok után tenni a kódba.

getFromLocalStorage(); function getFromLocalStorage() { todos = JSON.parse(localStorage.getItem('todos')) || []; handleCount(todos); createTodosElement(todos); }

Itt a vége: TIME TO CHILL!!!!

A teljes kód egyben a GitHub-on.