Offline Apps: Web Storage - JUMP START HTML5 (2014)

JUMP START HTML5 (2014)

Chapter 24 Offline Apps: Web Storage

Web storage (also known as DOM storage) is a simple client-side, key-value system that lets us store data in the client. It consists of two parts: localStorage and sessionStorage.

The main difference between localStorage and sessionStorage is persistence. Data stored using localStorage persists from session to session. It’s available until the user or the application deletes it.

Data stored using sessionStorage, on the other hand, persists only as long as the browsing context is available. Usually this means that we lose our sessionStorage data once the user closes the browser window or tab. It may, however, persist in browsers that allow users to save browsing sessions.

In this chapter, we’ll talk about some advantages web storage offers over cookies. We’ll then discuss how to use the Web Storage API and look at some of its limitations.

Why Use Web Storage Instead of Cookies?

There are three main reasons to use localStorage or sessionStorage over cookies:

· HTTP request performance

· data storage capacity

· better protection against cross-site scripting attacks

Cookies are included with each request to the server, increasing the size and duration of each request. Using smaller cookies helps, but using localStorage and sessionStorage helps most of all. Data is stored locally, and sent only when requested.

Another advantage of using web storage is that you can store much more data. Modern browsers begin to max out at 50 cookies per domain. At 4 kilobytes per cookie, we can safely store about 200KB before some browsers will start to delete cookies. And no, we’re unable to control what’s deleted. Web storage limits, on the other hand, range from about 2.5MB in Safari and Android to about 5MB in most other browsers. And we can delete data by key.

Finally, web storage is less vulnerable to cross-site scripting attacks than cookies. Cookies adhere to a same-domain policy. A cookie set for .example.com can be read or overwritten by any subdomain of example.com, including hackedsubdomain.example.com. If sensitivedata.example.com also uses the .example.com cookie, a script at hackedsubdomain.example.com can intercept that data.

A same-origin restriction means that only the origin that created the data can access the data. Data written to localStorage by http://api.example.com will not be available to http://example.com, http://store.example.com, https://example.com, or even http://api.example.com:80.

Browser Support

Of all of the APIs discussed in this book, Web Storage is the most widely supported among major browsers. It’s available in Internet Explorer 8+, Firefox 3.5+, Safari 4+, Opera 10.5+, and Android WebKit. There are a few polyfill scripts that mimic the Web Storage API in browsers that lack it. One such script is Storage polyfill by Remy Sharp. It uses cookies and window.name to mimic localStorage and sessionStorage respectively.

Inspecting Web Storage

For this chapter, use Chrome, Safari, or Opera. The developer tools for these browsers let you inspect and manage web storage values. In Chrome (seen in Figure 24.1), Safari, and Opera 15+, you can find web storage under the Resources panel of the developer tools. In Opera 12 (the only version of Opera available for Linux users), you can find it under the Storage panel of Dragonfly.

The Local Storage inspector in Google Chrome.

Figure 24.1. The Local Storage inspector in Google Chrome.

Testing for Web Storage Support

Both localStorage and sessionStorage are attributes of the window object. We can test for browser support by testing whether these attributes are undefined:

if( typeof window.localStorage == 'undefined' ){

// Use something other than localStorage.

} else {

// Save a key-value pair.

}

Testing for sessionStorage works similarly; use typeof window.sessionStorage == 'undefined' instead. Typically, though, if a browser supports localStorage it also supports sessionStorage, and vice versa.

Let’s look at using the Storage API by building a simple to-do list. We’ll use localStorage to save our to-do items and their status. Although we’ll focus on localStorage in this chapter, keep in mind that this also works for sessionStorage. Just swap the attribute names.

Setting Up Our HTML

A basic HTML outline is fairly simple. We’ll set up a form with an input field and a button for adding new items. We’ll also add buttons for deleting items. And we’ll add an empty list. Of course, we’ll also add links to our CSS and JavaScript files. Your HTML should look a little like this:

<!DOCTYPE html>

<html lang="en-us">

<head>

<meta charset="utf-8">

<title>localStorage to do list</title>

<link rel="stylesheet" href="css/style.css">

</head>

<body>

<form>

<p>

<label for="newitem">What do you need to do today?</label>

<input type="text" id="newitem" required>

<button type="submit" id="saveitem">Add</button>

</p>

<p>

<button type="button" id="deletedone">Delete completed</button>

<button type="button" id="deleteall">Reset the list</button>

</p>

</form>

<ul id="list"></ul>

<script src="js/todolist.js"></script>

</body>

</html>

Our form will resemble the one seen in Figure 24.2.

Our to-do list interface rendered in Firefox after adding the barest bit of CSS magic.

Figure 24.2. Our to-do list interface rendered in Firefox after adding the barest bit of CSS magic.

On form submission―when a submit event is fired on our form―we will append the new task to our unordered list element, and save it to localStorage.

Saving Values With localStorage.setItem()

To add an item to local storage or session storage, we need to use the setItem method. This method accepts two arguments: key and value, in that order. Both arguments must be strings, or be converted to strings:

localStorage.setItem(keyname, value);

For our to-do list, we’ll take the text entered in <input type="text" id="newitem" required>, create a new list item, and save it to localStorage. That’s what we’ve done in this function:

function addItemToList(itemtxt){

var li = document.createElement('li'),

list = document.getElementById('list');

/* Saves the item to storage */

localStorage.setItem(itemtxt, 0);

/* Update the innerHTML value of our list item

and add as a child of our list */

li.innerHTML = itemtxt;

list.appendChild(li);

}

Notice here that we are setting the value of our key to 0 (localStorage.setItem(itemtxt, 0)). For this application, we'll use 0 to indicate a task that needs to be completed and 1 for a task that is complete. When saved, these values will be converted to numeric strings.

Note: Sanitize Your Inputs

In this case, accepting user input for keys is low-risk from a security standpoint. This application and its data is contained entirely within the user's browser. When synchronizing the data with a server, be sure to sanitize and validate your input and escape your output.

Since we want to update our list every time the form is submitted, we also need to add an event listener to our form.

Adding an Event Listener

To add an event listener, use the addEventListener() method and define a function that will be invoked when our event is fired:

var form, updateList;

form = document.forms[0];

var updateList = function(event){

/* Prevent the default action, in this case, form submission */

event.preventDefault();

/* Invoke the addItemToList function */

addItemToList( document.getElementById('newitem').value );

/* Clear the input field */

document.getElementById('newitem').value = '';

}

form.addEventListener('submit', updateList);

Using localStorage.setItem to Update Existing Values

Should a key already exist, using setItem will overwrite the old value rather than create a new record. For our to-do list, this is exactly what we want. To prevent the value of your key from being overwritten, check whether the key exists before saving the item.

We will also use setItem to update our task's status when it's clicked, as we'll see in the next section.

Retrieving Values With localStorage.getItem()

To retrieve the value of a key, we use localStorage.getItem(). This method accepts one argument, the key of the value we’d like to retrieve, which must be a string.

If the key exists, getItem will return the value of that key. Otherwise, it will return null. That makes it useful for determining whether a key exists before calling setItem. For example, if we wanted to test the presence of a lunch key, we might do the following:

if( localStorage.getItem('lunch') ){

// do something.

}

In our case, we’re going use getItem to retrieve the status of our task. Based on the value returned by getItem, we will update the value of our key using setItem.

In the code below, we’ve added an event listener to our unordered list rather than to each list item. We’re using a technique called event delegation. When an event is fired on an element, it “bubbles up” to its ancestors. This means we can set a listener on its parent element, and use thetarget property of the event object to determine which element triggered the event. Because we only want to take an action if the element clicked was a list item, we need to check the nodeName property of the target object:

function toggleStatus(event){

var status;

if(event.target.nodeName == 'LI'){

/*

Using + is a trick to convert numeric strings to numbers.

*/

status = +localStorage.getItem(event.target.textContent);

if(status){

localStorage.setItem(event.target.textContent,0);

} else {

localStorage.setItem(event.target.textContent,1);

}

/* Toggle a 'done' class */

event.target.classList.toggle('done');

}

}

var list = document.getElementById('list');

list.addEventListener('click',toggleStatus);

The event.target property is a pointer to the element that was clicked. Every element object also has a textContent attribute, which is its child text, if applicable. Each list item’s textContent matches a localStorage key, so we can use it to retrieve the value we want. That’s what we’re doing with status = localStorage.getItem(event.target.textContent).

Note: localStorage keys and values are strings

Remember, all localStorage keys and values are strings. What look like numbers are actually numeric strings. To make comparisons with numbers or Boolean values, you’ll need to convert the variable type. Here we’ve used a + sign to convert each key’s value to a number. Zero is a falsy value, equivalent to but not equal to the Boolean false; 1 is a truthy value.

Alternative Syntaxes for Setting and Getting Items

Using setItem and getItem are not the only way to set or retrieve localStorage values. You can also use square-bracket syntax or dot syntax to add and remove them:

localStorage['foo'] = 'bar';

console.log(localStorage.foo) // logs 'bar' to the console

This is the equivalent of using localStorage.setItem('foo','bar') and localStorage.getItem('foo'). If there’s a chance that your keys will contain spaces though, stick to square-bracket syntax or use setItem()/getItem() instead.

Looping Over Storage Items

On page reload, our list of to-dos will be blank. They’ll still be available in localStorage, but not part of the DOM tree. To fix this, we’ll have to rebuild our to-do list by iterating over our collection of localStorage items, preferably when the page loads. This is where localStorage.key()and localStorage.length come in handy.

localStorage.length tells us how many items are available in our storage area. The key() method retrieves the key name for an item at a given index. It accepts one argument: an integer value that’s greater or equal to 0 and less than the value of length. If the argument is less than zero, or greater than equal to length, localStorage.key() will return null.

Indexes and keys have a very loose relationship. Each browser orders its keys differently. What’s more, the key and value at a given index will change as items are added or removed. To retrieve a particular value, key() is a bad choice. But for looping over entries, it’s perfect. An example follows:

var i = localStorage.length;

while( i-- ){ /* As long as i isn't 0, this is true */

console.log( localStorage.key(i) );

}

This will print every key in our storage area to the console. If we wanted to print the values instead, we could use key() to retrieve the key name, then pass it to getItem() to retrieve the corresponding value:

var i = localStorage.length, key;

while( i-- ){

key = localStorage.key(i);

console.log( localStorage.getItem(key) );

}

Let’s go back to our to-do list. We have a mechanism for adding new items and marking them complete. But if you reload the page, nothing happens. Our data is there in our storage area, but not the page.

We can fix this by adding a listener for the load event of the window object. Within our event handler, we’ll use localStorage.length and localStorage.key() along with localStorage.getItem(key) to rebuild our to-do list:

function loadList(){

var len = localStorage.length;

while( len-- ){

var key = localStorage.key(len);

addItemToList(key, localStorage.getItem(key));

}

}

window.addEventListener('load', loadList);

Since we’re working with existing items in this loop, we want to preserve those values. Let’s tweak our addItemToList function a bit to do that:

function addItemToList(itemtxt, status){

var li = document.createElement('li');

if(status === undefined){

status = 0;

localStorage.setItem(itemtxt, status);

}

if(status == true){ li.classList.add('done'); }

li.textContent = itemtxt;

list.appendChild(li);

}

We’ve added a status parameter, which gives us a way to specify whether a task is complete. When we add a task and call this item, we’ll leave out the status parameter; but when loading items from storage as we are here, we’ll include it.

The line if(status == true){ li.classList.add('done'); } adds a done class to the list item if our task status is 1.

Clearing the Storage Area With localStorage.clear()

To clear the storage area completely, use the localStorage.clear() method. It doesn’t accept any parameters:

function clearAll() {

list.innerHTML = '';

localStorage.clear();

}

Once called, it will remove all keys and their values from the storage area. Once removed, these values are no longer available to the application. Use it carefully.

Storage Events

Storage events are fired on the window object whenever the storage area changes. This happens when removeItem() or clear() deletes an item (or items). It also happens when setItem() sets a new value or changes an existing one.

Listening for the Storage Event

To listen for the storage event, add an event listener to the window object:

window.addEventListener('storage', storagehandler);

Our storagehandler function that will be invoked when storage event is fired. We’re yet to define that function―we’ll deal with that in a moment. First, let’s take a look at the StorageEvent object.

The StorageEvent Object

Our callback function will receive a StorageEvent object as its argument. The StorageEvent object contains properties that are universal to all objects, and five that are specific to its type:

· key: contains the key name saved to the storage area

· oldValue: contains the former value of the key, or null if this is the first time an item with that key has been set

· newValue: contains the new value added during the operation

· url: contains the URL of the page that made this change

· storageArea: indicates the storage object affected by the update―either localStorage or sessionsStorage

With these properties, you can update the interface or alert the user about the status of an action. In this case, we’ll just quietly reload the page in other tabs/windows using location.reload().

function storagehandler(event){

location.reload();

}

Now our list is up-to-date in all tabs.

Storage Events Across Browsers

Storage events are a bit tricky, and work differently to how you might expect in most browsers. Rather than being fired on the current window or tab, the storage event is supposed to be fired in other windows and tabs that share the same storage area. Let’s say that our user hashttp://ourexamplesite.com/buy-a-ticket open in two tabs. If they take an action in the first tab that updates the storage area, the storage event should be fired in the second.

Chrome, Firefox, Opera, and Safari are in line with the web storage specification, while Internet Explorer is not. Instead, Internet Explorer fires the storage event in every window for every change, including the current window.

There isn’t a good hack-free way to work around this issue. For now, reloading the application―as we’ve done above―is the best option across browsers.

Note: IE’s Nonstandard remainingSpace Property

Internet Explorer includes a nonstandard remainingSpace property on its localStorage object. We can handle our storage event differently depending on whether or not localStorage.remainingSpace is undefined. This approach has its risks, though; Microsoft could remove theremainingSpace property without fixing the storage event bug.

Determining Which Method Caused the Storage Event

There isn’t an easy way to determine which method out of setItem(), removeItem(), or clear() triggered the storage event. But there is a way: examine the event properties.

If the storage event was caused by invoking localStorage.clear(), the key, oldValue, and newValue properties of that object will all be null. If removeItem() was the trigger, oldValue will match the removed value and the newValue property will be null. If newValue isn’t null, it’s a safe bet that setItem() was the method invoked.

Storing Arrays and Objects

As mentioned earlier in this chapter, web storage keys and arrays must be strings. If you try to save objects or arrays, the browser will convert them to strings. This can lead to unexpected results. First, let’s take a look at saving an array:

var arrayOfAnimals = ['cat','dog','hamster','mouse','frog','rabbit'];

localStorage.setItem('animals', arrayOfAnimals);

console.log(localStorage.animals[0])

If we look at our web storage inspector, we can see that our list of animals was saved to localStorage, as shown in Figure 24.3.

An array saved to web storage as a string in Safari’s developer tools

Figure 24.3. An array saved to web storage as a string in Safari’s developer tools

But if you try to read the first item of that array, you’ll see that the first item is the letter C and not cat as in our array. Instead, it’s been converted to a comma-separated string.

Similarly, when an object is converted to a string, it becomes [object Object]. You lose all your properties and values, as shown in Figure 24.4.

The result of saving an object to web storage in Safari’s developer tools

Figure 24.4. The result of saving an object to web storage in Safari’s developer tools

To prevent this from happening, use the native JSON.stringify() method to turn your objects and arrays into strings before saving:

var foodcouples = {

'ham':'cheese',

'peanut_butter':'jelly',

'eggs':'toast'

}

localStorage.setItem(

'foodpairs_string',

JSON.stringify(foodcouples)

);

Using JSON.stringify() will serialize arrays and objects. They’ll be converted to specially formatted strings that hold indexes or properties and values, as shown in Figure 24.5.

Using JSON.stringify() to save an object to web storage in Safari’s developer tools

Figure 24.5. Using JSON.stringify() to save an object to web storage in Safari’s developer tools

To retrieve these values, use JSON.parse() to turn it back into a JavaScript object or array. A word of caution: JSON.parse() and JSON.stringify() can affect the performance of your application. Use them sparingly, especially when getting and setting items.

Limitations of Web Storage

Web storage comes with a couple of limitations: performance and capacity.

Web storage is synchronous. Reads and writes are added to the JavaScript processing queue immediately. Other tasks in the queue won't be completed until the engine is done writing to or reading from the storage area. For small chunks of data, this usually is no issue. But for larger chunks of data or a large number of write operations, it will be.

Web storage also has a size limit. Should your application reach that limit, the browser will throw a DOMException error. Use a try-catch statement to catch and handle this error. In browsers that support it, you can also use window.onerror (or use addEventListener and the error event):

try {

localStorage.setItem('keyname',value);

return true;

} catch (error) {

// Could alternatively update the UI.

console.log(error.message);

}

Size limits vary by browser and version. The web storage specification suggests an initial size of 5MB per origin. However, Safari, currently allows 2.5MB of data to be stored per origin. Chrome, Internet Explorer, and Opera 15+ currently store 5MB of data. Firefox stores 5MB by default, but the user can adjust this limit in the about:config menu. Older versions of Opera (12 and below) prompt the user to raise the storage limit.

The good news is that these values may increase. The bad news is that most browsers fail to expose the amount of storage space available to the application.

Now that we’ve covered the ins and outs of web storage, let’s take a look at a web database: IndexedDB.