Předvánoční slevová akce PHP týden
Pouze tento týden sleva až 80 % na PHP e-learning!
Využij předvánočních slev a získej od nás 20 % bodů zdarma! Více zde

Lekce 7 - Vylepšení objektového diáře v JavaScriptu

Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem.
Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Objekty, JSON a vylepšení diáře v JavaScriptu, jsme si vylepšili náš diář o ukládání do localStorage. Dnes doplníme řazení záznamů podle data, seskupování záznamů se stejným datem a odstranění záznamů.

Výpis záznamů

Prvně si upravíme výpis našich záznamů. Bylo by vhodné záznamy seřadit dle data a seskupit záznamy ve stejný den, které vypíšeme pod sebou v jednom bloku.

Řazení

Nejdříve si naimplementujeme metodu na řazení záznamů. K tomu použijeme metodu sort() na poli. Ta dokáže pole seřadit postupným porovnáváním dvojic prvků. Jako parametr přijímá porovnávací funkci, která definuje jakým způsobem se mají 2 prvky v poli porovnat. Naše metoda k seřazení záznamů v poli podle data bude ve třídě Diar vypadat následovně:

seradZaznamy() {
    this.zaznamy.sort(function (zaznam1, zaznam2) {
        return (new Date(zaznam1.datum) - new Date(zaznam2.datum));
    });
}

Funkce porovnává data dvou záznamů, která si naparsujeme na datum pomocí konstruktoru objektu Date. Ze záznamu samozřejmě vybereme vlastnost datum. Porovnání dvou datumů provedeme jednoduše pomocí operátoru -, čímž se vrátí jejich rozdíl v milisekundách. Pokud bude první datum až po druhém, vrátí se záporné číslo. Pokud budou stejné, vrátí se 0. Jinak se vrátí kladné číslo. Právě kladné, záporné nebo nulové číslo metoda sort() pro svou práci potřebuje a tím zjistí, zda je datum větší, menší nebo rovné.

Metodu budeme volat před každým výpisem záznamů:

vypisZaznamy() {
    this.seradZaznamy();
    this.vypisElement.innerHTML = "";
    for (let i = 0; i < this.zaznamy.length; i++) {
        const zaznam = this.zaznamy[i];
        this.vypisElement.innerHTML += `<h3>${zaznam.nazev}</h3>kdy: ${zaznam.datum}<br>splněno: ${zaznam.splneno}`;
    }
}

Seřazení bychom mohli také volat po přidání záznamu a po jejich načtení, aby se nemuselo volat při každém výpisu. Nevýhodou tohoto řešení by ovšem bylo, že bychom na něj mohli zapomenou při jiné manipulaci se záznamy.

Pokud nyní diář spustíme a máme v localStorage již nějaká data, vypíší se nám již seřazená podle datumu.

Seskupování

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Nyní výpis záznamů dokončíme. Budeme tedy vypisovat datum a k tomuto datumu vždy všechny záznamy v daný den. Náš cyklus lehce upravíme:

vypisZaznamy() {
    this.seradZaznamy();
    this.vypisElement.innerHTML = "";
    let posledniDatum = null;
    for (const zaznam of this.zaznamy) {
        if (zaznam.datum !== posledniDatum) {
            this.vypisElement.innerHTML += `<h3>${zaznam.datum}</h3>`
        }
        posledniDatum = zaznam.datum;

        this.vypisElement.innerHTML += `<strong>${zaznam.nazev}</strong><br>splněno: ${zaznam.splneno}<hr>`;
    }
}

Do proměnné posledniDatum přiřadíme vlastnost datum z předchozího záznamu. Protože při prvním průběhu cyklu poslední záznam ještě není, nastavíme proměnnou prvně na null. Datum současného záznamu poté vypíšeme jen pokud se liší od předchozího. Tak se záznamy ve stejný den budou seskupovat. Nyní máme vypsané hezky úkoly se stejným datem pod sebou a seřazené:

Your page
localhost

Formátování data a splněnosti

Jelikož formát data a splněnost úkolu také není ideální, je třeba tyto výpisy upravit do "lidštější" podoby :) Pamatujete na vlastnost jazyk, kterou můžeme ovlivnit v konstruktoru? Nyní jí využijeme k lepšímu výpisu data a zároveň si vylepšíme výpis splněnosti úkolů:

vypisZaznamy() {
    this.seradZaznamy();
    this.vypisElement.innerHTML = "";
    let posledniDatum = null;
    for (const zaznam of this.zaznamy) {
        if (zaznam.datum !== posledniDatum) {
            const datum = new Date(zaznam.datum).toLocaleDateString(this.jazyk, {
                weekday: "long",
                day: "numeric",
                month: "short",
                year: "numeric"
            });
            this.vypisElement.innerHTML += `<h3>${datum}</h3>`
        }
        posledniDatum = zaznam.datum;

        this.vypisElement.innerHTML += `<strong>${zaznam.nazev}</strong><br>úkol ${!zaznam.splneno ? "ne" : ""}splněn<hr>`;
    }
}

Vytvoříme instanci objektu Date z našeho data a použijeme její metodu toLocaleString(), kam předáme jako první parametr vlastnost jazyk naší třídy a jako druhý parametr formátovací objekt, jehož vlastnosti udávají jak přesně se má datum vypsat.

U splněnosti úkolu jsme použili jednoduchý ternární operátor, podle nesplněnosti přidáme "ne" nebo prázdný string.

Výsledek nyní vypadá takto:

Your page
localhost

Mazání záznamů

Budeme pokračovat základní interakcí s našimi úkoly, budeme je moci mazat, nebo je označit jako splněné. Začneme mazáním.

Uložení záznamů

Jelikož po smazání bude nutné záznamy znovu uložit, vyčleňme si uložení záznamů z metody nastavUdalosti() do samostatné metody ulozZaznamy():

ulozZaznamy() {
    localStorage.setItem("zaznamy", JSON.stringify(this.zaznamy));
}

V metodě nastavUdalosti() nyní metodu ulozZaznamy() zavoláme:

nastavUdalosti() {
    this.potvrditButton.onclick = () => { // this zůstane nyní stále this
        const zaznam = new Zaznam(this.nazevInput.value, this.datumInput.value);
        this.zaznamy.push(zaznam);
        this.ulozZaznamy();
        this.vypisZaznamy();
    };
}

Tlačítko

Ke každému záznamu vygenerujeme tlačítko na jeho odstranění. To vytvoříme jako nový element <button> pomocí metody document.createElement() a do <div>u s výpisem záznamů jej vložíme pomocí appendChild(). Tlačítku rovněž přidáme událost reakce na kliknutí, kdy daný záznam odstraníme z pole, záznamy takto přeuložíme do localStorage a znovu vypíšeme. Metoda vypisZaznamy() bude po přidání mazacího tlačítka vypadat takto:

vypisZaznamy() {
    this.seradZaznamy();
    this.vypisElement.innerHTML = "";
    let posledniDatum = null;
    for (const zaznam of this.zaznamy) {
        if (zaznam.datum !== posledniDatum) {
            const datum = new Date(zaznam.datum).toLocaleDateString(this.jazyk, {
                weekday: "long",
                day: "numeric",
                month: "short",
                year: "numeric"
            });
            this.vypisElement.innerHTML += `<h3>${datum}</h3>`
        }
        posledniDatum = zaznam.datum;

        this.vypisElement.innerHTML += `<strong>${zaznam.nazev}</strong>
        <br>úkol ${!zaznam.splneno ? "ne" : ""}splněn`;
        const smazatButton = document.createElement("button");
        smazatButton.onclick = () => {
            if (confirm("Opravdu si přejete odstranit úkol?")) {
                this.zaznamy = this.zaznamy.filter(z => z !== zaznam); // Ponechá vše co není rovné proměnné zaznam
                this.ulozZaznamy();
                this.vypisZaznamy();
            }
        };
        smazatButton.innerText = "Smazat záznam";
        this.vypisElement.appendChild(smazatButton);
        this.vypisElement.innerHtml += "<br>";
    }
}

Všimněte si použití potvrzujícího dialogu confirm(), odstranění záznamu je určitě akce, kterou nechceme udělat omylem :)

Možná by vás napadlo vložit tlačítko rovnou do HTML kódu jako text a přiřadit mu do data atributu index záznamu v poli, který má smazat. Taková tlačítka by se poté někde všechna vybrala a obsloužila tak, aby z pole smazala prvek pod daným indexem. Problém by ovšem nastal, kdybychom diář otevřeli ve více záložkách najednou. Když bychom v jedné záložce smazali nějakou položku a druhou záložku neobnovili, tato položka by zde stále byla, ale v localStorage by pod tímto indexem již byla položka jiná. Mohli bychom tak na neobnovené záložce nechtěně smazat jiný úkol. Proto budeme veškerou manipulaci s položkami vždy dělat přímo pomocí anonymních funkcí, kam tuto jednu konkrétní položku předáme.

Pokud kroutíte hlavou nad kódem odstraňujícím položku:

this.zaznamy = this.zaznamy.filter(z => z !== zaznam);

Je to v současné době bohužel nejjednodušší způsob, jak v JavaScriptu smazat prvek v poli, jehož index neznáme a nechceme jej zbytečně zjišťovat. Kód profiltruje dané pole tak, že v něm zůstanou jen záznamy, které se nerovnají záznamu, který chceme odstranit.

Pokračovat budeme zas příště, v lekci Dokončení objektového diáře v JavaScriptu, kdy přidáme tlačítko na splnění úkolu, validaci data a diář dokončíme přidáním jednoduchých CSS stylů :)


 

 

Aktivity (5)

 

 

Komentáře
Zobrazit starší komentáře (1)

Avatar
Patrik Pastor:13. dubna 21:07

oprava, jede to ALE POUZE U POSLEDNIHO ZAZNAMU, proto jsem si myslel ze t nejede , kdyz jsem chtel mazat z prostredka... Jak to ale jde udelat, nebo jak to, ze zaznam lze mazat pouze posledni?

 
Odpovědět
13. dubna 21:07
Avatar
Martin
Člen
Avatar
Martin :29. července 15:35

Funguje vám řazení? Musel jsem zadávat datum ve formátu yyyy-mm-dd a pak následně upravit metodu serad zaznamy viz níže.....měl někdo podobný problém?

seradZaznamy() {
this.zaznamy.sor­t(function (zaznam1,zaznam2){
return (new Date(zaznam1.da­tum).getTime() - new Date(zaznam2.da­tum).getTime());
});
}

Editováno 29. července 15:38
 
Odpovědět
29. července 15:35
Avatar
Jakub Podskalský:27. října 15:26

Zdravím. Kód nefunguje, nefunguje tlačítko "smazat záznam". Respektive se ani nezobrazí ten confirm. Snažil jsem se na to přijít sám, ale nenapadá mě příčina. Jshint v Sublime Textu mi hází jshint: warning W083 - Functions declared within loops referencing an outer scoped variable may lead to confusing semantics. (confirm). To se ale nezdá jako striktní problém. V další lekci jsou ohledně toho komentáře, nevidím tam ale žádné jasné řešení a nechce se mi pokračovat s kódem, když je nefunkční. Opravdu bych uvítal jakoukoliv pomoc. Díky. :)

 
Odpovědět
27. října 15:26
Avatar
Šimon Raichl
Překladatel
Avatar
Odpovídá na Jakub Podskalský
Šimon Raichl:27. října 16:21

Ahoj, vim o tom, tenkrat mi David dost zmenil kod pri korekture, ktery fungoval spravne, a pak hodne lidi zacalo psat, ze jim to nejde. Mozna bych se na to mel uz kouknout. :D

 
Odpovědět
27. října 16:21
Avatar
Odpovídá na Šimon Raichl
Jakub Podskalský:27. října 16:35

Aha. :D Díky za rychlou odpověď. Když opomenu tu nefunkčnost, tak se mi tenhle úvod do OOP dost líbí. :) A skvěle vybraný projekt, na kterém se to demonstruje.
Takže tedy prosím o tu opravu, je to škoda. :D

 
Odpovědět
27. října 16:35
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Šimon Raichl
Překladatel
Avatar
Odpovídá na Jakub Podskalský
Šimon Raichl:27. října 16:43

Ted jsem na to koukal a je tam chyba v tom, ze se tam primo appenduje pomoci innerHTML nejaky HTMLko, jenze to ti samozrejme prekresli ten element a tim padem se ztrati eventy ze vsech elementu, to je taky duvod, proc to funguje jenom na poslednim zaznamu. Muzu ti sem poslat cely opraveny kod rovnou:

class Diar {

    constructor(jazyk = "cs-CZ") {
        const zaznamyZeStorage = localStorage.getItem("zaznamy");
        this.zaznamy = zaznamyZeStorage ? JSON.parse(zaznamyZeStorage) : [];
        this.jazyk = jazyk;

        this.nazevInput = document.getElementById("nazev");
        this.datumInput = document.getElementById("datum");
        this.potvrditButton = document.getElementById("potvrdit");
        this.vypisElement = document.getElementById("seznam-ukolu");

        this._nastavUdalosti();
    }

    _nastavUdalosti() {
        this.potvrditButton.onclick = () => { // this zůstane nyní stále this
                                if (this.datumInput.value !== "") {
                                        const zaznam = new Zaznam(this.nazevInput.value, this.datumInput.value);
                                        this.zaznamy.push(zaznam);
                                        this.ulozZaznamy();
                                        this.vypisZaznamy();
                                } else
                                        alert("Musíte vyplnit datum!");
                                };
    }

        seradZaznamy() {
                this.zaznamy.sort(function (zaznam1, zaznam2) {
                        return (new Date(zaznam1.datum) - new Date(zaznam2.datum));
                });
        }

        vypisZaznamy() {
                this.seradZaznamy();
                this.vypisElement.innerHTML = "";
                let posledniDatum = null;
                for (const zaznam of this.zaznamy) {
                        const kartaZaznamu = document.createElement("div");

                        if (zaznam.datum !== posledniDatum) {
                                const datum = new Date(zaznam.datum).toLocaleDateString(this.jazyk, {
                                        weekday: "long",
                                        day: "numeric",
                                        month: "short",
                                        year: "numeric"
                                });
                                kartaZaznamu.innerHTML += `<h3>${datum}</h3>`
                        }
                        posledniDatum = zaznam.datum;

                        kartaZaznamu.innerHTML += `<h4>${zaznam.nazev}</h4><br>úkol ${!zaznam.splneno ? "ne" : ""}splněn`;

                        this.vypisElement.appendChild(kartaZaznamu);

                        this._pridejTlacitko("Smazat", () => {
                                if (confirm("Opravdu si přejete odstranit úkol?")) {
                                        this.zaznamy = this.zaznamy.filter(z => z !== zaznam); // Ponechá vše co není rovné proměnné zaznam
                                        this.ulozZaznamy();
                                        this.vypisZaznamy();
                                }
                        });

                        this._pridejTlacitko("Označit jako " + (zaznam.splneno ? "nesplněný" : "splněný"), () => {
                                zaznam.splneno = !zaznam.splneno;
                                this.ulozZaznamy();
                                this.vypisZaznamy();
                        });
                }
        }

        _pridejTlacitko(titulek, callback) {
                const button = document.createElement("button");
                button.onclick = callback;
                button.innerText = titulek;
                this.vypisElement.appendChild(button);
        }

        ulozZaznamy() {
                localStorage.setItem("zaznamy", JSON.stringify(this.zaznamy));
        }

}
 
Odpovědět
27. října 16:43
Avatar
Jakub Podskalský:27. října 19:06

Moc Ti děkuji, žes mi to hned takhle poslal. Ale stejně se musím možná hloupě zeptat... Jakým způsobem mi to překreslí ten element (a tím se myslí vypisElement, že?) a tím pádem se ztratí i všechny ostatní eventy? Když se to innerHTML přidává pomocí +=, tak jak se může něco překreslit, když se to pouze přidává k aktuálnímu HTML? Nechápu to. :( Třeba by stačilo jen trochu navést... Fakt mi to vrtá hlavou.

 
Odpovědět
27. října 19:06
Avatar
Šimon Raichl
Překladatel
Avatar
Odpovídá na Jakub Podskalský
Šimon Raichl:27. října 20:47

Rozumim tomu, ze si myslis, ze jen "pridavas HTML", ale takhle to bohuzel nefunguje. Ty vlastne prirazujes puvodni HTML + to tvoje HTML, takze to funguje takto:

element.innerHTML = element.innerHTML + someHtml;

Tim samozrejme prekreslis cely element a znicis puvodni elementy s jejich eventy a nahradis za tvoje HTML, kde tvoje elementy zadne eventy nastaveny nemaji.

Na dynamicky checkovani zmen v DOMu muzes pouzit MutationObserver a do callbacku si hodit to, ze nastavis event listenery nejakym elementum. Neco u MutationObserveru si muzes precist zde: https://developer.mozilla.org/…tionObserver

 
Odpovědět
27. října 20:47
Avatar
Šimon Raichl
Překladatel
Avatar
Odpovídá na Jakub Podskalský
Šimon Raichl:27. října 20:54

Jeste kratky dodatek: zkus si treba ve webovy konzoli tady na itnetworku spustit:

document.body.innerHTML += "";

Ted si zkus otevrit chat. Nepujde ti to. :D Asi by sis rekl, ze prece jen pridavas prazdny string, ale jak jsem psal nahore, tak to neni.

Jinou veci by bylo pouzit funkci insertAdjacentHTML, jako prvni parametr das pozici kam v elementu chces HTML vlozit a jako druhy parametr tvoje HTML.

document.body.insertAdjacentHTML("beforeend", "");

Tohle uz ti nic nerozbije. :D

 
Odpovědět
27. října 20:54
Avatar
Odpovídá na Šimon Raichl
Jakub Podskalský:27. října 22:53

Už to chápu! :D Já jsem se ještě nikdy s takovým chováním totiž nesetkal. Bral jsem to tak, že se to HTML zkopíruje doslova celé včetně eventů jeho elementů. Vyzkoušel jsem si to ještě vedle na jednodušším příkladu a opravdu to funguje tak, jak říkáš.
Ještě jsem se kouknul na ten tvůj kód a pokud jsem dobře pochopil, tak je to řešeno tím, že se dají jednotlivé záznamy ještě do kontejnerů. To asi udělám taky... než používat tu Tvojí ukázkovou, méně známou metodu. :D Především pro budoucí lepší manipulaci s jednotlivými záznamy. Ovšem taky díky za zajímavost.
Ještě jednou díky za odpovědi a pomoc. Teď už to jdu přepsat na funkční verzi. :)

 
Odpovědět
27. října 22:53
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 11. Zobrazit vše