Aplikacja todo

W poniższym tekście spróbujemy zmierzyć się z raczej prostą aplikacją, która pozwalać będzie nam zapisywać i edytować zadania w bazie danych. Kod napiszemy z podziałem na pliki i przy użyciu dodatkowych narzędzi bundlujących, czyli zbierzemy wiedzę z ostatnich kilku rozdziałów. Tutaj od razu mała uwaga. Poniższe podejście z wykorzystaniem bundlerów i podziałem na osobne pliki nie jest wymagane by stworzyć taki mini projekt. Rób po prostu jak ci wygodniej.

Wstęp

Aplikację, którą będziemy tworzyć poniżej możesz zobaczyć tutaj. Demo ma wyłączone operacje na bazie, więc nie wszystkie funkcjonalności będą w nim działać.

W poniższym projekcie będziemy do korzystać z parcela do bundlowania plików, oraz json-server jako małej bazy danych.

Początkowe pliki do projektu możesz ściągnąć tutaj.

Po pobraniu paczki, rozpakuj ją do jakiegoś katalogu, a następnie zainstaluj wszystko poleceniem npm i. Przejrzyj strukturę plików i zerknij na wygląd bazy danych w katalogu data.

My będziemy pracować w katalogu src, natomiast parcel będzie naszą pracę na bieżąco kompilować do katalogu dist.

Jak już się zaznajomisz ze strukturą projektu, odpal całość poleceniem npm start. Zadanie to odpala dwa zadania na raz. Wpierw odpali się json-server miło nas witając, a zaraz potem parcel. Dostaniesz w terminalu dwa adresy - jeden na który będziemy się łączyć, a drugi z aktualnym widokiem strony.

W poniższym tekście będę zaznaczał z jakiego pliku jest dany kod. Zakładam, że stosowne pliki będziesz w razie potrzeby tworzył.

Wczytanie zadań

Zacznijmy od najprostszej rzeczy, czyli wczytania zadań z bazy. Stwórz plik _api.js i dodaj do niego funkcję:


//_api.js

const apiUrl = "http://localhost:3000/tasks"; //tutaj twój adres do json-server, który pokazał się w terminalu

export async function apiGetTasks() {
    const request = await fetch(apiUrl);
    if (request.ok) {
        return request.json();
    } else {
        throw Error(request.status);
    }
}

A następnie zaimportuj ją w pliku app.js:


//app.js

require("../scss/style.scss");
import {apiGetTasks} from "./_api";

apiGetTasks().then(res => {
    console.log(res);
});

Sprawdź w konsoli czy dostaniesz odpowiedni wynik. Jeżeli tak, możemy przejść do wstępnego wyświetlenia zadań. Do tego celu napiszemy odpowiednie funkcje. Jedna będzie generować kod zadania, druga będzie tworzyć pojedyncze zadanie, a trzecia wykorzysta je do stworzenia całej listy:


//_render.js

const ul = document.querySelector(".task-list");

export function getTaskHTML(dataElement) {
    const {date, title, body} = dataElement;
    //kod pobrałem za pomocą debugera bezpośrednio ze strony dema
    return `
        <div class="task-inside">
            <div class="task-header">
                <h3 class="task-date">${date}</h3>

                <div class="task-actions">
                    <button class="task-edit" title="Edytuj zadanie">
                        Edytuj
                    </button>
                    <button class="task-delete" title="Usuń zadanie">
                        Usuń
                    </button>
                </div>
            </div>

            <div class="row">
                <div class="task-title"${title}</div>
            </div>

            <div class="row">
                <div class="task-body">
                    ${body}
                </div>
            </div>
        </div>
    `;
}

export function renderSingleTask(dataElement) {
    //tworzę article, dodaję mu klasę i ustawiam atrybut data-id na id pobrane z bazy
    const element = document.createElement("article");
    element.classList.add("task");
    element.dataset.id = dataElement.id;

    //wypełniam go odpowiednim html
    element.innerHTML = getTaskHTML(dataElement);
    ul.append(element);
}

export function renderTaskList(tasks) {
    ul.innerHTML = "";
    tasks.forEach(dataElement => {
        renderSingleTask(dataElement);
    })
}

//app.js

require("../scss/style.scss");
import { apiGetTasks } from "./_api";
import { renderTaskList, renderSingleTask } from "./_render";

apiGetTasks().then(res => {
    renderTaskList(res);
});

Dodawanie zadania

Dodawanie zadania podzielmy na 2 części. Po pierwsze musimy dodać je do bazy danych:


//_api.js

...

export async function apiAddTask({title, date, body}) {
    const request = await fetch(apiUrl, {
        method: "post",
        headers: {
            "Content-Type" : "application/json;charset=utf-8"
        },
        body: JSON.stringify({title, date, body})
    })

    if (request.ok) {
        return request.json();
    } else {
        throw Error(request.status);
    }
}

Po drugie musimy obsłużyć formularz na stronie przy okazji korzystając z powyższej funkcji:


//app.js

import { apiGetTasks, apiAddTask } from "./_api";

...

const form = document.querySelector("#todoForm");
form.addEventListener("submit", async e => {
    e.preventDefault();

    const title = form.querySelector("#todoTitle").value;
    const date = form.querySelector("#todoDate").value;
    const body = form.querySelector("#todoMessage").value;

    if (title && date && body) {
        const dataElement = await apiAddTask({title, date, body});
        renderSingleTask(dataElement);
        form.reset();
    } else {
        //tu można dodać ewentualnie walidację
    }
})

Po dodaniu i wyświetleniu zadania na liście, użytkownik z pewnością tego nie zauważy, ponieważ ląduje ono na końcu listy.

Rozwiązania są dwa - albo zamiast wrzucać go na koniec (append), możemy wrzucać go na początek listy (prepend). Zmienimy to na końcu funkcji renderSingleTask().


export function renderSingleTask(dataElement) {
    ...
    element.innerHTML = getTaskHTML(dataElement);
    ul.prepend(element);
}

Możemy też automatycznie przewijać listę do dodanego zadania:


export function renderSingleTask(dataElement) {
    ...
    element.innerHTML = getTaskHTML(dataElement);
    ul.append(element);

    //przewijam do dodanego zadania
    const ulCnt = document.querySelector(".task-list");
    ulCnt.scrollTop = ulCnt.scrollHeight - ulCnt.clientHeight;
}

Usuwanie zadań

To samo wykonamy w przypadku usuwania zadań. Wpierw komunikacja z serwerem:


//_api.js

export async function apiDeleteTask(id) {
    const request = await fetch(apiUrl + "/" + id, {
        method: "delete",
    });

    if (request.ok) {
        return request.json();
    } else {
        throw Error(request.status);
    }
}

A następnie wykorzystanie tej funkcji po kliknięciu na guzik usuwania zadania. Takie kliknięcie moglibyśmy podpiąć wewnątrz funkcji renderSingleTask pobierając odpowiedni element, natomiast możemy też skorzystać z propagacji zdarzeń:


//app.js

import { apiGetTasks, apiAddTask, apiDeleteTask } from "./_api";

...

document.addEventListener("click", async e => {
    //jeżeli kliknięty element ma klasę task-delete
    if (e.target.classList.contains("task-delete")) {
        const task = e.target.closest(".task");
        const id = +task.dataset.id; //pobieram id, który dodawaliśmy przy tworzeniu pojedynczego zadania

        const request = await apiDeleteTask(id);

        task.remove();
    }
});

Usuwając zadanie po prostu je wycinamy ze strony. Takie "chamskie" usuwanie od strony użytkownika nie jest najlepszym rozwiązaniem, ponieważ przy podobnych zadaniach na stronie nie zauważy on ich usuwania, bo kolejne momentalnie wleci na miejsce właśnie usuniętego. Aby to poprawić możemy pokusić się o dodanie tuż przed usunięciem subtelnej animacji zwijania. Do takich animacji najlepiej skorzystać z dodatkowej biblioteki - np. greensock. Ja tym razem skorzystam z dostępnego w nowych przeglądarkach api animate:


//app.js

...

document.addEventListener("click", async e => {
    //jeżeli kliknięty element ma klasę task-delete
    if (e.target.classList.contains("task-delete")) {
        const task = e.target.closest(".task");
        const id = +el.dataset.id; //pobieram id, który dodawaliśmy przy tworzeniu pojedynczego zadania

        const request = await apiDeleteTask(id);

        const anim = task.animate([
            {height: `${task.offsetHeight}px`},
            {height: '0px'}
        ], {
            duration: 300,
            iterations: 1
        })
        anim.onfinish = e => {
            task.remove();
        }
    }
});

Wyszukiwanie zadań

Kolejna dość podobna rzecz do poprzednich. Wpierw funkcja pobierająca dane:


//_api.js

...

export async function apiSearchTasks(query) {
    const request = await fetch(apiUrl + `?q=${query}`);
    if (request.ok) {
        return request.json();
    } else {
        throw Error(request.status);
    }
}

Zauważ, że w adresie zapytania do serwera przekazujemy parametr q=... - zgodnie z instrukcją json-server.

Wykorzystajmy powyższą funkcję podpinając się pod formularz wyszukiwania:


//app.js

import { apiGetTasks, apiAddTask, apiDeleteTask, apiSearchTasks } from "./_api";

...

const search = document.querySelector("#todoSearch");
search.addEventListener("input", async e => {
    const tasks = await apiSearchTasks(search.value)
    renderTaskList(tasks);
});

Przy bardzo szybkim wpisywaniu frazy do takiego pola możemy przyblokować połączenie, ponieważ będziemy robić wiele zapytań w bardzo krótkim czasie. Aby tego uniknąć możemy zastosować funkcję spowalniającą debounced, którą omawialiśmy tutaj. Stwórz plik _utility.js i dodaj do niego odpowiednią funkcję (a można i dwie):


//_utility.js

export function debounced(delay, fn) {
    let timerId;
    return function(...args) {
        if (timerId) {
            clearTimeout(timerId);
        }
        timerId = setTimeout(() => {
            fn(...args);
            timerId = null;
        }, delay);
    }
}

//app.js

import { debounced } from "./_utility.js";

...

const tHandler = debounced(500, async e => {
    console.log(search.value);
    const tasks = await apiSearchTasks(search.value)
    renderTaskList(tasks);
});
const search = document.querySelector("#todoSearch");
search.addEventListener("input", tHandler);

Edycja zadania

Najgorsze na koniec. Po kliknięciu na przycisk edycji w zadaniu powinniśmy dane elementy zamienić na odpowiednie kontrolki, a dodatkowo dodać do zadania przycisk Zapisz i Anuluj.

Zacznijmy jednak od funkcji zmieniającej dane na serwerze:


//_api.js

...

export async function apiEditTask({id, title, date, body}) {
    const request = await fetch(apiUrl + "/" + id, {
        method: "put",
        headers: {
            "Content-Type" : "application/json"
        },
        body: JSON.stringify({title, date, body})
    })

    if (request.ok) {
        return request.json();
    } else {
        throw Error(request.status);
    }
}

Podobnie do usuwania obsłużmy kliknięcie na przycisk edycji w zadaniu:


//app.js

import { apiGetTasks, apiAddTask, apiDeleteTask, apiSearchTasks, apiEditTask } from "./_api";
import { renderTaskList, renderSingleTask, getTaskHTML } from "./_render";

document.addEventListener("click", async e => {
    if (e.target.classList.contains("task-delete")) {
        ...
    }
    if (e.target.classList.contains("task-edit")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        const date = el.querySelector(".task-date").innerText;
        const title = el.querySelector(".task-title").innerText;
        const body = el.querySelector(".task-body").innerText;
        const dataElement = {
            id, date, title, body
        }
        el.innerHTML = getTaskHTML(dataElement, true); //uwaga tutaj dodałem wartość dla parametru
    }
});

Musimy zmienić funkcję getTaskHTML(dataElement), która od tej pory będzie też zwracała kod edytowanego zadania:


//_render.js

export function getTaskHTML(dataElement, editMode = false) {
    const {date, title, body} = dataElement;

    if (editMode) {
        return `
            <div class="task-inside">
                <div class="task-header">
                    <label>
                        <span>Podaj datę</span>
                        <input type="date" class="task-date" value="${date}">
                    </label>

                    <div class="task-actions">
                        <button class="task-delete" title="Usuń zadanie">
                            Usuń
                        </button>
                    </div>
                </div>

                <div class="row">
                    <label>
                        <span>Tytuł</span>
                        <input type="text" class="task-title" value="${title}">
                    </label>
                </div>

                <div class="row">
                    <label>
                        <span>Treść</span>
                        <textarea class="task-body">${body}</textarea>
                    </label>
                </div>

                <div class="task-footer">
                    <button class="button task-edit-save">Zapisz</button>
                    <button class="button task-edit-cancel button-secondary">Anuluj</button>
                </div>
            </div>
        `;
    } else {
        return `
            <div class="task-inside">
                <div class="task-header">
                    <h3 class="task-date">${date}</h3>

                    <div class="task-actions">
                        <button class="task-edit" title="Edytuj zadanie">
                            Edytuj
                        </button>
                        <button class="task-delete" title="Usuń zadanie">
                            Usuń
                        </button>
                    </div>
                </div>

                <div class="row">
                    <div class="task-title">${title}</div>
                </div>

                <div class="row">
                    <div class="task-body">
                        ${body}
                    </div>
                </div>
            </div>
        `;
    }
}

A także funkcje, które z niej korzystają:


export function renderSingleTask(dataElement, editMode) {
    //tworzę article, dodaję mu klasę i ustawiam atrybut data-id na id pobrane z bazy
    const element = document.createElement("article");
    element.classList.add("task");
    element.dataset.id = dataElement.id;

    //wypełniam go odpowiednim html
    element.innerHTML = getTaskHTML(dataElement, editMode);
    ul.append(element);

    //przewijam do dodanego zadania
    const ulCnt = document.querySelector(".task-list");
    ulCnt.scrollTop = ulCnt.scrollHeight - ulCnt.clientHeight;
}

export function renderTaskList(tasks) {
    ul.innerHTML = "";
    tasks.forEach(dataElement => {
        renderSingleTask(dataElement, false);
    })
}

//app.js

import { apiGetTasks, apiAddTask, apiDeleteTask, apiSearchTasks, apiEditTask } from "./_api";

document.addEventListener("click", async e => {
    if (e.target.classList.contains("task-delete")) {
        ...
    }
    if (e.target.classList.contains("task-edit")) {
        ...
        el.classList.add("task-edit-mode");
        el.innerHTML = generateSingleTask(dataElement, true);
    }
});

Dodatkowo musimy obsłużyć też dwa przyciski, które nam doszły:


//app.js

document.addEventListener("click", async e => {
    ...
    if (e.target.classList.contains("task-edit")) {
        ...
    }

    if (e.target.classList.contains("task-edit-save")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        const title = el.querySelector(".task-title").value;
        const body = el.querySelector(".task-body").value;
        const date = el.querySelector(".task-date").value;
        const dataElement = {
            id, date, title, body
        }

        const request = await apiEditTask(dataElement);
        el.classList.remove("task-edit-mode");
        el.innerHTML = getTaskHTML(dataElement, false);
    }

    if (e.target.classList.contains("task-edit-cancel")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        const title = el.querySelector(".task-title").value;
        const body = el.querySelector(".task-body").value;
        const date = el.querySelector(".task-date").value;
        const dataElement = {
            id, date, title, body
        }
        el.classList.remove("task-edit-mode");
        el.innerHTML = getTaskHTML(dataElement, false);
    }
}

Zastosowanie template

Jeżeli robisz aplikację w całości napisaną w Javascript (np. używając jakiegoś frameworka typu Vue), na sztywno zakodowany html w twoich skryptach nie powinien być raczej problematyczny. Zdarza się jednak, że twoja aplikacja, a w zasadzie wygląd html będzie zależał od backendu i to właśnie "tył" powinien ci go jakoś dostarczyć. Rozwiązań na to jest przynajmniej naście (np. podobnie do zadań możesz taki kod pobierać z bazy), natomiast jednym z najczęściej stosowanych jest użycie elementu template.

Spróbujmy je zastosować w naszej aplikacji. Dodajmy więc odpowiednie elementy do naszej strony (na końcu body):


<!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, shrink-to-fit=no">
        <script src="./js/app.js" defer></script>
    </head>
    <body>

        ...

        <template id="taskTemplate">
            <div class="task-inside">
                <div class="task-header">
                    <h3 class="task-date">{{date}}</h3>

                    <div class="task-actions">
                        <button class="task-edit" title="Edytuj zadanie">
                            Edytuj
                        </button>
                        <button class="task-delete" title="Usuń zadanie">
                            Usuń
                        </button>
                    </div>
                </div>

                <div class="row">
                    <div class="task-title">{{title}}</div>
                </div>

                <div class="row">
                    <div class="task-body">
                        {{body}}
                    </div>
                </div>
            </div>
        </template>

        <template id="taskEditTemplate">
            <div class="task-inside">
                <div class="task-header">
                    <label>
                        <span>Podaj datę</span>
                        <input type="date" class="task-date" value="{{date}}">
                    </label>

                    <div class="task-actions">
                        <button class="task-delete" title="Usuń zadanie">
                            Usuń
                        </button>
                    </div>
                </div>

                <div class="row">
                    <label>
                        <span>Tytuł</span>
                        <input type="text" class="task-title" value="{{title}}">
                    </label>
                </div>

                <div class="row">
                    <label>
                        <span>Treść</span>
                        <textarea class="task-body">{{body}}</textarea>
                    </label>
                </div>

                <div class="task-footer">
                    <button class="button task-edit-save">Zapisz</button>
                    <button class="button task-edit-cancel button-secondary">Anuluj</button>
                </div>
            </div>
        </template>
    </body>
</html>

a następnie użyjmy ich w funkcji generującej html dla zadania:


//_render.js

export function getTaskHTML(dataElement, editMode = false) {
    let html = '';
    if (editMode) {
        html = document.querySelector("#taskEditTemplate").innerHTML;
    } else {
        html = document.querySelector("#taskTemplate").innerHTML;
    }
    html = html.replaceAll("{{date}}", dataElement.date);
    html = html.replaceAll("{{title}}", dataElement.title);
    html = html.replaceAll("{{body}}", dataElement.body);
    return html
}

Tutaj mała uwaga. Nasz template trzymamy bezpośrednio w html. Dla zaznajomionego z webem użytkownika nie będzie żadnym problemem by go podmienić, i wstrzyknąć do niego potencjalnie szkodliwy kod. Żeby temu zapobiec, moglibyśmy na zakończenie powyższej obróbki potraktować go dodatkową biblioteką - np. DOMPurify. Wpierw przerwij działanie projektu (kilka razy Ctrl + C w terminalu),zainstaluj tą bibliotekę poleceniem npm i dompurify, a następnie dodaj do pliku _render.js:


//_render.js

import DOMPurify from 'dompurify';

...

export function getTaskHTML(dataElement, editMode = false) {
    let html = '';
    if (editMode) {
        html = document.querySelector("#taskEditTemplate").innerHTML;
    } else {
        html = document.querySelector("#taskTemplate").innerHTML;
    }
    html = html.replaceAll("{{date}}", dataElement.date);
    html = html.replaceAll("{{title}}", dataElement.title);
    html = html.replaceAll("{{body}}", dataElement.body);
    html = DOMPurify.sanitize(html);

    return html
}

Handlebars

Niestety z powyższym rozwiązaniem jest pewien problem. Obecnie mamy dwa stany - spoczynkowy i edycji. A gdyby takich stanów było nieco więcej - np 5? Oczywiście możemy stworzyć pięć oddzielnych templatów w html, ale może to później spowodować uciążliwą ich edycję, bo dodanie nowego elementu wymusi na nas edycję w 5 miejscach na raz. Zamiast tego możemy skorzystać tutaj z bardziej rozbudowanych templatek, które pozwalają w swoim kodzie używać pętli, ifów i dodatkowych instrukcji. Jedną z najbardziej popularnych jest handlebars.

Przerwij działanie projektu i zainstalują tą bibliotekę poleceniem npm i handlebars.

Następnie zamień powyższe templaty na jeden wspólny (odpowiednie helpery masz opisane tutaj):


<!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, shrink-to-fit=no">
        <script src="./js/app.js" defer></script>
    </head>
    <body>

        ...

        <template id="taskTemplate">
            <div class="task-inside">
                <div class="task-header">
                    {{#if editMode}}
                    <input type="date" class="task-date" value="{{date}}">
                    {{else}}
                    <h3 class="task-date">{{date}}</h3>
                    {{/if}}

                    <div class="task-actions">
                        {{#unless editMode}}
                        <button class="task-edit" title="Edytuj zadanie">
                            Edytuj
                        </button>
                        {{/unless}}
                        <button class="task-delete" title="Usuń zadanie">
                            Usuń
                        </button>
                    </div>
                </div>

                <div>
                    {{#if editMode}}
                    <input type="text" class="task-title" value="{{title}}">
                    {{else}}
                    <div class="task-title">{{title}}</div>
                    {{/if}}
                </div>

                <div>
                    {{#if editMode}}
                    <textarea class="task-body">{{body}}</textarea>
                    {{else}}
                    <div class="task-body">{{body}}</div>
                    {{/if}}
                </div>

                {{#if editMode}}
                <div class="task-footer">
                    <button class="button task-edit-save">Zapisz</button>
                    <button class="button task-edit-cancel button-secondary">Anuluj</button>
                </div>
                {{/if}}
            </div>
        </template>
    </body>
</html>

A następnie wykorzystanie go w funkcji generującej kod zadania:


//_render.js
const Handlebars = require("handlebars");

export function getTaskHTML({editMode, dataElement}) {
    const html = document.querySelector("#taskTemplate").innerHTML;
    const template = Handlebars.compile(html);
    return template({editMode, ...dataElement});
}

...

Obsługa błędów

W powyższych kodach zakładaliśmy, że każde połączenie do bazy danych zakończy się sukcesem, a przecież nie zawsze będzie to prawdą. Dodajmy więc obsługę błędów w postaci odpowiednich komunikatów dla użytkownika. Moglibyśmy tutaj użyć console.log, ale w moim odczuciu console jest dobre dla osób które wiedzą, że istnieje coś takiego jak debuger - czyli nei dla zwykłych użytkowników. Zamiast tego pokażmy błędy bezpośrednio na stronie. Wykorzystamy do tego bibliotekę toast-me. Podobnie do poprzednich zainstaluj ją poleceniem npm i toast-me, a następnie dodaj ją przy wyświetlaniu komunikatów:


//app.js
import toast from 'toast-me';

...

apiGetTasks().then(res => { //tu nie jesteśmy wewnątrz funkcji stąd nie używamy await
    renderTaskList(res);
}).catch(err => {
    toast(err, "error");
});

document.addEventListener("click", async e => {
    if (e.target.classList.contains("task-delete")) {
        const task = e.target.closest(".task");
        const id = +task.dataset.id;

        try {
            const request = await apiDeleteTask(id);
            store.taskList = store.taskList.filter(el => el.id !== id);

            const anim = task.animate([
                {height: `${task.offsetHeight}px`},
                {height: '0px'}
            ], {
                duration: 300,
                iterations: 1
            })
            anim.onfinish = e => {
                task.remove();
            }
        } catch(err) {
            toast(err, "error");
        }
    }

    if (e.target.classList.contains("task-edit")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        el.classList.add("task-edit-mode");
        el.innerHTML = getTaskHTML(dataElement, true);
    }

    if (e.target.classList.contains("task-edit-save")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        const title = el.querySelector(".task-title").value;
        const body = el.querySelector(".task-body").value;
        const date = el.querySelector(".task-date").value;
        const dataElement = {
            id, date, title, body
        }

        try {
            const request = await apiEditTask(dataElement);
            el.classList.remove("task-edit-mode");
            el.innerHTML = getTaskHTML(dataElement, false);
        } catch(err) {
            toast(err, "error");
        }
    }

    if (e.target.classList.contains("task-edit-cancel")) {
        const el = e.target.closest(".task");
        const id = +el.dataset.id;
        const title = el.querySelector(".task-title").value;
        const body = el.querySelector(".task-body").value;
        const date = el.querySelector(".task-date").value;
        const dataElement = {
            id, date, title, body
        }
        el.classList.remove("task-edit-mode");
        el.innerHTML = getTaskHTML(dataElement, false);
    }
})

const tHandler = debounced(500, async e => {
    try {
        const tasks = await apiSearchTasks(search.value)
        renderTaskList(tasks);
    } catch(err) {
        toast(err, 'error');
    }
});
const search = document.querySelector("#todoSearch");
search.addEventListener("input", tHandler);

const form = document.querySelector("#todoForm");
form.addEventListener("submit", async e => {
    e.preventDefault();

    const title = form.querySelector("#todoTitle").value;
    const date = form.querySelector("#todoDate").value;
    const body = form.querySelector("#todoMessage").value;

    if (title && date && body) {
        try {
            const dataElement = await apiAddTask({title, date, body});
            renderSingleTask(dataElement);
            form.reset();
        } catch(err) {
            toast(err, "error");
        }
    } else {
        //tu można dodać ewentualnie walidację
        toast("Wypełnij wszystkie pola", "error");
    }
})

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.