Coffee and chocolate (validation, localStorage)

The story so far

The piggy bank app persisted data to localStorage. localStorage stores string data, with string keys.

Let's make something like the piggy bank app, but a bit more complex

Requirements

Goals

Make an app that tracks your coffee and chocolate expenses. When you buy some coffee, you enter the amount you spend, and it gets added to a coffee total. Same for chocolate.

The totals are persistent.

You can try a partial version of it. It's only the coffee part.

Screen

There's just one screen. Here's a mock up:

Mock up

We should be careful with the reset button. Make sure that users don't erase data accidentally.

Events

  • Add button: Take the user's new expenses, and add them to the current expenses.
  • Reset button: After the user confirms that s/he wants to reset, set current expenses to zero.
  • App start: Show current expenses.

Pseudocode

We'll need two variables to coordinate the code fragments. Let's call them coffeeExpense, and chocolateExpense.

Event: page load

Load coffeeExpense and chocolateExpense from localStorage
Show them

Event: Add button

The Add button processes new coffee and chocolate expenses. One or both of the input fields could be empty, though.

If there is a new coffee expense:
  If the data is not valid:
    Show an error message
  Else:
    Update coffeeExpense and localStorage
    Show the new value
If there is a new chocolate expense:
  If the data is not valid:
    Show an error message
  Else:
    Update chocolateExpense and localStorage
    Show the new value
Georgina
Georgina
You could flip the ifs around, right? The ones testing for valid data?

Sure.

If there is a new coffee expense:
  If the data is valid:
    Update coffeeExpense and localStorage
    Show the new value
  Else:
    Show an error message
If there is a new chocolate expense:
  If the data is valid:
    Update chocolateExpense and localStorage
    Show the new value
  Else:
    Show an error message

Either way would be fine.

Ray
Ray
When you say "data is valid," what are you checking for, exactly?
Good question. Two things:

  • Is the data numeric?
  • Is it zero or more?

We could do other tests, but that will work for this app.

Event: Reset button

Here's the pseudocode:

If user confirms:
  Set coffeeExpense and chocolateExpense to 0
  Erase localStorage for them
  Update the display

OK, let's implement.

HTML for coffee

Here's the display:

Start

Let's work on coffee first. Get it working, then copy-and-paste for chocolate.

  1. <h1>Coffee and Chocolate</h1>
  2. <h2>Current expenses</h2>
  3. <ul>
  4.     <li>Coffee: $<span id="coffeeExpense"></span></li>
  5.     ...
  6. </ul>
  7. <h2>New expenses</h2>
  8. <div class="form-group">
  9.     <label for="new-coffee-expense">Coffee</label>
  10.     <input type="number" class="form-control" id="new-coffee-expense"
  11.         aria-describedby="new-coffee-expense-help"
  12.         placeholder="New coffee expense">
  13.     <small id="new-coffee-expense-help" class="form-text text-muted">
  14.         Enter new expense, or leave empty.</small>
  15. </div>
  16. ...
  17. <p>
  18.     <button onclick="CoffeeChocolate.recordNewExpenses()"
  19.         type="button" class="btn btn-primary"
  20.         title="Add new expenses">Add</button>
  21.       
  22.     <button onclick="CoffeeChocolate.resetExpenses()"
  23.         type="button" class="btn btn-danger"
  24.         title="Reset expenses to zero">Reset</button>
  25. </p>
Line 4…

Coffee: $<span id="coffeeExpense"></span>

… creates an output place for coffee expense.

The input field is on lines 8 to 15:

<div class="form-group">
    <label for="new-coffee-expense">Coffee</label>
    <input type="number" class="form-control" id="new-coffee-expense"
        aria-describedby="new-coffee-expense-help"
        placeholder="New coffee expense">
    <small id="new-coffee-expense-help" class="form-text text-muted">
        Enter new expense, or leave empty.</small>
</div>

As before, this is a copy-and-paste from the Bootstrap docs, changed to make it what we want.

The buttons are as before. Here's one:

<button onclick="CoffeeChocolate.recordNewExpenses()"
    type="button" class="btn btn-primary"
    title="Add new expenses">Add</button>

Namespace and variable

<script>
    "use strict";
    var CoffeeChocolate = CoffeeChocolate || {};
    (function($) {
        var coffeeExpense = 0;
        ...
    }(jQuery));
</script>

Note the variable coffeeExpense. It ties together the code fragments attached to the events.

Events

We want code for three events:

  • App start: Show current expenses.
  • Add button: Take the user's new expenses, and add them to the current expenses.
  • Reset button: After the user confirms that s/he wants to reset, set current expenses to zero.

Let's look at them.

Loading the page

The pseudocode from before:

Load coffeeExpense and chocolateExpense from localStorage
Show them

Remember that we're just doing coffee. We'll add chocolate later.

$(document).ready(function () {
    //Template stuff
    ...
    //Load initial expenses.
    //Expense variables have already been initialized to zero.
    if ( localStorage.getItem("coffeeExpense") ) {
        coffeeExpense = parseFloat(localStorage.getItem("coffeeExpense"));
    }
    //Show current expenses.
    CoffeeChocolate.showCurrentExpenses();
});

It's much the same as the piggy bank code, except for the last bit:

//Show current expenses.
CoffeeChocolate.showCurrentExpenses();

In the piggy bank app, we updated the amount shown on the screen three times:

  • On page load
  • When the add button was pressed
  • When the "Take it all" button was pressed

It was just one line each time, but that line was repeated.

Let's move the screen updating code into its own function, that's called when needed. Here it is, so far:

/**
* Show current expenses.
*/
CoffeeChocolate.showCurrentExpenses = function() {
    $("#coffeeExpense").html(coffeeExpense);
};

Why do this? Any ideas?

Ray
Ray
It's easier to change how displaying data works. Since the output code is in one place, changing the code in showCurrentExpenses changes output for the entire app.

Adela
Adela
Oh, and another thing. Debugging is easier. The code is only there once, so there is less to go wrong.
Let's see where we are. Here are all of the events.

  • App start: Show current expenses.
  • Add button: Take the user's new expenses, and add them to the current expenses.
  • Reset button: After the user confirms that s/he wants to reset, set current expenses to zero.

We've done the first one.

Add button

Here's the pseudocode for the second.

If there is a new coffee expense:
  If the data is not valid:
    Show an error message
  Else:
    Update coffeeExpense and localStorage
    Show the new value
If there is a new chocolate expense:
  If the data is not valid:
    Show an error message
  Else:
    Update chocolateExpense and localStorage
    Show the new value

Let's remove the chocolate stuff for now:

If there is a new coffee expense:
  If the data is not valid:
    Show an error message
  Else:
    Update coffeeExpense and localStorage
    Show the new value

The code:

/**
* Record new expenses.
*/
CoffeeChocolate.recordNewExpenses = function() {
    //Process new coffee expense.
    var newCoffeeExpense = $("#new-coffee-expense").val();
    //Anything found?
    if ( newCoffeeExpense != "" ) {
        //Something in the coffee field.
        //Check that it is not negative.
        if ( newCoffeeExpense < 0 ) {
            alert("Sorry, coffee expense cannot be negative. (Wish it could be.)")
        }
        else {
            //Coffee expense is OK.
            coffeeExpense += parseFloat(newCoffeeExpense);
            localStorage.setItem("coffeeExpense", coffeeExpense.toString());
        }
    }
    //Show current expenses.
    CoffeeChocolate.showCurrentExpenses();
    //Clear the input fields.
    $("#new-coffee-expense").val("");
};

There is one new thing here. Check this out:

  1. var CoffeeChocolate = CoffeeChocolate || {};
  2. (function($) {
  3.     var coffeeExpense = 0;
  4.     ...
  5.     CoffeeChocolate.recordNewExpenses = function() {
  6.         var newCoffeeExpense = $("#new-coffee-expense").val();
  7.         ...
  8.     };
  9.     ...
  10. }(jQuery));
Two variables are declared: coffeeExpense, and newCoffeeExpense. Each has its scope, that is, code where the variable exists.

coffeeExpense is created inside the wrapper for $:

(function($) {
    var coffeeExpense = 0;
    ...
}(jQuery));

It's available to the code inside that wrapper. Since all of the page's code is inside the wrapper (apart from the code in the HTML onclick="code here"), the variable coffeeExpense is available to all of the code.

newCoffeeExpense is declared inside the function CoffeeChocolate.recordNewExpenses().

  1. var CoffeeChocolate = CoffeeChocolate || {};
  2. (function($) {
  3.     var coffeeExpense = 0;
  4.     ...
  5.     CoffeeChocolate.recordNewExpenses = function() {
  6.         var newCoffeeExpense = $("#new-coffee-expense").val();
  7.         ...
  8.     };
  9.     ...
  10. }(jQuery));
The variable is created when the browser starts running CoffeeChocolate.recordNewExpenses(). It is destroyed when the function exits. newCoffeeExpense is only available to code inside CoffeeChocolate.recordNewExpenses().

Variables like newCoffeeExpense are called local variables. They only exist inside the functions that declare them.

The last line isn't in the pseudocode, but is needed:

$("#new-coffee-expense").val("");

It puts an empty string in the coffee expense field, erase what was there.

Some other reminders:

  • Remember that += means "add to," or "increase by."
  • The value of an <input> field is always a string, even when users type numbers. parseFloat() converts a string to a number that can have a decimal part.
  • localStorage. localStorage will only handle strings. toString() handles that.
Adela
Adela
Is there something missing? The code doesn't check whether the data in the input field is a number. The user might have typed in text.

Good point. The check isn't needed in this case, because of the HTML we used:

<input type="number" ...

Because it's a number field, the browser will only let users type in numbers.

Let's see where we are. Here are all of the events.

  • App start: Show current expenses.
  • Add button: Take the user's new expenses, and add them to the current expenses.
  • Reset button: After the user confirms that s/he wants to reset, set current expenses to zero.

We've done the first two.

Reset button

Here's the pseudocode for the reset button, with the chocolate stuff omitted:

If user confirms:
  Set coffeeExpense to 0
  Erase localStorage for coffeeExpense
  Update the display

Here's the JS:

  1. /**
  2. * Reset expenses to zero.
  3. */
  4. CoffeeChocolate.resetExpenses = function() {
  5.     if ( confirm("Reset expenses to zero?") ) {
  6.         //Reset variables.
  7.         coffeeExpense = 0;
  8.         //Remove from localStorage.
  9.         localStorage.removeItem("coffeeExpense");
  10.         //Show the current expenses.
  11.         CoffeeChocolate.showCurrentExpenses();
  12.     }
  13. };
Line 5…

if ( confirm("Reset expenses to zero?") ) {

…shows a dialog box with the given message, and an OK and Cancel button.

Confirm

If the user clicks OK, the confirm() function returns true. If the user clicks Cancel (or closes the dialog with the X in the upper right), the confirm() function returns false.

Here's the code again:

  1. /**
  2. * Reset expenses to zero.
  3. */
  4. CoffeeChocolate.resetExpenses = function() {
  5.     if ( confirm("Reset expenses to zero?") ) {
  6.         //Reset variables.
  7.         coffeeExpense = 0;
  8.         //Remove from localStorage.
  9.         localStorage.removeItem("coffeeExpense");
  10.         //Show the current expenses.
  11.         CoffeeChocolate.showCurrentExpenses();
  12.     }
  13. };
The rest is stuff we've seen before. Remember that coffeeExpense is a shared variable.

At this point, we should test the code, and make sure it works. And… it does!

Try it if you want.

Adding chocolate

Exercise: Add chocolate
Add chocolate to the expense tracking app.

Submit your app's URL.

(If you were logged in as a student, you could submit an exercise solution, and get some feedback.)

Summary

We made an app that uses input fields, validation, and localStorage.

We're almost ready to start on CRUD apps. Only a little more before that.