Bookmark Manager App Part 2

Pytania

Nie rozumiem jak to try…catch działa i item.name w ostatnim Test 23 jak to nie działa:

const updatedBookmarksList = bookmarksList.filter((item) => {
         return item.name !== bookmarkNameToDelete
}

 

Odpowiedzi

Twoja logic dla item.name !== bookmarkNameToDelete jest w 100% poprawna.

The 'try...catch' Syntax Issue

Próbujesz wrzucić Block Structure (try…catch) do środka Inline Condition (if ( … )). JavaScript na to nie pozwala.

 

Pomyśl o try…catch dokładnie jak o if…else Blocku.

On stoi samemu.

 

Jest to sposób na powiedzenie JavaScript’owi:

“Go into this Room (try) and do a dangerous Task.

If you mess up and drop the plates, don’t burn down the House, just walk into the other Room (catch) and clean it up safely.”

 

O to Structural Blueprint na to jak twój else Block powinien wyglądać w środku getBookmarks():

} else {

       // We have a String. Time to do the dangerous task of parsing it.

       try {

               // 1. Create a variable and try to parse the String:

               // let parsedData = JSON.parse(bookmarkArr)

 

// 2. Now do your normal if/else check on that parsedData:

// if (array.isArray(parsedData)) {

       return parsedData

  } else {

       return []

  }

 

} catch (error) {

      // 3. If JSON.parse FAILS (because the String is garbled),

      // JavaScript instantly abandons the try block and jumps down here

      // What should we return if the data is broke?

      return []

}

}

 

Warning about User Story 23 (Delete)

Gdy Syntax Error zniknie, to twój item.name !== … filter będzie poprawnie usuwał item z localStorage

 

Jednak, możesz wciąż mieć fail na ostatniej części testu: “and update the displayed bookmark list”

 

Spójrz na ostatnią Line twojej delete funkcji: viewCategoryBtn.click()

Jest to mądry pomysł żeby reuse’ować kod, ale zobaczmy co się dzieje gdy Button jest kliknięty:

  1. Bierze nowe dane
  2. Buduje nowy HTML
  3. Update’uje Ekran
  4. Potem, na samym dole twojego viewCategoryBtn Listenera, on call’uje displayOrHideCategory()

 

Co robi displayOrHideCategory() ? Toggle’uje .hidden class.

Z racji tego że obecnie patrzysz na Listę, to Lista nie jest hidden.

Call’owanie tej Funkcji niechcący zamknie Listę przed twoimi oczami odrazu po naciśnięciu Delete.

How to fix it:

Zamiast udawać Click Event z viewCategoryBtn.click(), to musisz skopiować logic która faktycznie update’uje Ekran.

 

1. Usuń viewCategoryBtn.click()

2. Weź filteredBookmarks logic i innerHTML String Building logic (Loop i if/else) z View Buttonu, 

i wklej ją na dół Delete Button Funkcji, używając twojej nowej updatedBookmarksList.

Pytania

Nie rozumiem co znaczy Branie logic od filteredBookmarks i jakiegoś innerHTML z View Buttonu?

Nie pamiętam żadnego View Buttonu.

Jaki Loop?

Nie mam żadnego loopa oprócz tego z forEach w środku else block?

Odpowiedzi

Gdy powiedziałem View Button, to miałem na myśli viewCategoryBtn

Gdy powiedziałem Loop, to dokładnie chodziło mi o forEach block.

 

Spójrz na sam koniec deleteBookmarkBtn Event Listenera.

 

Prawidłowo zrobiłeś Hard Part:

  1. Znalazłeś item do usunięcia
  2. Użyłeś .filter() żeby stworzyć updatedBookmarkList (co usunęło item)
  3. Zapisałeś updatedBookmarksList do localStorage

The Final Problem:

Ekran sam w sobie (HTML) automatycznie nie wie że localStorage się zmieniło.

Usunięty Radio Button wciąż siedzi na Ekranie.

Musisz powiedzieć JavaScript’owi żeby zbudował od nowa tą określoną część HTML.

Why 'viewCategoryBtn.click()' almost worked, but didn't:

Spróbowałeś striggerować viewCategoryBtn żeby przebudowało HTML za ciebie. 

 

Ale spójrz na ostatnią linijkę twojego viewCategoryBtn Event Listenera. (Line 97):

displayOrHideCategory()

 

Jeśli zrobisz symulację kliknięcia tego Buttona, to przebuduje twoją Listę,

ale potem odrazu odpali displayOrHideCategory(), co dodaje “hidden” class i sprawia że cała Lista znika z Ekranu odrazu.

How to fix the end of your Delete Function:

Zamiast udawać kliknięcie, to poprostu musisz odwzorować Visual Listę manualnie tam na samym dole twojej Delete Funkcji.

 

Spójrz na kod który napisałeś w środku viewCategoryBtn Event Listenera.

Już napisałeś perfect logic żeby zbudować ekran.

Teraz tylko musisz zrobić to znowu tutaj, ale używając Updated Data.

 

Blueprint żeby napisać na dole twojego deleteBookmarkBtn Event Listenera, zamiast symulacji click eventu:

1. Filter for the Current Category:

Twoja updatedBookmarkList ma wszystkie bookmarks.

Musisz odfiltrować ją żebyś tylko patrzył na te dla category obecnie wybranej w Dropdown’ie.

Już to zrobiłeś używając .filter i categoryDropdown.value

2. Check if its empty:

Jeśli user dopiero co usunął ostatni bookmark w tej category, to twój filtered Array będzie miał length 0.

Jeśli tak, to ustaw categoryList.innerHTML na <p>No Bookmarks Found</p>

3. Rebuild the HTML:

Jeśli nie jest Empty (else), to zrób dokładnie co robiłeś wcześniej:

  1. Stwórz empty htmlString variable
  2. Użyj .forEach() na twojej Filtered List żeby dodać <input>, <label>, i <a> Tags do tego Stringu
  3. Finalnie, ustaw categoryList.innerHTML = htmlString

 

Już raz napisałeś tą logic. 

Teraz musisz zrobić reusing jej żeby refresh’ować Ekran po usunięciu.

Pytania

Wciąż mam te 5 błędów.

Odpowiedzi

Dobra robota, try…catch Block jest perfect.

Umknęły ci 2 małe fixes o których mówiłem.

1. The Root Cause: Fixing Test 11 (The Save Bug)

Obecnie, twoja App kompletnie fail’uje zapisywanie Nowych Bookmarks prawidłowo.

Ponieważ zapisuje złe dane, to też pewnie przez to Test 5 fail’uje.

 

Spójrz na tą Line w środku twojego addBookmarkFormBtn Listenera:

localStorage.setItem(JSON.stringify(updatingBookmark.push({name: nameInput.value, category: categoryDropdown.value, url: url.value})))

 

Są tutaj 2 duże Errors i musimy je naprawić.

Usuń całkowicie tą Linijkę i zamień ją na 2 oddzielne:

Step A:

Update’uj Array w Memory najpierw. 

Pamiętaj, .push() return’uje Number, a nie Array,

więc nie możemy włożyć go do środka JSON.strinigfy

updatingBookmark.push({name: nameInput.value, category: categoryDropdown.value, url: url.value})

 

Step B:

Zapisz Updated Array do localStorage.

Pamiętaj, że setItem wymaga Dwóch Arguments:

  • String Key Name “bookmarks”
  • Dane

localStorage.setItem(“bookmarks”, JSON.stringify(updatingBookmark))

 

2. Fixing Tests 17 & 22 (The Erasing Bug)

Spójrz na swój viewCategoryBtn Listener. Spójrz na Lines 95-103.

  1. Jeśli jest Empty, to ustawiasz <p> Tag czyli git.
  2. Jeśli nie jest, to budujesz htmlString i też git
  3. Na Line 106, zaraz przed displayOrHideCategory(), masz taką Linijkę: categoryList.innerHTML = htmlString

The Bug:

Jeśli Array jest empty, to JavaScript ustawia <p> Tag, potem skip’uje else Block, a potem odrazu runnuje Line 106.

Ponieważ htmlString jest empty (“”), to odrazu usuwa <p> Tag który dopiero co zrobiłeś.

The Fix:

Chcesz tylko assign’ować htmlString jeśli faktycznie go zbudowałeś.

 

Przenieś tą Linę categoryList.innerHTML = htmlString do góry o jedną linijkę,

żeby siedziało w środku na dole twojego else Blocku, zaraz pod forEach Loopem.

 

Note:

Musisz zrobić tą samą zmianę w Delete Button Listenerze, bo skopiowałeś Logic.

3. Fixing Test 23 (The Delete Button Variable Bug)

Poprawnie skopiowałeś UI Update logic na dół twojego Delete Buttonu.

Ale jest mały Error z Variable Name, który crashuje to wszystko.

 

Spójrz na dół twojego deleteBookmarkBtn Listenera:

const filteredBookmarks = allBookmarks.filter((object) => {

         object.category === categoryDropdown.value

})

 

The Bug:

Skąd masz allBookmarks? To nie istnieje w tej Funkcji.

Użyłeś tego w viewCategoryBtn Funkcji, ale tutaj jest undefined, przez co JavaScript crashuje.

The Fix:

Nie chcesz filtrować Starej Listy i tak, bo chcesz filtrować Nową Listę którą dopiero co stworzyłeś.

Zmień allBookmarks na updatedBookmarksList, a to jest Variable które dopiero co stworzyłeś kilka linijek wyżej.

Checklist for this round:

  1. Podziel setItem / push na 2 Oddzielne Linijki
  2. Przenieś categoryList.innerHTML = htmlString do środka else Blocku w obu UI Update Areas.
  3. Zmień allBookmarks na updatedBookmarksList na dole twojej Delete Funkcji.

Pytania

Dlaczego musiałem usunąć tą 1 Linijkę i podzielić ją na 2 Oddzielne Linijki?

Zrobiłem wszystkie zmiany, ale wciąż mam 5. i 23. Errors.

Odpowiedzi

The "Why": Why did we split .push() and setItem into 2 Lines?

Jest to klasyczny JavaScript Trap. Chodzi o to co methods return’ują.

 

Gdy piszesz Funkcję, to używasz return Keyword żeby wyrzucić Final Value.

Wbudowane JavaScript methods też to robią.

  • Jeśli runnujesz localStorage.getItem(“key”), to return’uje String.
  • Jeśli runnujesz “hello”.toUpperCase(), to return’uje “HELLO”

 

Więc, co array.push({name: “test”}) będzie return’ować?

Logicznie, to pomyślałbyś że return’uje Updated Array, co nie? No właśnie nie.

 

.push() method modyfikuje twój Array Behind the Scenes, ale method sama w sobie return’uje New Length of the Array as a Number.

 

Jeśli masz let arr = [“A”] i runnujesz arr.push(“B”), to JavaScript modyfikuje Array, ale Funkcja wyrzuca Number 2.

Your Old Code:

localStorage.setItem(“bookmarks”, JSON.stringify( updatingBookmark.push(…) ))

 

Ponieważ .push() ewaluuje do Numberu, to JavaScript czytało twój kod tak:

localStorage.setItem(“bookmarks”, JSON.stringify( 1 ))

 

Niechcący zapisywałeś Number “1” do localStorage zamiast twoich danych.

 

Poprzez rozdzielenie tego w 2 Lines:

  • Line 1 update’uje Array w Memory.
  • Line 2 bezpiecznie przerabia na String to Array Variable.

Tackling Test 23 (The Delete Logic Bug)

Spójrzmy na słowa w User Story 14:

…delete the bookmark corresponding to the selected radio button and appropriate category...”

The Flaw in your logic:

Twój filter wygląda tak:

return item.name !== bookmarkNameToDelete

 

Wyobraź sobie że user ma x2 Bookmarks z tym samym name:

  1. Name: “Google”, Category: “News”
  2. Name: “Google”, Category: “Work”

 

Jeśli user spojrzy na “News” category i kliknie delete na “Google”

to twój .filter() sprawdza każdy Item i mówi: “Is the name Google? Yes? DELETE IT!”

 

Twój kod niechcący usuwa oba bookmarks, bo sprawdził tylko name.

The Fix:

Musisz sprawić żeby twój filter był bardziej rygorystyczny.

Chcesz tylko odfiltrować (usunąć) Item jeśli zarówno name match’uje Radio Button AND category match’uje Dropdown.

Logic:

W środku .filter(), chcesz return’ować true (zostawić Item), jeśli item.name NIE równa się Radio Button’owi,

OR jeśli item.category NIE równa się Dropdown Value.

 

Spróbuj update’ować Line 112 żeby sprawdzało VS oba Variables używając OR operatora.

Tackling Test 5 (The "Garbage Data" Bug)

Twoja getBookmars() Funkcja obecnie dobrze radzi sobie z null.

Zajmuje się broken Syntaxem używając try…catch również.

Więc dlaczego Test 5 fail’uje?

 

Spójrzmy na słowa: “When the bookmarks Key… does not contain a Valid Array of bookmark Objects…”

User Story 2: “Each bookmark object should have x3 Keys: name, category, and url

The Flaw:

A co jeśli Testy specjalnie inject’ują Valid Array, ale pełny garbage data żeby oszukać twój kod?

Na przykład, co jeśli inject’uje: [1, 2, 3] ?

  1. JSON.parse działa dobrze i nie crashuje.
  2. Array.isArray return’uje true.
  3. Twój kod return’uje [1, 2, 3]

 

Test fail’uje bo [1, 2, 3] to Array of Numbers, a nie Array of Bookmark Objects.

The Fix:

Zaraz po tym jak weryfikujesz czy to jest Array, to musisz zweryfikować czy ten Array faktycznie zawiera Objects z Keys których oczekujesz.

Jeśli zawiera garbage, to return’ujesz [ ].

 

W środku twojej getBookmarks Funkcji, zaraz pod if (Array.isArray(parsedData)) {, dodaj Check.

Możesz spojrzeć na pierwszy Item w Arrayu żeby zobaczyć czy ma .name Property.

if (Array.isArray(parsedData)) {

            // If the array has items, but the first item is missing the ‘name’ property, it’s Garbage Data

            if (parsedData.length > 0 && parsedData[0].name === undefined) {

                           return [ ]

             }

            // Otherwise, it’s a good Array, return it

            return parsedData

}

 

Pytania

Wciąż nie działa

Odpowiedzi

1. Fixing Test 23: The "Wrong Filter"

W poprzedniej wiadomości, powiedziałem ci żebyś dodał || OR logic do Delete filter.

Dobrze napisałeś logic, ale włożyłeś ją do złego filtru.

 

Spójrz uwaznie na 2nd Screenshot (Lines 139-141)

Dałeś delete logic do środka filteredBookmarks.

 

Pamiętaj co te 2 różne filters mają robić w środku Button Listenera:

Filter 1 (updatedBookmarksList): The "Delete" Filter

Ten filter patrzy na całą Database. Jego zadaniem jest znalezienie Jednego Określonego Itemu który user kliknął i usunięcie go.

 

Tutaj musisz dać ten Strict Check.

Zachowaj Item jeśli name nie match’uje Radio Buttona, OR jeśli category nie match’uje Dropdown’u.

Filter 2 (filteredBookmarks): The "Screen" Filter

Jedynym zadaniem tego filtru jest zdecydowanie co zostaje narysowane na Ekranie w tym momencie.

Jeśli user patrzy na “News” Tab, to on poprostu bierze “News” bookmarks.

 

Powinien zostać taki jak był wcześniej: 

return object.category === categoryDropdown.value

 

The Scope Bug (Line 140):

Ponieważ dałeś Delete logic do środka Filter 2, to napisałeś item.category !== …

 

Ale spójrz na Line 139: Ty nazwałeś to Variable jako object dla tego Loop’a.

JavaScript crashuje tutaj, bo mówi “What is item? I only know about object

The Fix:

1. Napraw Screen Filter (lines 139-141) żeby tylko sprawdzał category:

const filteredBookmarks = updatedBookmarksList.filter((object) => {

         return object.category === categoryDropdown.value

})

 

2. Daj to OR logic do środka Delete Filter.

Użyj item dla obu checks z racji tego że tak nazwałeś Variable dla tego Loop’a:

const updatedBookmarksList = bookmarksList.filter((item) => {

         return item.name !== bookmarkNameToDelete || item.category !== categoryDropdown.value

})

 

2. Fixing Test 5: The "Incomplete Data" Trap

Twoja logic do sprawdzania jest git: parsedData[0].name === undefined

 

Więc dlaczego fail’uje? Pamiętasz User Story 2: “Each bookmark object should have three keys: name, category, and url.

 

Test prawdopodobnie próbuje oszukać twój kod poprzez injectowanie Arrayu który wygląda tak: [{ name: “Google” }]

Ponieważ ma name, to twój kod mówi “Looks good to me!” i return’uje go.

 

Ale Testy fail’ują bo brakuje mu URL i Category, co oznacza że nie jest to Valid bookmark Object według zasad.

The Fix:

Musisz powiększyć IF Statement żeby sprawdzał czy istnieją te 3 Keys.

Jeśli któregoś brakuje, to return [ ].

 

Możesz połączyć to wszystko z OR.

 

Update’uj Line 34 w twojej getBookmarks funkcji jak tu:

if (parsedData.length > 0 && (parsedData[0].name === undefined || parsedData[0].category === undefined || parsedData[0].url === undefined)) {

            return [ ]

}

 

Pytania

Dlaczego parsedData[0].category gdy category jest jako drugie property i to samo z url?

Odpowiedzi

Kurwa sam sobie odpowiem,

Bo parsedData to Array, a [0] to Cały Object który tam siedzi,

czyli Array => [0] = Object =>  [0].property = Object.property

Why parsedData[0]

Dobre pytanie, i to że zastanawiasz się nad tym pokazuje że masz logical mind.

Mylisz jak Arrays działają z tym jak Objects działają.

 

Myślisz że parsedData wygląda tak (Array of Strings):

[“MyWebsite”, “News”, “https://website.com”]

 

Jeśli by to tak wyglądało to miałbyś rację, wtedy [0] to byłoby name, a [1] to category.

 

Ale parsedData to Array of Objects.

Wygląda tak:

[

        // Index 0: The FIRST Entire Bookmark

       {

            name: “Google”,

            category: “News”,

            url: “https://google.com”

         },

 

         // Index 1: The SECOND Entire Bookmark

        {

             name: ‘Reddit”,

             category: “Fun”,

             url: “https://reddit.com”

         }

]

 

Gdy piszesz parsedData[0], to nie pytasz o name.

Pytasz JavaScript’a żeby wziął Cały Pierwszy Box (Cały Object).

 

Gdy już trzymasz ten First Box (parsedData[0]), to patrzysz do jego środka:

  • parsedData[0].name –> Patrzysz do środka First Boxa i szukasz name
  • parsedData[0].category –> Patrzysz do środka tego samego First Boxa i szukasz category
  • parsedData[0].url –> Patrzysz do środka tego samego First Boxa i szukasz url

 

Dlaczego sprawdzamy tylko First Box ([0]) ?

Ponieważ jeśli Test da nam Garbage Dane, to tylko musimy sprawdzić First Item żeby ogarnąć że File jest corrupted i odrzucić dane.