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.
handleItemCheckClicked
function to: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:
STORE
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.
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:
STORE
, generate a string representing an <li>
with:STORE
set as a data attribute on the <li>
(more on that in a moment).shopping-item__checked
from index.css)<li>
s string inside the .js-shopping-list
<ul>
in the DOM.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.
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.
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.
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:
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.
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.