The persistence of information in the visitor’s browser has historically been limited to keys and values in cookies. Cookie storage is limited in size and structure, and cookies are passed with each request to their corresponding websites, creating unnecessary overhead. Certain sets of data could improve the user experience if cookies could be stored and retrieved locally, instead of being retrieved by web servers each time they are used. In HTML5, two client-side storage facilities have been added: web storage, which includes session and local storage, and database storage. In this chapter, you will learn about these new client-side storage options and work through some recipes to get you started with storing data locally in the browser.
To start talking about the new storage APIs, let’s first look at what we previously had available in browsers to store information locally. Typically, to store information that could be retrieved at a later time in a visitor’s browser, you had to create a cookie. Information stored may have included user preferences, form information, user keys, or the like. However, the storage of cookies is limited to approximately 4KB, contains only simple key/value pairs, and increases overhead by sending the cookie with each request to the server.
The session and local web storage options provided in HTML5 are similar to cookies in that the structure is in key/value format, in which string values can be assigned to string-based keys. A value can be accessed in the session or local storage by asking for the appropriate key in the storage object. The difference between the session and local options is simply the scope of the object in the visitor’s session. In session storage, the data is stored only for that particular session with the website. Once the session has ended, through closing the window or tab in the browser, the storage will be removed. When the user returns, the session storage will be empty. If, instead, you want the data to be available across sessions, whether at the same time in another window or a new session at a later date, then you can use the local storage, which is persisted even after closing the current session.
Key/value pairs limit storing more complex information (or at least make it difficult). Thus, a third type of storage has been added to provide data storage like you would normally use on the server side: database storage. Database storage leverages a SQLite database or IndexedDB and allows you to store more complex data objects, as you will see later in the chapter. Like local storage, database storage persists across sessions. Which storage mechanism you use in your website or application depends on the type of information you need to store, the scope of the data, and how long you need the data to be persisted. Table 11.1 provides a quick way to determine the appropriate storage type.
When we discuss data storage, we must also discuss security. Like storing information in a server database of a web application, similar security guidelines should be applied to database storage on the client side. This is especially true since unlike a server where you may have control over the firewalls, users, passwords, and other security features, a visitor’s browser is outside the immediate network. This makes it that much more important to be vigilant about what is stored in the client browser and how it is stored. Encryption may be a consideration depending on the data you are storing, but if you are considering encryption of the data, you may want to reconsider storing it in the first place.
The storage options in HTML5 employ “origin-based” security by limiting access to session, local, and database client-side storage to pages that originate from the same domain from which the storage was created. In this manner, pages from other sites or applications cannot access the data. However, the “origin” security implementation uses the page’s origin as the determining factor for access to the session and local storage lists of key/value pairs. Because of this, there is the potential for storage to be exposed to embedded scripts. This makes knowing the actions that the external files you use in your pages that much more important.
Note
Data you store using the local, session, or database objects is limited to the specific browser that is being used by the visitor at the time. If the user returns to your site or application using a different browser or a different computer, then the storage will not be accessible. Data that you would like accessible anywhere will still need to be stored on your database server or in the cloud.
Client-side storage is being adopted rapidly by the different browser platforms, and Chrome is the leader in this support. There is still debate about the implementation of the best database storage mechanism by the different browsers, but Table 11.2 lists the current support of the client-side storage objects by various browsers.
sessionStorage
and localStorage
The session storage and local storage objects are similar in their implementations and differ only in the scope and persistence of the data that is written, as shown in Table 11.1. The browser provides a built-in storage interface for each storage type, sessionStorage
and localStorage
, and each uses a list of key/value pairs to store the data. You can set a value to a key and then retrieve the value by asking for the key. Both storage objects provide methods to set values, get values, remove a key, retrieve a key for a position in the list, and clear all the key/value pairs in the storage object. The methods and properties of each storage object type are the same, because they are inherited from the same Storage
interface defined in the Web Storage specification:
• setItem(key,value)
: Sets the key/value pair passed as parameters. If the key exists, then the value is updated with the value passed.
• getItem(key)
: Returns the value for the key passed as a parameter.
• removeItem(key)
: Removes the key/value pair as defined by the key passed.
• key(n)
: Returns the name of the key for the index provided.
• clear
: Removes all key/value pairs.
• length
: Provides the number of key/value pairs in the storage list.
To set an item, you call setItem
and provide a key and a value to be set:
setItem(key, value)
• key
: The key to file the string value under
• value
: The value to be stored with the file
The value is a string, so if you are storing a number, it will be treated as a string in the storage and will need to be converted back to the proper data type when retrieved. The key and value will then be stored in no particular order in the list of key/value pairs in the browser storage for the domain that the page has been loaded from.
If you set the item with a key that already exists, then the value of that key will be updated with the new value provided. Thus, the setItem
method acts as both a create method and an update method. To store the value “book” under the key “source” in the session storage, you would make the following call in JavaScript:
sessionStorage.setItem('source', 'book'),
The setItem
method does not have a return value, so to protect against possible storage errors, you can enclose the call in a try-catch block.
To retrieve the value of a key from either the session storage or the local storage, you call getItem
with the key that you are interested in. The return will be a string value, which you can then use in your script:
string getItem(key)
Here key
is the string key to retrieve the corresponding value.
To retrieve the value stored in the previous example, you would ask for the “source” key from the session storage by performing the following call:
textSource = sessionStorage.getItem('source'),
Since the session and local storage are based around storage objects, you can also use object dot notation to access stored values of keys by using the key as the property name, as shown here:
textSource = sessionStorage.source;
Now that we have briefly discussed the get
and set
methods of the session and local storage, we’ll show a basic recipe in action. We will start the recipes with the session object and then move to local storage.
Tip
In browsers such as Firefox, the user can disable storage. To validate that storage is available, your code should attempt to write and then retrieve a value. If the value cannot be retrieved, then storage may be disabled in the browser.
In this recipe, the HTML page will use the getItem
and setItem
methods of the sessionStorage
object to store and retrieve the number of times that a visitor views a page in the session. This number is incremented with each refresh of the page and displayed to the visitor. Perform the following steps to create the page in Listing 11.1:
1. Create a blank HTML page with a div
titled divVisits
.
2. Add the init
function in a set of script
tags with the code from Listing 11.1.
3. Add the window.addEventListener
event handler to launch the init
function after the page loads.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>11.1 Session Storage Page Visits</title>
<script>
function init() {
// reference the div for display
var divVisits = document.getElementById('divVisits'),
// check if our browser supports sessionStorage
if (window.sessionStorage) {
var visits; // number of visits to this page
// check to see if our variable exists using dot notation
if (sessionStorage.visits) {
// retrieve key and convert to int
visits = parseInt(sessionStorage.getItem('visits'));
// increment the visits
visits++;
} else {
// default to first visit
visits = 1;
}
// update our visits variable
sessionStorage.setItem('visits',visits);
// display the number of session visits
divVisits.innerHTML = 'Session page visits: ' + visits;
} else {
// sessionStorage not available
divVisits = 'Window sessionStorage is not available';
}
}
// onload launch our init function
window.addEventListener('load',init,false);
</script>
</head>
<body>
<div id="divVisits"></div>
</body>
</html>
When you load the page created in Listing 11.1 in your browser, the init
function will be launched. The init
function will first check to see whether the sessionStorage
object is available in the window by checking for window.sessionStorage
. If the sessionStorage
object is not available, then you will display a message in your div
on the page.
Assuming that the session Storage
object is available, the JavaScript code then checks to see whether the key “visits” exists by asking for the value through the dot notation: sessionStorage.visits
. If the key “visits” exists, then the value will be returned, and the check will pass. Otherwise, you know that this is the first time for this session and will start the “visits” count at 1. If the key is in session storage, then to demonstrate the get method of retrieving values, you will retrieve the value of the key via the getItem
method. When the value is retrieved, you convert the “visits” string value to an integer data type with parseInt
. This conversion must be done because all values in web storage are stored as basic strings. If your script is storing and then retrieving other data types, you will want to convert these strings to their proper types.
After you have either retrieved the number of visits and incremented it by 1 or set the initial value to 1, the script will then update the “visits” key in the session storage with the setItem
method. If the key was not in session storage previously, then setItem
will add the key/value pair, but if the “visits” key was already in session storage, then the value will be updated with the new number of visits.
You may wonder after loading the page created in Listing 11.1 in your browser how you can validate what is happening behind the scenes in the local session storage as the script runs. The Google Chrome browser has a set of tools titled Developer Tools that you may already be familiar with. If you open these tools in your browser window and refresh your page, you will be able to see the session storage key/value pairs under the Resources section, as shown in Figure 11.1.
In Figure 11.1, the domain that has stored keys is shown on the left under Session Storage, and on the right are the key/value pairs that are currently stored. In this case, the current value of visits is 2, and the JavaScript has displayed this to the user. The Developer Tools of Chrome are useful for working with the session and local storage because you can confirm not only that your script is working correctly but also add, update, and remove key/value pairs through the user interface. In the current version of the Developer Tools, you will need to refresh the storage key/value view pane with the Refresh button at the bottom if you change a key/value pair.
Tip
Any object that has a toString
built-in method can be stored in the value field of the key/value pairs of session and local storage. Even more complex JSON structured objects can be stored by using the JSON.stringify(yourObject)
and JSON.parse(itemRetrieved)
methods to convert the JSON object to a string and then convert the string retrieved back to your object.
This recipe performed a very simple setting and getting of a key/value pair in session storage. The next recipe will include error handling and show the availability of session storage across pages.
In this recipe, you will provide the visitor to your page with the opportunity to select a theme color for the background of the pages. The visitor’s selection will be stored in the session storage and retrieved on a second page to control the background color. A theme reset option is provided, which will remove the stored background key/value pair from the session storage. The following is the removeItem
method that will be used in this recipe:
removeItem(key)
Here key
is the key string to be removed.
Like any data storage mechanism, session and local storage have size limitations in place to protect the browser and the client’s machine. Each browser sets its own size limitation, but unlike cookies, which are limited to a rather small 4KB, the sizes allocated for session and local storage are much larger. At the time of writing this book, Internet Explorer 9, for example, has a 10MB limitation. If your script attempts to set or update a value for a key and hits this ceiling, an error will be thrown. To prevent your script from abruptly ending because of this error, you should wrap your setItem
method calls in a try-catch block. In this recipe, we will show you how to catch this error if it were to happen. The following steps and Listing 11.2 will allow you to create page 1 of the recipe:
1. Create the page in Listing 11.2 with the style
, script
, and body
tags.
2. Add the themeContent
and themeSettings div
s, as shown in Listing 11.2 with the corresponding HTML.
3. Add the initTheme
, setTheme
, resetTheme
, and applyTheme
functions to your script.
4. Add the window.addEventListener
statement to trigger the initTheme
function.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>11.2 Theme Selector - Page 1</title>
<style>
#themeContent {
background-color:#FFF;
border-style:solid;
border-width:2px;
}
#themeSettings {
margin:10px;
}
</style>
<script>
var themeDiv; // output display div
// function to initialize the theme
function initTheme() {
// set our div reference for output
themeDiv = document.getElementById('theme'),
// check if our browser supports sessionStorage
if (window.sessionStorage) {
// set the button handler
var btnResetTheme = document.getElementById('resetTheme'),
btnResetTheme.addEventListener('click',resetTheme,false);
// set the select list change handler
var selThemeColor = document.getElementById('themeColor'),
selThemeColor.addEventListener('change',setTheme,false);
// check if we have previously set the theme color
if (sessionStorage.themeColor) {
// set the initial theme color
var themeColor = sessionStorage.getItem('themeColor'),
document.getElementById(themeColor).selected = true;
applyTheme(themeColor);
}
} else {
themeDiv.innerHTML = 'sessionStorage is not supported.';
}
}
// set the chosen theme
function setTheme() {
// retrieve the theme color selected
var themeColor = document.getElementById('themeColor').value;
// use our try catch and set the theme color
try {
sessionStorage.setItem('themeColor',themeColor);
applyTheme(themeColor);
}
catch(err){
// error code 22 QUOTA_EXCEEDED_ERR says we ran out of space
if(err.code == QUOTA_EXCEEDED_ERR){
themeDiv.innerHTML = 'sessionStorage ran out of memory.';
// perform any other handling we want to here
}
}
}
// function to reset the theme color
function resetTheme() {
// remove the item from the session storage
sessionStorage.removeItem('themeColor'),
// reset display
document.getElementById('default').selected = true;
document.body.style.backgroundColor = '';
themeDiv.innerHTML = 'Theme reset.';
}
// apply a theme to the page
function applyTheme(themeColor) {
document.body.style.backgroundColor = themeColor;
themeDiv.innerHTML = 'Theme ' + themeColor + ' applied. ';
}
// initialize our window
window.addEventListener('load',initTheme,false);
</script>
</head>
<body>
<div id="themeContent">
<div id="themeSettings">
<H1>Page 1</H1>
Choose Theme:
<select id="themeColor">
<option id="default" value="">Select color...</option>
<option id="blue" value="blue">Blue</option>
<option id="red" value="red">Red</option>
<option id="yellow" value="yellow">Yellow</option>
<option id="green" value="green">Green</option>
</select>
<button id="resetTheme">Reset Theme</button><br><br>
<div id="theme"></div><br><br>
<a href="11_3_theme_page_2.html">Go To Page 2</a>
</div>
</div>
</body>
</html>
After setting the theme on page 1, which is in Listing 11.2, you can verify that the theme has been stored by loading a different page that reads the storage and uses the theme information to set the proper theme. Use the following steps and Listing 11.3 to create the second page to show the theme color previously selected:
1. Create a new page based on Listing 11.3 with the appropriate sections.
2. Modify the page’s a
tag in both pages to correspond to the name of your pages so that you can navigate back and forth between the pages.
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8" />
<title>11.3 Showing the Theme - Page 2</title>
<style>
#themeContent {
background-color:#FFF;
border-style:solid;
border-width:2px;
}
#themeSettings {
margin:10px;
}
</style>
<script>
// initialize our page
function init() {
// retrieve the saved theme color
var themeColor = sessionStorage.getItem('themeColor'),
applyTheme(themeColor);
}
// apply the theme to the page
function applyTheme(themeColor) {
document.body.style.backgroundColor = themeColor;
var themeDiv = document.getElementById('theme'),
themeDiv.innerHTML = themeColor + ' theme.';
}
// initialize the page
window.addEventListener('load',init,false);
</script>
</head>
<body>
<div id="themeContent">
<div id="themeSettings">
<H1>Page 2</H1>
<div id="theme"></div>
<br><br>
<a href="11_2_theme_page_1.html">Go To Page 1</a>
</div>
</div>
</body>
</html>
When page 1 is loaded into your browser, the page first retrieves the currently stored theme color from the session storage. The first time the page is run, there is no theme, and the return value is empty, so the theme is set to the browser’s default. On subsequent loads of the page, the color would be retrieved, set to the background color, and set as the selected value in the color select control.
Note
To run recipes involving session storage, you will need to upload the pages to a server instead of running them in “local mode.” The reason is that in local mode some browsers will throw an error saying that the operation is not supported. For example, in Firefox, an “Operation is not supported” code 9 error will be thrown.
Once loaded, the visitor can change the drop-down list of options to another color, which will change the page background color and store the color in session storage under the key themeColor
. In the script, this setItem
call to store the setting has been wrapped in a try-catch block. If triggered because of the size constraints, the code informs the visitor that the session storage is full, but you could perform whatever actions are needed when this scenario occurs. After setting the theme color, the visitor can click Go To Page 2, which will load the second page. Since the second page is in the same session as the first page, it will also have access to the session storage key/value pair saved on page 1. The second page then retrieves this item and sets the background to the stored color.
In Listing 11.2, you also added the option for the visitor to reset the theme. In this case, the recipe removes the key/value pair from the session storage with the removeItem
method. The recipe could have also updated the key by calling setItem
with an empty string value.
At the beginning of this chapter, we discussed how session storage allows you to store data for that session only. If you load the page created in Listing 11.2 into two tabs or windows in the same browser and then select different colors, each tab will have its own background color since there is a different session for each tab or window. To have the values available across browser windows or tabs, and even after the browser is closed and reopened, you will need to use the local storage object. We will look at the local storage object in the next recipe.
Session storage, as shown in the previous recipes, provides you with a method to store items for the period of the visitor’s session, which can be beneficial for short-term storage. At times, though, you will want to store data in the visitor’s browser for use when they return or even across currently active sessions. This is where local storage is valuable. Local storage uses the same key/value pair list to store data with the getItem
, setItem
, and removeItem
web storage methods. The important factor is that the data you store in local storage persists even after the session is closed.
This recipe shows an example of remembering form data that a visitor has previously filled out. A visitor could come to your site, start a form, and then navigate away or close their browser. Typically, the visitor would have found the form empty when they returned to the site. In this recipe, as the visitor fills out the form, the JavaScript catches the changes and stores the form fields and values in the local storage. Then if the page is closed and reloaded, the page checks local storage to see whether there is stored form data and automatically populates the form with the information. A similar implementation could even handle collecting form data from a multipage form and storing it locally until the entire form has been completed.
Two new concepts with the session and local storage are employed in this recipe: key
and length
. The key
method allows you to retrieve the name of the key stored by supplying an index value to the key
method:
DOMString key(index)
Here, index
is the index of the key/value pair to return the key string for.
The recipe also uses the length
property of the session and local storage, which returns the number of key/value pairs in the session or local storage. To return the number of key/value pairs, you would use a call like the following:
var numItems = localStorage.length;
In this recipe, you will use both the key
method and the length
property to loop through the localStorage
key/value pairs. Let’s do the following steps to get started with Listing 11.4:
1. Create a blank HTML file and add the body HTML from Listing 11.4, which holds the form and form fields you will be using. For simplicity sake, we have used onchange
inline for the form fields, but you could replace these with event listeners for each of the fields.
2. Add the script
tags in the head of the HTML file with the window.addEventListener
function to launch the checkStorage
function when the page is loaded.
3. Add the checkStorage
function in the script, as shown in Listing 11.4.
4. Add the changeField
function in the script, as shown in Listing 11.4, which will handle the form field changes.
5. Load the file in your Chrome browser with the Developer Tools open to the local storage area, and enter information in the fields.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>11.4 Storing Form Data with Local Storage</title>
<script>
// function to read storage and init form
function checkStorage() {
// check if local storage available
if (window.localStorage) {
var key, value, field;
// loop through local storage
for (var i = 0; i < localStorage.length; i++) {
// retrieve the key
key = localStorage.key(i);
// set the field from the key
field = document.getElementById(key);
// check for field and assign value
if (field) {
// retrieve the value
value = unescape(localStorage.getItem(key));
// set the field value
field.value = value;
}
} // end for loop
} // end local storage check
} // end function
// set the localStorage with the changed field
function changeField(formField) {
// check if local storage available
if (window.localStorage) {
var key, value;
// set key to form field id
key = formField.id;
// set value to form field value
value = escape(formField.value);
// try to set item in local storage
try {
localStorage.setItem(key, value);
}
catch (err) {
if (err.code == QUOTA_EXCEEDED_ERR) {
alert('localStorage ran out of memory.'),
}
}
} else {
alert('localStorage is not supported.'),
}
}
// initialize our form from storage
window.addEventListener('load',checkStorage,false);
</script>
</head>
<body>
<h1>My Form</h1>
<form id='myForm'>
<table>
<tr>
<td>First Name:</td>
<td><input type="text" id="firstName" onchange="changeField(this);" /></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input type="text" id="lastName" onchange="changeField(this);" /></td>
</tr>
<tr>
<td>Email:</td>
<td><input type="email" id="email" onchange="changeField(this);" /></td>
</tr>
<tr>
<td>Telephone:</td>
<td><input type="tel" id="phone" onchange="changeField(this);" /></td>
</tr>
</table>
</form>
</body>
</html>
After you enter your first name in the firstName
field and tab to the next field, the onchange
event will fire for the firstName
field. The changeField
function will then execute with the form field of firstName
passed as a parameter. The changeField
function will then store the field data in a key/value pair in localStorage
. The function uses the ID of the form field for the key and the value as the value in the key/value pair. So, if you have Developer Tools open in Chrome when you are doing this, you should now see the key firstName
with the value of the text entry you supplied.
To see the real power of localStorage
, close your tab or browser window and then reopen it to this form page. When you reopen the page, the checkStorage
function will execute on page load. This function is designed to check whether you have previously stored any form information in local storage and retrieve this information. The function will retrieve each key/value pair by looping through the localStorage
list, check to see whether there is a form field ID that matches the key name, and if so set the field value to the value retrieved.
In this recipe, you use simple text fields to show the power of localStorage
, but this could be used for other types of form inputs as well. The changeField
and checkStorage
functions would need to be updated to handle different logic for different field types such as a select list or a radio button, but the value could be stored like the regular text input in local storage.
After the page is finished with the stored form information and the form is ultimately submitted to the server, you would ideally want to remove the fields. Besides the removeItem
method that you saw before, the storage API provides a method for clearing the entire session or local storage: clear
. The clear
method takes no parameters nor does it return any result and “clears” all key/value pairs for the page’s domain out of the list. The following is an example of the call:
localStorage.clear();
The execution of this line will clear all key/value pairs from the local storage list no matter which page added them or when they were added. Because this is an all-or-nothing type of method, you should be absolutely positive you want to clear the contents of the session storage or the local storage.
In the recipes to this point, you learned about the basics of adding, updating, and removing stored data for a site on the client machine. This assumes the visitor is on a single page interacting with the site at the time. If the stored information was changed from another page in the browser, you have had no way to automatically handle that change on a previously open page. The client-side storage API provides an event structure for session storage and local storage updates that you can catch and handle on an open page. This next recipe combines the methods you have seen along with storage update events to refresh your page content.
In this recipe, you will use the local storage methods and properties to implement a notes sidebar, which will allow the visitor to take notes as they browse through the site. The notes are stored in the local storage of the visitor’s browser, and the order of the notes is maintained. The visitor can add a new note, update a note, remove a note, or remove all their notes. In addition, if the visitor is viewing the site in multiple tabs or browser instances, the notes will automatically update on all pages when a change happens on one page.
To automatically update the list of notes on a browser tab or window based on a change in the list of the notes by the visitor on another tab or window, you will need to employ the storage event mechanism. Like other events you have seen, a storage event is raised when the session storage or local storage is modified. The event could occur when a new key/value pair is added, updated, or removed. A handler on the page can listen for these events and perform actions based on the receipt of the event. The storageEvent
contains attributes that will help determine the actions that need to be taken (Table 11.3). In this recipe, you will catch the event, verify that it is from local storage, alert the visitor of the change, and then refresh the notes list so that you are showing the current list.
Let’s get started with the following steps for Listing 11.5:
1. Create a blank HTML page with the HTML body
and style
tags, as shown in Listing 11.5.
2. Add the script
tags and keyCode
variable declaration. This variable holds a key that you will prefix each stored note with.
3. Add the window.addEventListener
line and the initNoteBoard
and updateNoteBoard
functions.
4. Add the note functions: addNote
, changeNote
, updateNote
, and removeNote
. The changeNote
function loads the note into the update form, while updateNote
modifies the note in local storage.
5. Add the clearAllNotes
function to allow the visitor to reset the note board.
6. Add the event handler function, onStorageEvent
, which will catch any storage events.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>11.5 Storing Notes in Local Storage</title>
<style>
* {margin: 0; padding: 0;}
body {padding: 20px;}
h1 {font-size: 120%; margin: 0 0 .5em;}
section {width: 300px;}
#noteBoard, textarea {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
-moz-box-shadow: 0px 0px 4px rgba(0,0,0,.4);
-webkit-box-shadow: 0px 0px 4px rgba(0,0,0,.4);
box-shadow: 0px 0px 4px rgba(0,0,0,.4);}
#noteBoard {
background: #FCFABA;
float: right;
padding: 10px 20px;}
#noteBoard div {
border-bottom: 1px dashed #CCC;
margin: 0 0 5px;
padding: 5px 0;
width: 100%;}
#noteBoard div.buttons {border: none;}
#addNote, #updateNote {float: left;}
#addNote {
border-right: 1px dashed #ccc;
margin: 0 50px 0 0;
padding: 0 50px 0 0;}
#updateNote { display:none;}
textarea {
border: none;
clear: both;
height: 150px;
margin: 0 0 10px;
padding: 10px;
width: 280px;}
input[type="text"] {margin: 0 0 10px; padding: 4px; }
button {padding: 5px;}
</style>
<script>
// note prefix for storage entries
var keyCode = 'note';
// initialize our note board
function initNoteBoard() {
// set our listener for storage changes
window.addEventListener('storage', onStorageEvent, false);
// set listeners for new and update note
var btnAddNote = document.getElementById('btnAddNote'),
var btnUpdateNote = document.getElementById('btnUpdateNote'),
btnAddNote.addEventListener('click',addNote,false);
btnUpdateNote.addEventListener('click',updateNote,false);
// update the display
updateNoteBoard();
}
// our storage event handler
function onStorageEvent(eventObj) {
if (eventObj.storageArea == localStorage) {
// alert visitor of change
alert(eventObj.key + ' changed from "' +
eventObj.oldValue +
'" to "' +
eventObj.newValue + '".'),
// update the display
updateNoteBoard();
}
}
// add a note function
function addNote() {
// retrieve the number of notes we have
var numNotes = parseInt(localStorage.getItem('numNotes'));
if (isNaN(numNotes)) {
numNotes = 0;
}
// set our key and value
var noteKey = keyCode+numNotes;
var noteValue = document.getElementById('note').value;
// set our note
localStorage.setItem(noteKey, noteValue);
// update the number of notes
numNotes++;
localStorage.setItem('numNotes', numNotes);
// update our note board
updateNoteBoard();
// reset our note entry
document.getElementById('note').value = '';
}
// function to load the note to be updated
function changeNote(noteKey) {
// set our key and value in the update form
document.getElementById('oldKey').value = noteKey;
document.getElementById('oldNote').value = localStorage.getItem(noteKey);
// show our update note area
document.getElementById('updateNote').style.display = 'block';
}
// function to update the note
function updateNote() {
// retrieve our new values for the note
var key = document.getElementById('oldKey').value;
var note = document.getElementById('oldNote').value;
// update the key/value pair
localStorage.setItem(key, note);
// clear our update area
document.getElementById('updateNote').style.display = 'none';
document.getElementById('oldKey').value = '';
document.getElementById('oldNote').value = '';
// update our display
updateNoteBoard();
}
// function to remove a note
function removeNote(noteKey) {
// retrieve our number of notes
var numNotes = parseInt(localStorage.getItem('numNotes'));
// extract our note key index from the note key
keyIdx = parseInt(noteKey.substring(keyCode.length,noteKey.length));
// loop through notes and move each down the list
for (var i = keyIdx; i < numNotes; i++) {
localStorage.setItem(keyCode+i,localStorage.getItem(keyCode+(i+1)));
}
// update our number of notes
numNotes--;
localStorage.setItem('numNotes',numNotes);
// remove the last note that is now a duplicate
localStorage.removeItem(keyCode + numNotes);
// update our display
updateNoteBoard();
}
// function to remove all notes
function clearAllNotes() {
// retrieve the number of notes
var numNotes = parseInt(localStorage.getItem('numNotes'));
if (isNaN(numNotes)) {
numNotes = 0;
}
// loop through note key/value pairs and remove
for (var i = 0; i < numNotes; i++) {
localStorage.removeItem(keyCode+i);
}
// update our number of notes to 0
localStorage.setItem('numNotes','0'),
// update the display
updateNoteBoard();
}
// display our notes
function updateNoteBoard() {
// set our display area
var noteBoard = document.getElementById('noteBoard'),
// retrieve our number of notes
var numNotes = parseInt(localStorage.getItem('numNotes'));
// set default to 0 if no notes
if (isNaN(numNotes)) {
numNotes = 0;
}
var notes = '<div>My Notes:</div>';
var key = '';
var value = '';
// loop through the notes
for (var i = 0; i < numNotes; i++) {
// create our key with our prefix
key = keyCode + i;
// retrieve our key
value = localStorage.getItem(key);
// build our display for this note
notes += '<div><p>'+value+'</p><div class="buttons">'+
'<button onclick="changeNote(''+key+''),">Change</button>'+
'<button onclick="removeNote(''+key+''),">Remove</button>'+
'</div>'+
'</div>';
}
// finish off our display
notes += '<div style="float:right;"><button id="clearAllNotes">Remove All Notes</button></div>';
// set the list to the display
noteBoard.innerHTML = notes;
// set listener for clearing all notes
var btnClearAllNotes = document.getElementById('clearAllNotes'),
btnClearAllNotes.addEventListener('click',clearAllNotes,false);
}
// initialize our note board
window.addEventListener('load',initNoteBoard,false);
</script>
</head>
<body>
<h1>Note Board</h1>
<section id="noteBoard"></section>
<section id="addNote">
<h1>Add a new note here:</h1>
<textarea name="note" id="note"></textarea>
<button id="btnAddNote">Add Note</button>
</section>
<section id="updateNote">
<h1>Update the note</h1>
<input type="text" name="oldKey" id="oldKey" disabled />
<textarea name="oldNote" id="oldNote"></textarea>
<button id="btnUpdateNote">Update Note</button>
</section>
</body>
</html>
When the page loads, the initialization launches the initNoteBoard
function. This function attaches the event listener to catch any events that are storage events and will launch the storageEvent
handler. Next, the initNoteBoard
updates the display of notes by checking local storage. In the updateNoteBoard
function, the script will first retrieve the key numNotes
, which is a count of the number of notes that you have stored previously. Then, using numNotes
, the script will loop through and retrieve each key/value pair using the keyCode
, “note” with the index starting at 0. So, to retrieve the first note, the page calls getItem
with the key note0
. The keyCode
and numNotes
keys are used for two reasons. First, using them will ensure that you do not have to cycle through any other key/value pairs that may be stored in local storage for the site, and second, this will allow you to keep your notes in order. Remember that for session and local storage there is no specific order to the key/value pairs in the list. The browser will insert them using setItem
in no particular order (see Figure 11.2).
The real power of this recipe is in the event handling. If you load the page into two browser tabs or windows and add a new note in one of the windows, the event handler will be triggered. In this case, the onStorageEvent
function will be called, and the function will perform two actions. First, the function alerts the visitor that there has been a change and provides the visitor with a little information on the update. Second, the event handler calls the update display function to show the updated list of notes. If you play around with the addition, update, and removal of notes, you will see how this event handling behaves.
If you remove a note, the event handler will show you multiple changes happening in local storage. The reason for this is how the order of the notes is maintained. To maintain the order of the list, the script uses an algorithm by which entries after the one to be removed are pushed down the list and the last entry is then removed. Remember that the clear method will not only remove the key/value pairs you are interested in but will remove all key/value pairs in the storage.
Note
Storage events do not get triggered on the same page that made the session or local storage modification. Instead, storage events are propagated notices to other browser windows or tabs. To catch a change on the page that made the change, you will need to call the logic directly when the change is made.
In this recipe, you used a specific key, numNotes
, to hold the number of notes that had been entered. In addition, to keep the notes ordered, you used a key prefix with an incrementing number such as note0
, note1
, and note2
. What if the script needed to sort the list based on different criteria or needed to catalog your notes into categories? You could devise your own method using JavaScript and objects, but with HTML5 and its definition of the Web SQL Database API, this task becomes quite simple, as you will see in this chapter’s final recipe.
As you saw in the previous recipes of this chapter, the Session and Local Storage APIs allow for the storage of simple data in the client browser. If you want to store more complex objects, properties, and relationships, you will need to leverage the new Web SQL Database API or IndexedDB API. Like other APIs, these are not part of HTML5 core specification but a set of additional specifications being adopted by browsers. In general, the Web SQL Database API is currently supported by several browsers, while the IndexedDB API is still being defined and has limited support. Over time, it is expected that the IndexedDB will be more widely supported, but we will focus on the Web SQL Database API in this chapter for demonstration.
If you have had the opportunity to work on server-side pages and database access, most likely you have used a similar SQL-based database. And if not, then here is your opportunity to have a powerful data management storage system right in the browser. Table 11.4 shows the browser support for the Web SQL Database API.
The Web SQL Database API specification is based on SQLite, so if you have already used SQLite in a mobile environment such as iOS development, then it will be familiar. At the same time, if you have used a server-side database such as MySQL, the basic commands and structure will be recognizable. If you have no SQL experience, then you may want to seek some resources on SQL commands, but we will cover the basic create, read, update, and delete commands in the next recipe. Like session and local storage, the web database is available only to the pages in the same origin from which it was created for security purposes; however, unlike session and local storage, the web database can store complex data with relationship information.
Note
Currently, there is some debate as to the underlying database format for use with the Web SQL Database API. SQLite is listed in the W3 Web Database specification and has been implemented by several browsers. However, until there is general acceptance, the web database group of the W3C has put the specification on hold for further development. This has resulted in further pushing of the IndexedDB specification, which is gaining traction.
To interact with the web database, the API provides three core asynchronous commands. The reason these commands are typically asynchronous is so that the rendering of the browser page does not get “blocked” while waiting for a database call to return. Depending on the amount of data requested in a transaction, the page could have to wait several seconds for the set of data to be returned. The three core commands allow you to open (or create a database), create a transaction, and execute SQL statements within the transaction:
• openDatabase
: Opens a present database or creates a database based on the parameters passed if the database is not present.
• Transaction
: Encompasses the commands to the database so that you can perform a rollback on the entire transaction if needed. There is a read-only version available, readTransaction
.
• executeSql
: The method to run an actual SQL command on the open database.
The openDatabase
method opens an established database, or if the database is not present, the method will automatically create the database described in the parameters and then open the database as follows:
WindowDatabase openDatabase(name, version, displayName, estimatedSize [, creationCallBack])
The parameters of the openDatabase
method are as follows:
• name
: The name of the database
• version
: A version number you assign to the database
• displayName
: A user-friendly display name for the database
• estimatedSize
: An estimated size for the database
• creationCallBack
: (Optional) The function called when the database has been created for the first time
A WindowDatabase
object is returned that will then be used to perform transactions with. The database is automatically closed when the browser window or tab is closed.
Tip
The version number of the openDatabase
method is a value that you can use to track versions of your database structure and data. This can be helpful if you are using the database to provide quick, local lookups of data. When your lookup data changes and the user comes back to your website, you could check this version by having stored the value in a table in a master web database. If the lookup database was not up-to-date, then a new one could be installed. Or, you could use the error code when opening to validate that it is the wrong version.
To query the database, a transaction request must be performed. The transaction method wraps one or more executeSql
commands to create a single transaction for the database to act on. The transaction can be used for rollback purposes if any of the SQL queries fail. The callback parameter of the transaction encompasses the inline function for the executeSql
methods, as shown here:
transaction(callback [, errorCallback] [, successCallback])
The parameters of the transaction
method are as follows:
• callback
: The functionality to perform within the transaction
• errorCallback
: (Optional) The function to handle any errors that occurred
• successCallback
: (Optional) The success function called if all processes within the transaction were successful
The executeSql
method takes your SQL query command as the first parameter and can include an array or optional arguments, a successful callback, and an optional error callback, as shown here:
executeSql(sqlStatement [, arguments] [, callback] [, errorCallback])
The parameters of executeSql
method are as follows:
• sqlStatement
: A SQL statement to be executed
• arguments
: (Optional) An array of optional arguments
• callback
: (Optional) The functionality to perform if the SQL statement execution is successful
• errorCallback
: (Optional) The functionality to perform if the SQL statement execution creates an error
The error handler parameter option for the database API methods is used to catch any errors that could occur in interacting with the database. This could include instances such as an error in your SQL or an issue in accessing the database:
• UNKNOWN_ERR (0)
: An unknown error beyond the ones listed here occurred.
• DATABASE_ERR (1)
: An error occurred in the database that is not covered by an error category.
• VERSION_ERR (2)
: The version stated in the command does not match the version of the database.
• TOO_LARGE_ERR (3)
: The resulting data set from the database to your request was too large to be returned.
• QUOTA_ERR (4)
: There is no remaining storage space or the user declined to allocate more for your database.
• SYNTAX_ERR (5)
: The request failed because of a syntax error.
• CONSTRAINT_ERR (6)
: The request failed because it violated a constraint in the structure of the database.
• TIMEOUT_ERR (7)
: The transaction failed because it could not establish a database lock in a timely manner.
A common error seen is SYNTAX_ERR
, which signifies there is a syntax issue in your SQL command. The message portion of the error object passed to your error handler will be critical in giving you more specifics about the particular error code.
At this point, you may be asking how this all works together to store and retrieve information in a database. The next recipe puts these core methods together to do exactly that.
This recipe will use the interfaces for the web database that we have just reviewed to create a shopping list that will be stored completely in the browser’s database resource. The visitor will be able to add items to the list, including a quantity and name, and select from a drop-down the grocery department the item is found in. Whenever an item is added, the grocery list will be updated and displayed. The list will be grouped by department to make a shopping trip easier. The visitor can remove items from the list and clear the list completely. If the visitor leaves and returns to the page, the page will show the stored items.
The database that is created by this recipe will have two tables: a groceryitems table to hold the items in the list and a departments table, which will be a lookup table with a predefined list of departments. When the page first loads, the database and tables will be created, and the departments table will be populated with the department information. The groceryitems table will have four fields, as defined in Table 11.5.
The departments table will have two fields, as defined in Table 11.6.
By default when the departments table is created, the script will add four departments into the departments table.
Like with session storage and local storage, the Chrome Developer Tools provide a proficient way to view the SQLite database as your JavaScript executes. You can find the Database section under the Resources section in the Developer Tools window. Unlike session storage and local storage items, the database tables and data cannot be edited in Developer Tools, but this functionality may be added in the future.
For debugging purposes, a Remove Database button is also put on the page so that you can remove the database. Currently, the web database specification does not contain a method to remove a database. To work around this, and remove a database, you will have to “drop” the tables that have been created in the database, and your database will be empty. As you develop using the web database API, re-creating the database will be useful to verify your SQL database creation transactions.
To create the grocery list page, follow these steps:
1. Create the basic structure of the page with the HTML body, the shoppingdb var
declaration, the window.addEventListener
load event handler, and the init
JavaScript function.
2. Add the JavaScript functions that are called from the init
function: openShoppingDb
and dbPresent
.
3. To finish the initialization of the database, add the initShoppingDb
, onDbError
, and nullHandler
functions. The initShoppingDb
function creates the tables and inserts the departments. The onDbError
function is a generic function to handle database errors that may occur as the script perform transactions, and the nullHandler
is used to catch successful transaction events. If you load the page at this point, you should see the database being created by your script in the Chrome Developer Tools.
4. You need to display the grocery items when the page is shown by adding the initPage
, getDepartments
, and showDepartments
functions. The “get and show” technique will be common because the transaction requests are asynchronous and will need a callback function to handle the results. In this case, the getDepartments
performs the request to get the departments from the departments table and then the showDepartments
function to display them. The showDepartments
will create the options for our select list.
5. To finish the display on initialization, you need to show any grocery items that have been previously stored in the database, so add getGroceryItems
and showGroceryItems
from Listing 11.6.
6. To add an item, the addGroceryItem
function is called, which will take the inputs of the input fields and insert them into the groceryitems table. If successful, the page will display the new list of grocery items. To remove an item, add the deleteItem
method.
7. To complete the page, add the resetGroceryList
method to clear the grocery list and the debug function of removeDatabase
to drop the database tables.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>11.6 Database Grocery List</title>
<style>
section {
margin-bottom:20px;
}
</style>
<script>
var shoppingdb = null; // our database reference
// function kick off init of page
function init() {
// check to see if database support available
if (window.openDatabase) {
// set the button click handlers
var btnAddGroceryItem = document.getElementById('addGroceryItem'),
var btnResetGroceryList = document.getElementById('resetGroceryList'),
var btnRemoveDatabase = document.getElementById('removeDatabase'),
btnAddGroceryItem.addEventListener('click',addGroceryItem,false);
btnResetGroceryList.addEventListener('click',resetGroceryList,false);
btnRemoveDatabase.addEventListener('click',removeDatabase,false);
// open the database
openShoppingDb();
// check if we have reference to database
if (shoppingdb) {
// check if the database initialized or not
dbPresent();
}
} else {
alert('Databases are not supported in this browser'),
}
}
// open database function
function openShoppingDb() {
// try to open our database
try {
var dbSize = 5000000; // 5MB size
shoppingdb = openDatabase('shoppingdb', '1.0', 'shopping list', dbSize);
} catch (err) {
// Error occurred opening database
shoppingdb = null;
console.log('Error opening database: ' + err.code + ' - ' + err.message);
return;
}
}
// function check to see if database tables present
function dbPresent() {
// start our transaction
shoppingdb.readTransaction(function(tx) {
// execute sql to pull first record
// if successful, then initialize the page
// if not, then initialize the database
tx.executeSql('SELECT 1 FROM departments', [],
initPage, initShoppingDb);}
);
}
// function initialize the page
function initPage() {
// get the departments
getDepartments();
// load any grocery items present
getGroceryItems();
}
// initialize the shopping database
function initShoppingDb() {
// start our database transaction
shoppingdb.transaction(function(tx) {
// create tables for database
tx.executeSql('CREATE TABLE IF NOT EXISTS ' +
'groceryitems(itemid INTEGER NOT NULL PRIMARY KEY, quantity INTEGER, itemname TEXT, deptid INTEGER)',
[], nullHandler, onDbError);
tx.executeSql('CREATE TABLE IF NOT EXISTS ' +
'departments(deptid INTEGER NOT NULL PRIMARY KEY, deptname TEXT)',
[], nullHandler, onDbError);
// Fill our departments table
tx.executeSql('INSERT INTO departments(deptname) VALUES (?)',
['Fresh Produce'], nullHandler, onDbError);
tx.executeSql('INSERT INTO departments(deptname) VALUES (?)',
['Deli'], nullHandler, onDbError);
tx.executeSql('INSERT INTO departments(deptname) VALUES (?)',
['Bakery'], nullHandler, onDbError);
tx.executeSql('INSERT INTO departments(deptname) VALUES (?)',
['Grocery'], initPage, onDbError);
});
}
// standard db error function
function onDbError(tx, err) {
alert('Database error occurred: ' + err.code + '|' + err.message );
}
// our null handler for success
function nullHandler(tx, r) {
return;
}
// function get the department list
function getDepartments() {
// begin our database transaction
shoppingdb.readTransaction(function(tx) {
// execute our sql to retrieve the departments
tx.executeSql('SELECT * FROM departments ORDER BY deptname ASC', [],
showDepartments, onDbError);
});
}
// function show the departments retrieved
function showDepartments(tx, rs) {
// get reference to the department select
var selectObj = document.getElementById('department'),
// loop through department recordset and add to select
for (var i=0; i < rs.rows.length; i++) {
row = rs.rows.item(i);
selectObj.options[selectObj.options.length] =
new Option(row.deptname, row.deptid, false, false);
}
}
// function to get the grocery items
function getGroceryItems() {
// start our database transaction
shoppingdb.readTransaction(function(tx) {
// retrieve the list of items
tx.executeSql('SELECT * FROM groceryitems, departments WHERE groceryitems.deptid=departments.deptid ORDER BY deptname ASC',
[], showGroceryItems, onDbError);
});
}
// function to show the grocery item list
function showGroceryItems(tx, rs) {
var myShoppingList = document.getElementById('myShoppingList'),
var tableRow = '<table>';
var row = null;
// set table headings
tableRow += '<tr><td>Quantity</td><td>Item</td><td>Department</td><td>Delete</td></tr>';
// loop through record set returned
for (var i=0; i < rs.rows.length; i++) {
// retrieve our row
row = rs.rows.item(i);
// build our table row
tableRow += '<tr><td>' + row.quantity + '</td>' +
'<td>' + row.itemname + '</td>' +
'<td>' + row.deptname + '</td>' +
'<td><button onclick="deleteItem(' +
row.itemid + '),">X</button></td></tr>';
}
tableRow += '</table>';
// set our info in the div
myShoppingList.innerHTML = tableRow;
}
// add a grocery item to our database groceryitems table
function addGroceryItem() {
// open our database transaction async call
shoppingdb.transaction(function(tx){
// retrieve our data for the grocery item
var itemname = document.getElementById('item').value;
var quantity = parseInt(document.getElementById('quantity').value);
var deptid = parseInt(document.getElementById('department').value);
// perform our executeSql insert
tx.executeSql('INSERT INTO groceryitems(quantity, itemname, deptid) VALUES (?,?,?)',
[quantity, itemname, deptid], getGroceryItems, onDbError);
});
}
// function remove an item from the list
function deleteItem(id) {
// start our transaction
shoppingdb.transaction(function(tx) {
// execute sql to delete the item from the database
tx.executeSql('DELETE FROM groceryitems WHERE itemid=?',
[id], getGroceryItems, onDbError);
}
);
}
// function reset grocery list
function resetGroceryList() {
// start our transaction
shoppingdb.transaction(function(tx) {
// execute our sql to drop the grocery item table
tx.executeSql('DROP TABLE groceryitems', [], nullHandler, onDbError);
});
}
// function remove database
function removeDatabase() {
// start our transaction
shoppingdb.transaction(function(tx) {
// drop our tables
tx.executeSql('DROP TABLE departments', [], nullHandler, onDbError);
tx.executeSql('DROP TABLE groceryitems', [], nullHandler, onDbError);
});
}
// call init on load
window.addEventListener('load',init,false);
</script>
</head>
<body>
<section>
Quantity:<input type="number" id="quantity" />
Item:<input type="text" id="item" placeholder="Milk" />
Department:<select id="department"></select>
<button id="addGroceryItem">Add Item</button>
</section>
<section id="myShoppingList"></section>
<section>
<button id="resetGroceryList">Reset Grocery List</button>
<button id="removeDatabase">Remove Database</button>
</section>
</body>
</html>
When the page is loaded in your browser, the init
function will check that the Web SQL Database API is available by verifying that the window.openDatabase
method is available. If available, the openDatabase
method is called with the database parameters. In this case, the script sets roughly 5MB for the size because the browsers appear not to confirm with the user for database creations of this size or smaller. If your database exceeds this size at creation or later, the browser will confirm with the user to allocate more space. If this is the first time that you have run the code, then the database will be created and the handle returned; otherwise, the database will be opened.
Since the recipe uses the departments table values to load the select list of departments, you need to make sure that if this is a new database instance that you populate the departments table with your lookup information. To check whether you need to create the tables, the script checks for the presence of the database tables by doing a simple query and tries to select the first row in the department table in the dbPresent
method. If the table does not exist yet, then the script creates both tables and inserts the department values into the departments table in the initShoppingDb
method. After the database is loaded, the script loads the departments into the select list from the database and displays any stored grocery items by calling the getGroceryItems
method. The getGroceryItems
method will open another transaction to request all items from the groceryitems table and, if successful, pass the resulting data set of rows to the showGroceryItems
method. The show method will then loop through the grocery items and display them in the myShoppingList
section of the page.
The transaction
method is used for SQL commands that require write permissions in the database. By requiring write permission, the transaction puts a write lock on the database, preventing others from writing to the database at that time. If you are performing only read commands on the database, you should use the readTransaction
method. The readTransaction
method is called the same way as the transaction
method.
Once the page is initialized and the database and the departments are populated, the visitor can then enter a quantity and item and select the department for the new item for their shopping list. The addGroceryItem
will then take this information, create a database transaction, and perform a SQL insert to add the item in an inline function call. If the insert fails, you will call the error handler, onDbError
, that will display our error. If successful, then the script will call getGroceryItems
and subsequently showGroceryItems
to display the resulting set of rows returned from the groceryitems table. Figure 11.3 shows the output after having entered several items.
When the getGroceryItems
function is called, the script will call the readTransaction
method with an inline function call to executeSql
. The SQL command selects from the groceryitems table all records, groups them by department, and sorts them by department alphabetically. In addition, the SQL command joins the groceryitems table and departments table on the deptid so that the return set will also include the department name to display. If the executeSql
call is successful, the script will return the results to showGroceryItems
, passing the transaction reference and recordset of results. To display the results from the recordset, the script loops through the recordset rows. Each row represents a record returned by the SQL query, and you can reference each field via dot notation.
To remove an item, the visitor clicks the X button, which calls the deleteItem
method with the item ID passed. The page then uses the item ID passed to delete the item from the groceryitems table. If the call is successful, then the list is refreshed through the get and show grocery items functions. Lastly, the visitor can reset the grocery list, which will delete all items from the groceryitems table.
This recipe is a quick sample of integrating client web databases into your websites and applications. The recipe covered writing, retrieving, updating, and deleting records along with the initial creation of the database. This functionality provides the building blocks needed to implement your own database storage.
In this chapter, you learned about some of the exciting new methods to store data on the client browser. Previously, client browser technology was limited to the use of cookies. With the addition of session storage, local storage, and web databases, there is now a robust platform, albeit young, on which you have several options to store simple key/value pairs or complex data structures. With these storage options, you can create powerful offline applications, reduce round-trips to your server for data, and store robust information across browser sessions at the client.