scottfontenot
7/22/2017 - 11:25 PM

Architecting Client Side Apps

Architecting Client Side Apps

'use strict';


// for a shopping list, our data model is pretty simple.
// we just have an array of shopping list items. each one
// is an object with a `name` and a `checked` property that
// indicates if it's checked off or not.
// we're pre-adding items to the shopping list so there's
// something to see when the page first loads.
const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];


// below we've set up a bunch of constants that point to
// classes, ids, and other attributes from our HTML and CSS
// that our application code will need to use to manipulate
// the DOM.
// We *could* just have these values hard coded into the particular
// functions that use them, but that is harder to maintain. What if
// for some reason we need to change '.js-shopping-list', which is the
// identifier for the shopping list element in our HTML? With these
// constants, it only needs to be changed in one place, at
// the top of the file. Without constants, we have three separate functions
// that currently use `SHOPPING_LIST_ELEMENT_CLASS`, so without constants
// there would be three places in our code we'd need to remember to update!
const NEW_ITEM_FORM_ID = "#js-shopping-list-form";
const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_BUTTON_IDENTIFIER = "js-item-toggle";
const ITEM_DELETE_BUTTON_IDENTIFIER = "js-item-delete";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";


// this function is reponsible for generating an HTML string representing
// a shopping list item. `item` is the object representing the list item.
// `itemIndex` is the index of the item from the shopping list array (aka,
// `STORE`).
function generateItemElement(item, itemIndex, template) {
  // we use an ES6 template string
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}


// this function is reponsible for generating all the `<li>`s that will eventually get
// inserted into the shopping list `ul` in the com. it takes one argument,
// `shoppingList` which is the array representing the data in the shopping list.
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");
  // `items` will be an array of strings representing individual list items.
  // we use the array `.map` function
  // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map?v=control)
  // to loop through `shoppingList`. For each item in `shoppingList`, we...
  const items = shoppingList.map(
    (item, index) => generateItemElement(item, index));
  // this function is responsible for returning a string, but `items` is an array.
  // we return `items.join` because that will join the individual strings in `items`
  // together into a single string.
  return items.join();
}


// we call `generateShoppingItemsString` to generate the string representing
  // the shopping list items
function renderShoppingList() {
  console.log("Rendering shopping list");
  // we call `generateShoppingItemsString` to generate the string representing
  // the shopping list items
  const shoppingListItemsString = generateShoppingItemsString(STORE);
  // we then find the `SHOPPING_LIST_ELEMENT_CLASS` element in the DOM,
  // (which happens to be a `<ul>` with the class `.js-shopping-list` on it )
  // and set its inner HTML to the value of `shoppingListItemsString`.
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}


// name says it all. responsible for adding a shopping list item.
function addItemToShoppingList(itemName) {
  console.log(`Adding "${itemName}" to shopping list`);
  // adding a new item to the shopping list is as easy as pushing a new
  // object onto the `STORE` array. we set `name` to `itemName` and default
  // the new item to be unchecked (`checked: false`).
  //
  // Note that this function intentionally has *side effects* -- it mutates
  // the global variable STORE (defined at the top of this file).
  // Ideally you avoid side effects whenever possible,
  // and there are good approaches to these sorts of situations on the front
  // end that avoid side effects, but they are a bit too complex to get into
  // here. Later in the course, when you're learning React though, you'll
  // start to learn approaches that avoid this.
  STORE.push({name: itemName, checked: false});
}


// name says it all. responsible for deleting a list item.
function deleteListItem(itemIndex) {
  console.log(`Deleting item at index "${itemIndex}" from shopping list`);
  // as with `addItemToShoppingLIst`, this function also has the side effect of
  // mutating the global STORE value.
  //
  // we call `.splice` at the index of the list item we want to remove, with a length
  // of 1. this has the effect of removing the desired item, and shifting all of the
  // elements to the right of `itemIndex` (if any) over one place to the left, so we
  // don't have an empty space in our list.
  STORE.splice(itemIndex, 1);
}


// this function is reponsible for toggling the `checked` attribute on an item.
function toggleCheckedForListItem(itemIndex) {
  console.log(`Toggling checked property for item at index ${itemIndex}`);
  // if `checked` was true, it becomes false, and vice-versa. also, here again
  // we're relying on side effect / mutating the global `STORE`
  STORE[itemIndex].checked = !STORE[itemIndex].checked;
}


// responsible for watching for new item submissions. when those happen
// it gets the name of the new item element, zeros out the form input value,
// adds the new item to the list, and re-renders the shopping list in the DOM.
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_ID).submit(function(event) {
    // stop the default form submission behavior
    event.preventDefault();

    // we get the item name from the text input in the submitted form
    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    // now that we have the new item name, we remove it from the input so users
    // can add new items
    newItemElement.val("");
    // update the shopping list with the new item...
    addItemToShoppingList(newItemName);
    // then render the updated shopping list
    renderShoppingList();
  });
}


// this function is responsible for retieving the array index of a
// shopping list item in the DOM. recall that we're storing this value
// in a `data-item-index` attribute on each list item element in the DOM.
function getItemIndexFromElement(item) {
  const itemIndexString = $(item)
    .closest(`.${ITEM_INDEX_ELEMENT_IDENTIFIER}`)
    .attr(ITEM_INDEX_ATTRIBUTE);
  // the value of `data-item-index` will be a string, so we need to convert
  // it to an integer, using the built-in JavaScript `parseInt` function.
  return parseInt(itemIndexString, 10);
}


// this function is responsible for noticing when users click the "checked" button
// for a shopping list item. when that happens it toggles the checked styling for that
// item.
function handleItemCheckClicked() {
  // note that we have to use event delegation here because list items are not originally
  // in the DOM on page load.
  $(SHOPPING_LIST_ELEMENT_CLASS).on("click", `.${ITEM_CHECKED_BUTTON_IDENTIFIER}`, event => {
    // call the `getItemIndexFromElement` function just above on the target of
    // the current, clicked element in order to get the index of the clicked
    // item in `STORE`
    const itemIndex = getItemIndexFromElement(event.currentTarget);
    // toggle the clicked item's checked attribute
    toggleCheckedForListItem(itemIndex);
    // render the updated shopping list
    renderShoppingList();
  });
}


// this function is responsible for noticing when users click the "Delete" button for
// a shopping list item. when that happens, it removes the item from the shopping list
// and then rerenders the updated shopping list.
function handleDeleteItemClicked() {
  // like in `handleItemCheckClicked`, we use event delegation

  $(SHOPPING_LIST_ELEMENT_CLASS).on("click", `.${ITEM_DELETE_BUTTON_IDENTIFIER}`, event => {
    // get the index of the item in STORE
    const itemIndex = getItemIndexFromElement(event.currentTarget);
    // delete the item
    deleteListItem(itemIndex);
    // render the updated shopping list
    renderShoppingList();
  });
}


// this function will be our callback when the page loads. it's responsible for
// initially rendering the shopping list, and activating our individual functions
// that handle new item submission and user clicks on the "check" and "delete" buttons
// for individual shopping list items.
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}


// when the page loads, call `handleShoppingList`
$(handleShoppingList);

In this assignment, we'll implement the checking/unchecking functionality for our app. Like the new item functionality, compared to rendering the shopping list, this feature will be relatively easy to implement.

In plain language, we'll need our handleItemCheckClicked function to:

  • Listen for when a user clicks the 'check' button on an item.
  • Retrieve the item's index in STORE from the data attribute.
  • Toggle the checked property for the item at that index in STORE.
  • Re-render the shopping list.
  • Note that for this feature, we'll need to use event delegation, because our list items won't be in the DOM on page load.
const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const NEW_ITEM_FORM_IDENTIFIER = '#js-shopping-list-form';

const ITEM_CHECKED_BUTTON_IDENTIFIER = "js-item-toggle";

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join();
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function addItemToShoppingList(itemName) {
  console.log(`Adding "${itemName}" to shopping list`);
  STORE.push({name: itemName, checked: false});
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');
    
    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    newItemElement.val('');
    addItemToShoppingList(newItemName);
    renderShoppingList();
  });
}
function handleItemCheckClicked() {
  $(SHOPPING_LIST_ELEMENT_CLASS).on('click', `.${ITEM_CHECKED_BUTTON_IDENTIFIER}`, event => {
    console.log('`handleItemCheckClicked` ran');
    // get the index of item in STORE from the data attribute
    // toggle the checked value of the item in the STORE
    // re-render the shopping list
  });
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

Taking a look at handleItemCheckClicked, we've taken care of the first bullet point from above (listening for user clicks on the element). We listen for click events SHOPPING_LIST_ELEMENT_CLASS because that item will be in the DOM on page load, but we filter those events by ITEM_CHECKED_BUTTON_IDENTIFIER.

Next, create an additional function getItemIndexFromElement that we'll use to retrieve the item's index in STORE from the data attribute on the DOM element.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const NEW_ITEM_FORM_IDENTIFIER = '#js-shopping-list-form';

const ITEM_CHECKED_BUTTON_IDENTIFIER = "js-item-toggle";

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join();
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function addItemToShoppingList(itemName) {
  console.log(`Adding "${itemName}" to shopping list`);
  STORE.push({name: itemName, checked: false});
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');
    
    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    newItemElement.val('');
    addItemToShoppingList(newItemName);
    renderShoppingList();
  });
}
function getItemIndexFromElement(item) {
  const itemIndexString = $(item)
    .closest(`.${ITEM_INDEX_ELEMENT_IDENTIFIER}`)
    .attr(ITEM_INDEX_ATTRIBUTE);
  return parseInt(itemIndexString, 10);
}
function handleItemCheckClicked() {
  $(SHOPPING_LIST_ELEMENT_CLASS).on('click', `.${ITEM_CHECKED_BUTTON_IDENTIFIER}`, event => {
    console.log('`handleItemCheckClicked` ran');
    const itemIndex = getItemIndexFromElement(event.currentTarget);
    console.log('User clicked "check" on item ' + itemIndex);
    // toggle the checked value of the item in the STORE
    // re-render the shopping list
    // get the index of item in STORE from the data attribute
    // toggle the checked value of the item in the STORE
    // re-render the shopping list
  });
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

In handleItemCheckClicked we pass the value of event.currentTarget to getItemIndexFromElement to get the item's index number. We then log that value to the console.

Inside of getItemIndexFromElement, we turn the item into a jQuery object, and then use the .closest method to get the parent element that has the ITEM_INDEX_ELEMENT_IDENTIFIER on it. Then we set itemIndexString to the value of that element's ITEM_INDEX_ATTRIBUTE attribute. That raw value will be a string since it's stored in the DOM, so parse an integer from the string, and return the resulting value.

Now we just need to use that item index to find the right item in STORE and toggle its checked property.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const NEW_ITEM_FORM_IDENTIFIER = '#js-shopping-list-form';

const ITEM_CHECKED_BUTTON_IDENTIFIER = "js-item-toggle";

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join();
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function addItemToShoppingList(itemName) {
  console.log(`Adding "${itemName}" to shopping list`);
  STORE.push({name: itemName, checked: false});
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');
    
    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    newItemElement.val('');
    addItemToShoppingList(newItemName);
    renderShoppingList();
  });
}
function toggleCheckedForListItem(itemIndex) {
  console.log("Toggling checked property for item at index " + itemIndex);
  STORE[itemIndex].checked = !STORE[itemIndex].checked;
}
function getItemIndexFromElement(item) {
  const itemIndexString = $(item)
    .closest(`.${ITEM_INDEX_ELEMENT_IDENTIFIER}`)
    .attr(ITEM_INDEX_ATTRIBUTE);
  return parseInt(itemIndexString, 10);
}
function handleItemCheckClicked() {
  $(SHOPPING_LIST_ELEMENT_CLASS).on('click', `.${ITEM_CHECKED_BUTTON_IDENTIFIER}`, event => {
    console.log('`handleItemCheckClicked` ran');
    const itemIndex = getItemIndexFromElement(event.currentTarget);
    toggleCheckedForListItem(itemIndex);
    renderShoppingList();
  });
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

we've created another function toggleCheckedForListItem which is responsible for toggling the checked property of an item at a particular index in STORE. Inside handleItemCheckClicked, we call toggleCheckedForListItem with the itemIndex value, before calling renderShoppingList to re-render our list with the updated checked status.

Up next, you will complete the final feature for this app: deleting shopping list items.

Implement the behavior for adding shopping list items.

To begin, let's nail down the details of what adding a new shopping list item entails, in plain language. We'll need to:

  • Listen for when users submit a new list item. And then...
  • Get the name of the new item from the text input in our new item form
  • Clear out the value from the input so eventually new items can be added
  • Create an object representing the new item and add it to the shopping list STORE
  • Re-render the shopping list in the DOM in light of the updated STORE.
const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const ITEM_CHECKED_BUTTON_IDENTIFIER = ".js-item-toggle";

const NEW_ITEM_FORM_IDENTIFIER = '#js-shopping-list-form';

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join();
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');
    // get name of new item
    // clear input on new item form
    // add item to shopping list
    // re-render shopping list
  });
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

Looking at index.js, we've modified the handleNewItemSubmit so that it now listens for submissions of the new item form. When that happens, the callback function stops the default form submission behavior and then logs to the console. This takes care of our first bullet: we now can run a callback function when users submit a new item.

Next, we'll take care of the second and third bullets: getting the item name and clearing the text from the input element. Here's what that looks like.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const ITEM_CHECKED_BUTTON_IDENTIFIER = ".js-item-toggle";

const NEW_ITEM_FORM_IDENTIFIER = '#js-shopping-list-form';

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join();
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');
    
    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    console.log(newItemName);
    newItemElement.val('');
    // add item to shopping list
    // re-render shopping list
  });
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

Inside handleNewItemSubmit, we now have:

const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
const newItemName = newItemElement.val();
console.log(newItemName);
newItemElement.val('');

If you hit play and try adding an item to the list, you'll see that the item name gets logged to the console and the input gets cleared out after submission, but you won't see the new item appear.

To complete this feature, in this last step, we'll add the new item to the STORE and re-render the shopping list. Here's how we've implemented that:

function addItemToShoppingList(itemName) {
  console.log(`Adding "${itemName}" to shopping list`);
  STORE.push({name: itemName, checked: false});
}
function handleNewItemSubmit() {
  $(NEW_ITEM_FORM_IDENTIFIER).submit(function(event) {
    event.preventDefault();
    console.log('`handleNewItemSubmit` ran');

    const newItemElement = $(NEW_ITEM_FORM_INPUT_CLASS);
    const newItemName = newItemElement.val();
    newItemElement.val('');
    addItemToShoppingList(newItemName);
    renderShoppingList();
  });
}

We've created a separate function (addItemToShoppingList) that's responsible for updating the store with the new item. Inside addItemToShoppingList, we push a new object literal onto STORE. Note that in this function we've chosen to explicitly mutate the global STORE variable so that it's crystal clear that there's an (intended) side effect in our function.

Finally, after calling addItemToShoppingList inside handleNewItemSubmit, we call renderShoppingList to re-render the shopping list. This is where our "anti-spaghetti" approach really begins to shine. Because we've captured the logic for rendering the shopping list in a reusable function, we just call that function, and don't have to worry about repeating ourselves.

Up next, we'll implement the item checking functionality.

renderShoppingList function. The responsibility of this function is to render the current state of the shopping list STORE to the DOM. We call renderShoppingList inside of handleShoppingList, which runs after page load, in order to initially render the shopping list. We'll also call renderShoppingList in our functions for altering the list. After adding, deleting, or toggling an item's checked property, we'll call renderShoppingList to update the DOM with the current state of the list.

Describing renderShoppingList in plain language

before jumping into coding, we should determine the steps this function will need to take in order to render the shopping list, stating them in plain language which will then guide us as we code. Let's first ask where the shopping list should be rendered. Taking a look at index.html, below the form for adding new list items, we have: <ul class="shopping-list js-shopping-list"></ul> This unordered list is where we'll render our list items. Our jQuery code will need to target the .js-shopping-list element, inserting <li>s inside.

We need STORE to get translated into an HTML string that can be inserted into the right place in the DOM. So the pseudocode version of renderShoppingList would be something like this:

  • For each item in STORE, generate a string representing an <li>with:
  • the item name rendered as inner text
  • the item's index in the STORE set as a data attribute on the <li> (more on that in a moment)
  • the item's checked state (true or false) rendered as the presence or absence of a CSS class for indicating checked items (specifically, .shopping-item__checked from index.css)
  • Join together the individual item strings into one long string
  • Insert the <li>s string inside the .js-shopping-list <ul> in the DOM.

First things first: hardcoded list items

Looking at our pseudo code, the easiest moment to implement is appending the string representing list items to the DOM. We can initially hardcode a value to be inserted in the DOM, and focus on our implementation of the DOM traversal and insertion.

For this, we're going to use a new strategy. Instead of hardcoding the selector for the shopping list ul (.js-shopping-list) into our renderShoppingList function, we'll assign that value to a constant at the top of index.js, and then refer to that constant in our function. We'll discuss why that's advantageous in a moment, but first, have a look at index.js in the repl.it below:

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const SHOPPING_LIST_ELEMENT_CLASS = '.js-shopping-list';

function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = '<li>hello world</li>';
  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  // listen for users adding a new shopping list item, then add
  // to list and render list 
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

we've added a constant SHOPPING_LIST_ELEMENT_CLASS whose value is the class of the shopping list <ul> in index.html.

Inside of renderShoppingList we've hardcoded a string representing a single list item (shoppingListItemsString). In a moment, we'll replace that hardcoded value with one dynamically generated by a function whose purpose is generate a string representing shopping list items.

renderShoppingList ends by targeting SHOPPING_LIST_ELEMENT_CLASS and setting its inner HTML to the value of shoppingListsItemsString. If you hit play on the repl.it, you can see that the hardcoded single list item gets rendered.

This means that we already know that renderShoppingList's end behavior is correctly wired. Line 15 ($(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);) doesn't care how shoppingListItemsString gets generated, it just cares that it's generated.

We'll move on to implementing the logic for generating shoppingListItemsString in a moment, but first let's discuss why we're creating a constant SHOPPING_LIST_ELEMENT_CLASS instead of hardcoding that value in renderShoppingList. This strategy has a few benefits.

  • First, if you have selectors that multiple functions will need to utilize, you can use the constant inside these functions, and if for some reason you need to update its value, you only have to update it one place.

  • Second, by storing all such values in constants at the top of the file, it's easy to get a quick understanding of the expectations your app has about what appears in the DOM.

As we build out the rest of this app, we'll stick to this strategy of storing selectors in constants at the top of our file.

Generating the list item string

The first step we'll take is to create a new function called generateShoppingItemsString. For now it will return a hardcoded string of <li>s, but we'll call it from inside renderShoppingList.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const SHOPPING_LIST_ELEMENT_CLASS = '.js-shopping-list';

function generateShoppingItemsString() {
  console.log("Generating shopping list element");

  // generate an <li> with the right attributes for each item in the list.
  
  return '<li>hello world</li><li>goodbye world</li>';
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString();

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  // listen for users adding a new shopping list item, then add
  // to list and render list 
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

It's wired up to render whatever string value gets returned by generateShoppingItemsString.

In order to generate this string, we'll need to iterate over each item in STORE and generate an <li> string with the right text and class set to reflect the properties of the item. Our first pass of this will be to map over the items in STORE, calling a new function (generateItemElement) on each one to generate the item string. We'll then join these individual item strings into one big string to be returned by generateShoppingItemsString.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const SHOPPING_LIST_ELEMENT_CLASS = '.js-shopping-list';

function generateItemElement(item, itemIndex) {
  // generate a string representing an `<li> for each list item.
  
  // set a data attribute to store item's index index in the shopping list store
  
  // correctly set the item name
  
  // correctly set the `.shopping-item__checked` class on the right part of the item
  
  return `<li>${item.name}</li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join("");
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  // listen for users adding a new shopping list item, then add
  // to list and render list 
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

We now pass STORE to generateShoppingItemsString as the shoppingList parameter. Since this function won't modify the shopping list (aka, there are no subtle side-effects to make explicit), we've chosen to pass it as a function parameter, rather than directly referencing the global STORE inside of generateShoppingItemsString.

In generateShoppingItemsString, we create a new array of item strings (items) by mapping over shoppingList and calling a new function, generateItemElement on each item. generateShoppingItemsString will return a single string that joins together the individual item strings (return items.join()).

In our first pass at generateItemElement, we create a string representing a list element that displays the item name, but does not support adding, checking, unchecking, or deleting items. We'll tackle those features soon enough, but before doing that, click play on the repl.it above, and you'll see that we're now displaying the name of each item in STORE in the DOM.

Generating individual list item strings

The final thing to do in this assignment is fully implement our generateItemElement function. At the moment, it is only returning the name of each item.

To complete this function, we need to be generating list items that support checking and unchecking, and deletion. We won't fully implement checking and deletion until later, but we want our rendered shopping list items to contain buttons for checking and deleting, as well as a data attribute indicating their index in the STORE array.

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

const NEW_ITEM_FORM_INPUT_CLASS = ".js-shopping-list-entry";
const SHOPPING_LIST_ELEMENT_CLASS = ".js-shopping-list";
const ITEM_CHECKED_TARGET_IDENTIFIER = "js-shopping-item";
const ITEM_CHECKED_CLASS_NAME = "shopping-item__checked";
const ITEM_INDEX_ATTRIBUTE  = "data-item-index";
const ITEM_INDEX_ELEMENT_IDENTIFIER = "js-item-index-element";
const ITEM_CHECKED_BUTTON_IDENTIFIER = ".js-item-toggle";

function generateItemElement(item, itemIndex, template) {
  return `
    <li class="${ITEM_INDEX_ELEMENT_IDENTIFIER}" ${ITEM_INDEX_ATTRIBUTE}="${itemIndex}">
      <span class="shopping-item ${ITEM_CHECKED_TARGET_IDENTIFIER} ${item.checked ? ITEM_CHECKED_CLASS_NAME : ''}">${item.name}</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle ${ITEM_CHECKED_BUTTON_IDENTIFIER}">
            <span class="button-label">check</span>
        </button>
        <button class="shopping-item-delete js-item-delete">
            <span class="button-label">delete</span>
        </button>
      </div>
    </li>`;
}
function generateShoppingItemsString(shoppingList) {
  console.log("Generating shopping list element");

  const items = shoppingList.map((item, index) => generateItemElement(item, index));
  
  return items.join("");
}
function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
  const shoppingListItemsString = generateShoppingItemsString(STORE);

  // insert that HTML into the DOM
  $(SHOPPING_LIST_ELEMENT_CLASS).html(shoppingListItemsString);
}
function handleNewItemSubmit() {
  // listen for users adding a new shopping list item, then add
  // to list and render list 
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

At the top, we've added new constants storing selector values. These get used inside the generateShoppingItemsString function to target the right element for cloning, and inside generateItemElement to set the name, the checked class, and the data attribute for the item's index in STORE.

Inside generateShoppingItemsString, for each item we map over, we return the result of calling generateItemElement(item, index). Looking at that function, we use ES6 template strings to generate a list item.

And with that, we've implemented the most complex feature of our app: rendering the shopping list. Next up we'll implement the behavior for adding new shopping list items. Because we have created a reusable function for rendering the shopping list, adding list items will relatively easy to implement.

know why Spaghetti Code is less than ideal
spaghetti code is difficult to read, maintain, and scale, and for those reasons and more, we need a better approach to architecting our applications.

Solution to Shopping List App:

$(function(){
  $('#js-shopping-list-form').submit(function(event) {
    event.preventDefault();
    const listItem = $('.js-shopping-list-entry').val();

    $('#shopping-list-entry').val('');

    $('.shopping-list').append(
      `<li>
        <span class="shopping-item">${listItem}</span>
        <div class="shopping-item-controls">
          <button class="shopping-item-toggle">
            <span class="button-label">check</span>
          </button>
          <button class="shopping-item-delete">
            <span class="button-label">delete</span>
          </button>
        </div>
      </li>`);
  });

  $('.shopping-list').on('click', '.shopping-item-delete', function(event) {
    $(this).closest('li').remove();
  });

  $('.shopping-list').on('click', '.shopping-item-toggle', function(event) {
    $(this).closest('li').find('.shopping-item').toggleClass('shopping-item__checked');
  });

});

it's important to develop good coding habits and a sense of what makes for good code architecture. So with no further ado, here are some of the deficiencies with this solution:

You have to read every line of code to understand how the app works.
The app is composed entirely of a single, anonymous document ready function that has jQuery event listeners for submit and click events that in turn call additional anonymous functions, which appear to do something to the DOM, but without inspecting the HTML, it's hard to know what they do.

It's difficult to reason about the shopping list data
The problem is that the data for the shopping list itself is stored entirely in the DOM. The only way you can understand the overall state of the shopping list at any time is by inspecting each list item in the DOM. And to make matters worse, the problem you're trying to diagnose involves the DOM being wrong about displaying the list! Ideally, we need to establish** one part of our code for storing the underlying data for our shopping list**, and then have a different part of our code deal with how that code is rendered in the DOM. This will allow us to ask and answer questions about how what the DOM is displaying corresponds (or doesn't) to the data model.

Spaghetti code doesn't scale

Our shopping list app is about as simple as apps come in terms of what users can do with it: they can read a shopping list, add items to it, delete them, and check and uncheck them. Now imagine a complex app like Facebook or Gmail implemented in a single document ready function. The fact is that neither Facebook nor Gmail would be working apps if they were built with spaghetti code.

Clearly describing your application with user stories

Before opening your text editor, it's critical to get a clear picture of what you're building. That means coming up with clear, concise statements that describe what the app you're building will do. These statements will tell you what you need to build and when a particular feature of the app is complete. They also provide a common language for speaking with non-technical stakeholders on a project.

Here's how we could break down our shopping list:

  • A shopping list should be rendered to the page
  • You should be able to add items to the list
  • You should be able to check items on the list
  • You should be able to delete items from the list These statements are examples of user stories, which are short, plain language descriptions of what a user should be able to do with an app. User stories can be written by and discussed by technical and non-technical stakeholders. Notice that these user stories don't say anything about how the app is to be implemented. Instead they focus on what the app should do. Recall that we made a similar observation about how well-named, small functions allow a coder to quickly understand what an app does without having to look at how each function is implemented. This happy coincidence is one we can take advantage of as we start architecting our application.

From user stories to function stubs with pseudocode

We're going to write function stubs. These functions will have names but inside of each one, we won't add any working code. Instead, we'll just add a description of what the code should do. We'll also go ahead and hook these functions up to a document ready function, since we know we'll definitely need that.

function renderShoppingList() {
  // this function will be repsonsible for rendering the shopping list in
  // the DOM
  console.log('`renderShoppingList` ran');
}
function handleNewItemSubmit() {
  // this function will be responsible for when users add a new shopping list item
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // this funciton will be reponsible for when users click the "check" button on
  // a shopping list item.
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // this function will be responsible for when users want to delete a shopping list
  // item
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  // render the shopping list
  // handle when a new item is submitted
  // handle when an item is deleted
  // handle when an item is checked/unchecked
}

Now lest fully implement the handleShoppingList function:

function renderShoppingList() {
  // render the shopping list in the DOM
  console.log('`renderShoppingList` ran');
}
function handleNewItemSubmit() {
  // listen for users adding a new shopping list item, then add
  // to list and render list 
  console.log('`handleNewItemSubmit` ran');
}
function handleItemCheckClicked() {
  // listen for users checking/unchecking list items, and
  // render them checked/unchecked accordingly
  console.log('`handleItemCheckClicked` ran');
}
function handleDeleteItemClicked() {
  // Listen for when users want to delete an item and 
  // delete it
  console.log('`handleDeleteItemClicked` ran')
}
function handleShoppingList() {
  renderShoppingList();
  handleNewItemSubmit();
  handleItemCheckClicked();
  handleDeleteItemClicked();
}
$(handleShoppingList);

Perform console.log on the above code and in browser [developer tools] this proves that everything is wired up correctly.

Modeling our data

What we need is a single source of truth about the state of our shopping list. We want to be able to store that data some place, and what the user sees in the DOM should ultimately be a reflection of the current state of our data model. Specifically, we need a way of modeling a shopping list. How can we best store data about a shopping list? The name shopping list itself already implies that we'll probably want to use a JavaScript array. That would allow us to store multiple list items, add items, and delete them. Each item on the list needs at least two attributes: the item name, and whether or not it is currently checked. We say "at least two" because there's arguably a third attribute we need: each item needs a unique identifier that we can use to choose a specific item and delete or check/uncheck it. But since we're using an array, we can use each item's array index as its id (for instance, the first item in our shopping list array will be at index 0).

const STORE = [
  {name: "apples", checked: false},
  {name: "oranges", checked: false},
  {name: "milk", checked: true},
  {name: "bread", checked: false}
];

STORE is a constant and that does NOT mean that the shopping list array itself should not be altered, that would be useless for our app...it's OK to alter the underlying array, it's NOT ok to reassign the variable name to a new value. // okay STORE.push({name: "chocolate", checked: true});

// not okay! STORE = {foo: 'bar'};

A final note about STORE. In JavaScript, complex data types (aka, objects and arrays) are passed by reference, not by value. That means that if you pass an array or object to a function as an argument, and the function mutates it, the value of the original variable outside of the function will also be mutated.