Projekt TODO

Czas przećwiczyć to co poznaliśmy do tej pory. Naszym celem będzie stworzenie prostej aplikacji służącej do dodawania prostych zadań. Aplikacja będzie umożliwiała dodawać, usuwać oraz wyszukiwać zadania.

Zanim zaczniemy pracę, ściągnij tą paczkę z plikami, na których będziemy działać. W paczce znajdują się 2 katalogi: project i finish-project. Wersja finish to skończony projekt. Nas głównie teraz interesuje katalog project.

Jak widzisz zaczynamy od prostego HTML.


<!doctype html>
<html lang="pl">
    <head>
        <meta charset="utf-8">
        <title>TODO</title>
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div class="todo-cnt">
            
        </div>

        <script src="js/script.js"></script>
    </body>
</html>

Nasze wszystkie elementy będziemy wstawiać w element .todo-cnt

Formularz dodawania zadania

Zanim zaczniemy pisać nasze skrypty, powinniśmy stworzyć wygląd naszych elementów. Zaczynamy od html i css dla formularza dodawania zadań.


<form class="todo-form" id="todoForm">
    <div class="todo-form-row">
        <label class="todo-form-label" for="todoMessage">Podaj treść zadania</label>
        <textarea class="todo-form-message" name="todoMessage" id="todoMessage"></textarea>
    </div>
    <div class="todo-form-row">
        <button type="submit" class="button todo-form-button">Dodaj</button>
    </div>
</form>

Nie jest to kurs o HTML, ale małe info dla Ciebie. Pamiętaj że tworząc kod formularzy zawsze powinieneś dawać labele dla inputów. Nawet jeżeli takiego labela nie ma na layoucie a input ma placeholder, label w kodzie powinien się znajdować - ewentualnie powinieneś go ukryć dowolną techniką do ukrywania tekstu.

Nasz formularz jest prostym tworem. Nie zakładamy możliwości jego działania bez JavaScript, dlatego dla elementu form pominąłem atrybuty method i action. W praktyce gdybyś celował w jak największą dostępność twojej aplikacji, wtedy takie atrybuty powinny się tutaj znaleźć, a sam formularz powinien być możliwy do użycia bez JavaScript (oczywiście użycie takie było by średnio wygodne, ale było by możliwe...).


* {
    box-sizing: border-box;
}
body {
    background: #fafafa;
}

.todo-cnt {
    max-width:60rem;
    margin:0 auto;
}
.todo-form {
    font-family:sans-serif;
    box-shadow: 0 1px 2px rgba(0,0,0,0.2);
    padding:1rem;
    background: #fff;
}
.todo-form-row {
    overflow:hidden;
}
.todo-form-label {
    display: block;
    font-size:0.8rem;
    margin-bottom:0.2rem;
}
.todo-form-message {
    padding:1rem;
    height:10rem;
    width:100%;
    border:1px solid #ddd;
    margin-bottom: 0.5rem;
    resize: vertical;
    font-family:sans-serif;
}
.todo-form-button {
    padding:0.8rem 1.6rem;
    background: #F15C5C;
    color:#fff;
    border:0;
    border-radius: 0.188rem;
    transition:0.4s all;
    cursor: pointer;
    float: right;
}
.todo-form-button:hover {
    background: #ED2D2D;
}
1 - Formularz dodawania

Lista zadań

Kolejnym elementem będzie lista z zadaniami. Na razie stwórzmy ich wygląd na sztywno. W końcowej fazie zadania te będą generowane dynamicznie. Zanim jednak tą dynamikę dodamy, musimy stworzyć HTML by mieć na czym się opierać.

Lista zadań składa się z tytułu i mini formularza służącego do wyszukiwania zadań.


<section class="todo-list-cnt">
    <header class="todo-list-header">
        <h2 class="todo-list-title">
            Lista zadań
        </h2>
        <form class="todo-list-search-form">
            <input type="search" id="todoSearch" class="todo-list-search">
        </form>
    </header>

    <div class="todo-list" id="todoList">
        <!-- tutaj trafią zadania -->
    </div>
</section>

.todo-list-cnt {
    background: #fff;
    font-family:sans-serif;
    margin:1rem 0;
    border:1px solid #ddd;
    padding:1rem 2rem;
}
.todo-list-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.todo-list-search-form {
    width:50%;
}
.todo-list-search {
    border:1px solid #ddd;
    padding:0.5rem;
    width: 100%;
}
.todo-list-title {
    margin-top:0;
    margin-bottom:0;
    font-size:1rem;
    text-transform: uppercase;
}
2 - Formularz i lista zadań

Pojedyncze zadanie

Ostatnim elementem będzie dodawane zadanie.


<div class="todo-element">
    <div class="todo-element-bar">
        <h3 class="todo-element-date">20-10-2016 godz. 16:32</h3>
        <button class="todo-element-delete" title="Usuń task">
            <i class="fas fa-times-circle"></i>
        </button>
    </div>
    <div class="todo-element-text">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Distinctio laudantium quasi blanditiis enim molestias explicabo id totam veniam corporis maiores.
    </div>
</div>

Nasze zadanie składa się z belki z datą utworzenia i przyciskiem usunięcia. Przycisk usunięcia ma w sobie ikonkę [x]. Ikonkę tą dodamy za pomocą fontAwesome. Niedawno wyszła 5 wersja tej świetnej biblioteki ikon. Aż prosi się, by spróbować ją w praktyce.

Wchodzimy na stronę https://fontawesome.com/get-started i wedle instrukcji dodajemy do naszej strony skrypt:


<script defer src="https://use.fontawesome.com/releases/v5.0.2/js/all.js"></script>

Po dodaniu skryptu, wystarczy użyć odpowiedniej ikonki - w naszym przypadku to ikonka .fa-times-circle

Pozostaje ostylować elementy zadań:


.todo-element {
    border-top:1px solid #ddd;
    margin-top:1rem;
    padding-top:2rem;
    padding-bottom: 2rem;
}
.todo-element-bar {
    overflow:hidden;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom:0.75rem;
}
.todo-element-date {
    margin:0;
}
.todo-element-date {
    font-size:0.9rem;
    font-weight: normal;
}
.todo-element-delete {
    color:#F15C5C;
    border:0;
    font-size:1rem;
    background: none;
    cursor: pointer;
    padding:0;
    transition:0.4s all;
}
.todo-element-delete svg {
    pointer-events: none; /* nie chcemy obsługi kliknięć na elementy svg - bo takie dodaje użyty fontAwesome */
}
.todo-element-delete:hover {
    color: #ED2D2D;
}
.todo-element-text {
    color:#777;
    font-size:0.8rem;
}
3 - Lista zadań z zadaniami

Dodawanie taska do listy

Skrypt rozpoczynamy od wyłapania elementów, na których będziemy pracować:


let todoList = null;
let todoForm = null;
let todoSearch = null;

document.addEventListener('DOMContentLoaded', function() {
    todoList = document.querySelector('#todoList');
    todoForm = document.querySelector('#todoForm');
    todoSearch = document.querySelector('#todoSearch');

    ...
});

Pierwszą rzeczą którą zrobimy to podpięcie się pod wysyłkę formularza. Gdy ktoś wyśle formularz, sprawdzimy czy treść nie jest pusta. Jeżeli tak jest, dodamy nowe zadanie do listy. Zadanie powinno zawierać tekst pobrany z formularza oraz datę dodania. Po dodaniu zadania do listy wyczyścimy treść w formularzu.


let todoList = null;
let todoForm = null;
let todoSearch = null;

function addTask(text) {
    console.log('Dodaję zadanie do listy')
}

document.addEventListener('DOMContentLoaded', function() {
    const todoList = document.querySelector('#todoList');
    const todoForm = document.querySelector('#todoForm');
    const todoSearch = document.querySelector('#todoSearch');

    todoForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const textarea = this.querySelector('textarea');
        if (textarea.value !== '') {
            addTask(textarea.value);
            textarea.value = '';
        }
    });
});

Kolejnym krokiem jest stworzenie kodu funkcji addTask(). Musimy wygenerować kod elementu, który już stworzyliśmy powyżej. Moglibyśmy tutaj użyć innerHTML, ale w tym przypadku kodu aż tyle nie ma, a i nie chcemy tracić referencji do tworzonych elementów. Wykorzystanie createElement, mimo, że powoduje powstanie o wiele więcej kodu, jest dobrą praktyką:


function addTask(text) {
    //element todo
    const todo = document.createElement('div');
    todo.classList.add('todo-element');

    //belka górna
    const todoBar = document.createElement('div');
    todoBar.classList.add('todo-element-bar');

    //data w belce
    const todoDate = document.createElement('div');
    todoDate.classList.add('todo-element-bar');
    const date = new Date();
    const dateText = date.getDate() + '-' + (date.getMonth()+1) + '-' + date.getFullYear() + ' godz.: ' + date.getHours() + ':' + date.getMinutes();
    todoDate.innerText = dateText;

    //przycisk usuwania
    const todoDelete = document.createElement('button');
    todoDelete.classList.add('todo-element-delete');
    todoDelete.classList.add('button');
    todoDelete.innerHTML = '<i class="fas fa-times-circle"></i>';

    //wrzucamy elementy do belki
    todoBar.appendChild(todoDate);
    todoBar.appendChild(todoDelete);

    //element z tekstem
    const todoText = document.createElement('div');
    todoText.classList.add('todo-element-text');
    todoText.innerText = text;

    //łączymy całość
    todo.appendChild(todoBar);
    todo.appendChild(todoText);

    //i wrzucamy do listy
    todoList.append(todo);
}
4 - Dodawanie zadania

Usuwanie zadania z listy

Po kliknięciu na przycisk [x] w elemencie zadania, zadania to powinno być usunięte.

Event taki nie możemy podpiąć tak samo jak event dla formularza. Pamiętaj, że elementy z zadaniami jeszcze nie istnieją, więc podpięlibyśmy event pod przyciski usuń, których nie ma.

W tym przypadku zastosujmy podpięcie eventu pod rodzica wraz ze sprawdzaniem elementu który został kliknięty. Ale spokojnie moglibyśmy tutaj zastosować normalne podpięcie pod te przyciski. Wszystko zależy od sytuacji - u nas raczej nigdy nie będziemy mieli miliona zadań na liście (a jeżeli tak, wtedy trzeba by bardziej przemyśleć tak prostą aplikację)


...

document.addEventListener('DOMContentLoaded', function() {
    ...

    todoList.addEventListener('click', function(e) {
        console.log(e.target);
    });
});

Po kliknięciu na taką ikonkę powinniśmy przejść do elementu zadania czyli .todo-element.

Problem jaki zobaczysz po odpaleniu powyższego skryptu jest taki, że e.target wskazuje na element svg - czyli ikonkę fontAwesome. Nie wiadomo dokładnie jak głęboko dany element jest zagnieżdżony. Może to będzie svg, a może jakiś element <i> w którym jest zagnieżdżona ikonka. Możliwe, że zaraz coś się w tym pluginie zmieni i ta struktura zostanie zmodyfikowana. Przez to właśnie nie jest bezpieczne zastosować tutaj konstrukcji e.target.parentElement.parentElement.parentElement

Na szczęście tak samo jak w jQuery w JavaScript istnieje metoda el.closest(selektor) która wyszukuje najbliższy element pasujący do danego selektora.


...

document.addEventListener('DOMContentLoaded', function() {
    ...

    if (e.target.closest('.todo-element-delete') !== null) {
        e.target.closest('.todo-element').remove();
    }
});

Jeżeli chcemy wspierać starsze IE, wtedy do naszego kodu musimy dodać tak zwany polyfill, czyli kod łatający brak wsparcia dla danej metody:


...

//polyfill dla przeglądarek nie obsługujących closest()
if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector;
if (!Element.prototype.closest) Element.prototype.closest = function (selector) {
    let el = this;
    while (el) {
        if (el.matches(selector)) {
            return el;
        }
        el = el.parentElement;
    }
};

document.addEventListener('DOMContentLoaded', function() {
    ...

    todoList.addEventListener('click', function(e) {
        if (e.target.closest('.todo-element-delete') !== null) {
            e.target.closest('.todo-element').remove();
        }
    });
});

A samą metodę remove() zastąpić odpowiednikiem w postaci removeChild:


todoList.addEventListener('click', function(e) {
    if (e.target.closest('.todo-element-delete') !== null) {
        const todoElem = e.target.closest('.todo-element');
        todoElem.parentNode.removeChild(todoElem);
    }
});

Dodaj kilka zadań, a następnie spróbuj je usunąć.

5 - Usuwanie zadania

Wyszukiwanie zadań

Ostatnia funkcjonalność to wyszukiwanie zadań. Wykorzystamy do tego mini formularz leżący w nagłówku listy zadań.

W tym przypadku zadanie jest bardzo proste. Po wpisaniu jakiejś wartości musimy zrobić pętlę po liście zadań. Dla każdego elementu będziemy sprawdzać czy tekst w danym zadaniu zawiera szukaną frazę - wykorzystamy do tego indexOf. Jeżeli zawiera to takie zadanie to taki element pokażemy, w przeciwnym wypadku ukryjemy go poprzez ustawienie display:none. Samą pętlę po elementach zadań wykonamy nie za pomocą for, a za pomocą forEach:


todoSearch.addEventListener('input', function() {
    const val = this.value;
    const elems = todoList.querySelectorAll('.todo-element');

    [].forEach.call(elems, function(el) {
        const text = el.querySelector('.todo-element-text').innerText;

        if (text.indexOf(val) !== -1) {
            el.style.setProperty('display', '');
        } else {
            el.style.setProperty('display', 'none');
        }
    });
});

Dodaj kilka zadań, a następnie korzystając z wyszukiwarki spróbuj coś wyszukać.

6 - Wyszukiwanie zadań

Podsumowanie

Nasz powyższy przykład jest bardzo prosty i w praktyce jeszcze trochę mu brakuje. Jego główną bolączką jest to, że każdorazowo po wejściu na stronę zadania są czyszczone. Aby móc zapisywać zadania trzeba wykorzystać do tego celu localStorage albo - lepiej - skorzystać z Ajax i zapisywać dane na serwerze. Ale to już w kolejnych rozdziałach...