Lab 11 - Card Viewer

Make a viewer (and editor) for your greeting cards!

Greeting Cards, Part 2

In the previous lab, you made a page that lets users create greeting cards and save them to localStorage. But we need a way to view the cards, edit them, and delete them (read, update, and delete in the CRUD operations).

Let’s create a card viewer that displays the data for each card, with a way to edit and delete each one.

Getting Started

  1. Start with your code from the previous lab
    • The assignment is available in GitHub Classroom using your instructor-provided link (in Canvas)
    • Let your instructor know if you do not have a working card-creator.html or createcard.js
  2. Here are some more hints for completing the previous lab:
    • localStorage.setItem("cards", ...) will completely replace the existing item in localStorage (meaning you will override the previous array of cards)
    • Thus, you need to make sure to load the existing array of cards with localStorage.getItem("cards"), push a new item to that array, and then save the whole array back to localStorage
    • Don’t forget that localStorage stores values as strings
      • To convert an array or object to a string: var a_string = JSON.stringify(an_object);
      • To convert a string back to an array or object: var an_object = JSON.parse(a_string);

Open up the card-viewer.html and scripts/viewcards.js files. You will be working with these for the lab.

Loading and Looping

First, we will need to load the array of cards from localStorage so that we can loop over each card and display it.

  1. In viewcards.js, use localStorage to get the item with the key "cards" and convert it back to an array. Store this to a variable or constant called cards.

    • Don’t forget to use JSON.parse(...)!
  2. Next, use a “traditional” for loop to loop over the array (using indices – this will be important later!)

    • Also important: use let instead of var for your variables
    • Recommended: save cards[i] to a variable called card to make it easier to reference
  3. Inside the loop, use console.log(...) to print the current card object in the iteration.

  4. Test your page in the browser and open the developer console

    • FYI, you should always be testing your code like this to make sure there are no JavaScript errors!
  5. You should see something like this:

Creating Multiple Cards

The next task you will tackle is to display a card for each item in the array. In the HTML, you will find a <main> element with the id of card-list. All the card displays should be a child of this element.

One way to do this is to create all the elements manually in JavaScript (using document.createElement(...)) to form parts of a card, and then add it to the card list. This, however, is not maintainable. We should be able to define what we want a single card to look like in HTML and CSS and then reuse that code for each card.

The <template> element makes our lives much easier. It lets us write HTML that doesn’t render in the browser, but can be cloned and inserted into our page.

  1. First, take a look at this article on using templates: HTML <template> Tag

    1. Look at the HTML for the examples. Do you see where the templates are defined?
    2. We can use document.getElementById() or document.querySelector() to first select the template
    3. We can then call the .content.cloneNode(true) method on that template, which will return a new Node representing a copy of that template
    4. Then, we can append that copy somewhere on our page
  2. Let’s apply the same principle to our card viewer:

    1. In your JavaScript, select the template element and save it to a variable named template.
      • (You can do this inside or outside the loop)
    2. Inside the loop, call template.content.cloneNode(true) to create a copy of the template, and then save it to a variable named cardView.
    3. Finally, select the element with the card-list class, and call .appendChild(cardView) on it to add the newly created card to the list.
  3. Test your code. You should have multiple cards displayed, one for each card in localStorage:

Displaying the Data

Now, we need to populate the newly created cardView with the data from the current card.

  1. Put your code before you call .appendChild(), but after .cloneNode():

  2. First, select each of the <span>s that are used to display the card’s text and save them to variables:

    • For example, let titleText = cardView.querySelector(".title-text");
    • You should have five of these, one for each span.
  3. Set each of those <span>’s textContent to the appropriate values from the current card

    • Remember that each card is an object, so you can access the attributes directly: e.g., card.title
    • The five attributes are: to, from, title, subtitle, message
  4. Test your code. Each card should display its corresponding text:

Deleting Cards

Let’s make the delete button work. To do so, we’ll need to do a few things.

  1. Continue with your code inside the loop. Make sure that the following lines are still before you call .appendChild()!
  2. We need to make the delete button for the current card do something.
    1. First, select the delete button on the cardView
    2. Then, add an event listener of type "click"
    3. For the callback function, make an inline function – this is important!
  3. When the button is clicked, we want to delete this card. The problem is, how does each delete button know which card its referring to?
    1. In the callback function, write console.log(i) to log the current index.
    2. Test your page and open the developer console. Click on the delete buttons. Notice how each button correctly prints out the corresponding index!
      • How does this work? When we create a function, JavaScript will “save” the state of our variables in something called a closure.
      • If this isn’t working, you may have declared your variables with var instead of let. This is one of the advantages of let. It scopes the variable to the block instead of the function, which lets us keep its current state.
      • If closures scare you, there is an alternative method below
  4. Now, in the button’s callback function:
    1. Make the button delete the item in the array at the current index
    2. Afterwards, save the array of cards back to localStorage
      • Don’t forget to stringify it!
    3. Reload the page by calling location.reload()
      • We do this because, after deleting an item, the indices in the array are no longer valid, so let’s refresh the page and display all the cards again
  5. Test your code. You should be able to delete a card and, once the page reloads, it should no longer be displayed.

(If you have time) Making the Cards Editable

Last but not least, let’s make the cards editable. You may have noticed that there’s a contenteditable attribute on each of the <span>s! This makes the element into something that works both as text and an input.

Elements with contenteditable have an event called "input", which is fired whenever a user changes the value of the element.

  1. Inside the loop, but still appendChild, let’s create a new function. Yes, right here.

    1. Name the function updateCard()
    2. Inside, you should set each of the current card object’s attributes (to, from, title, subtitle, message) to the content from the corresponding <span>s
      • For instance, card.title = titleText.textContent; or cards[i].title = ...
    3. Afterwards, save the entire array to localStorage again.
      • Don’t forget to stringify it!
  2. After the function definition, but still before appendChild, add an event listener for the "input" event to all five <span>s. Use your updateCard function as the callback:

    • E.g., titleText.addEventListener("input", updateCard);
  3. Test your code by clicking in one of the fields and editing it. You should see the value change in localStorage:

    • If you are having issues, double-check:
      • Make sure you are using either let or const for your variables/constants, not var
      • Make sure you are passing updateCard as an argument, and not accidentally calling it with updateCard()

Alternative to Using Closures

Instead of using closures, you can also “store” the index of each card in the HTML elements themselves! We can create a custom HTML attribute, called a data attribute.

Here’s how that would work instead of closures:

  1. On each card view and/or each of the <span>s:
    1. Add a new data attribute called index using JavaScript (aka "data-index" in HTML)
    2. Set the value to the current index of the card
  2. In your event handlers/callback functions, retrieve the index attribute.
    1. Attributes are stored as strings, so you may need to convert it back to an int
    2. Use this value to specify which card to delete/update

Submission

Commit and push your code to GitHub.