Chapter 20. Persistence
20.0. Introduction
In the early years of web development, to keep a value around as users moved from page to page, you’d have to make use of session persistence functionality built into the environment, attach the data to the web page URL, or provide it in a hidden form field. Of course, this persisted the data from page to page, but once you closed the browser, the data was lost.
JavaScript added a third approach, a cookie, which could not only persist from page to page, but cause the data to persist beyond the current session. With cookies, you could not only access data such as login information throughout a website, but you could close the browser and reopen it another day and still have access to the same data.
These techniques still exist, but now, because of the needs of more complex web applications, including applications that support offline functionality, browsers support a variety of sophisticated data storage and persistence techniques.
No area of JavaScript has gone through more upheaval recently than persistence. Whether it’s storing data between sessions or enabling access to information while offline, complex new ideas have been spawned, supported by web pages and implementations, only to quietly fade away when something new and shiny appeared.
Probably one of the more famous examples of an idea that came and went is Google Gears—an offline storage mechanism that generated considerable excitement in 2007 when it debuted, only to die a fairly quick and unexpected death when Google announced in November 2009 that it was dropping support for Gears in favor of the new Web Applications/HTML5 persistence initiatives.
Fun new technology aside, the old approaches, such as using data encoding on an URL or a cookie, still exist and still provide a service that some of the newer techniques don’t provide: simplicity. This chapter touches on all of the techniques, from the old time to the new, the simple to the complex.
Note
Some of the methods explored in this chapter are cutting edge, based on specifications that are still being developed. Use with caution.
See Also
For more on Google’s Gears decision, see the blog post at http://gearsblog.blogspot.com/2010/02/hello-html5.html.
20.1. Attaching Persistent Information to URLs
Problem
You want to store a small fragment of information so that the information is available to anyone accessing the page.
Solution
Persisting a fragment of information in a web page for general access by everyone is dependent on the old school capability of attaching information to a URL. The information can be passed as a page fragment:
http://somecompany.com/firstpage.html#infoasfragment
Discussion
JavaScript can be used to add a page fragment, though this is more of a way to store a simple state than anything more complex:
http://somecompany.com/test.html#one
The data can also be encoded as parameters in a query string, which starts with a question mark, followed by the parameters passed as key/value pairs separated by equal signs, and separated from each other by ampersands:
http://somecompany.com?test=one&test=two
There are limits to lengths of URLs, and probably for numbers of GET pairs, but I would hope we wouldn’t ever approach these limits. This also isn’t an approach you would use in place of a cookie. For instance, if you want to capture user input into a form, in case the users have to leave before finishing the form entries (or they accidentally close their browser tab page), you should put this data into a cookie, which is more secure, and more specific to the person.
Data encoding in the URL is more of a way to capture page state so
that a person can send a link to the page in that state to another
person, or link it within a web page. Example 20-1 demonstrates how
something like data persistence via URL can work. In the web page, there
are three buttons and one div
element
controlled by the action of the buttons. One button moves the div
element to the right, one button increases
the size of the element, and one button changes its color. As each
button is clicked, the div
element is
adjusted, and the newly applied CSS value is stored with the appropriate
button.
When the state of the page is changed, a link within the page is
updated to reflect the state of the page. Note that the actual window.location
property and even the window.location.search
property is not changed. The reason is that the page reloads as a
security precaution when you update any component of window.location
except the hash
value.
Note
Allowing a person to change the URL in the location bar, which isn’t reflected in the actual page, introduces the threat of spoofing—one page masquerading as another in order to scam passwords and other confidential information.
Now you can reload the page and have the script restore the page to the state in the URL when it reloads—but that’s a lot of moving parts. In the example, we’ll just create a static state link, and leave it at that.
<!DOCTYPE html> <head> <title>Remember me?</title> <style> #square { position: absolute; left: 0; top: 100px; width: 100px; height: 100px; border: 1px solid #333; background-color: #ffff00; } div p { margin: 10px; } </style> <script> // found at http://www.netlobo.com/url_query_string_javascript.html function getQueryParam( name ) { name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); var regexS = "[\\?&]"+name+"=([^&#]*)"; var regex = new RegExp( regexS ); var results = regex.exec( window.location.href ); if( results == null ) return null; else return results[1]; } window.onload=function() { // set up button document.getElementById("move").onclick=moveSquare; document.getElementById("size").onclick=resizeSquare; document.getElementById("color").onclick=changeColor; var move = getQueryParam("move"); if (!move) return; var size = getQueryParam("size"); var color = getQueryParam("color"); // update element var square = document.getElementById("square"); square.style.left=move + "px"; square.style.height=size + "px"; square.style.width=size + "px"; square.style.backgroundColor="#" + color; // update data-state values document.getElementById("move").setAttribute("data-state",move); document.getElementById("size").setAttribute("data-state",size); document.getElementById("color").setAttribute("data-state",color); } function updateURL () { var move = document.getElementById("move").getAttribute("data-state"); var color = document.getElementById("color").getAttribute("data-state"); var size = document.getElementById("size").getAttribute("data-state"); var link = document.getElementById("link"); var path = location.protocol + "//" + location.hostname + location.pathname + "?move=" + move + "&size=" + size + "&color=" + color; link.innerHTML="<p><a href='" + path + "'>static state link</a></p>"; } function moveSquare() { var move = parseInt(document.getElementById("move").getAttribute("data- state")); move+=100; document.getElementById("square").style.left=move + "px"; document.getElementById("move").setAttribute("data-state", move); updateURL(); } function resizeSquare() { var size = parseInt(document.getElementById("size").getAttribute("data- state")); size+=50; var square = document.getElementById("square"); square.style.width=size + "px"; square.style.height=size + "px"; document.getElementById("size").setAttribute("data-state",size); updateURL(); } function changeColor() { var color = document.getElementById("color").getAttribute("data-state"); var hexcolor; if (color == "0000ff") { hexcolor="ffff00"; } else { hexcolor = "0000ff"; } document.getElementById("square").style.backgroundColor="#" + hexcolor; document.getElementById("color").setAttribute("data-state",hexcolor); updateURL(); } </script> </head> <body> <button id="move" data-state="0">Move Square</button> <button id="size" data-state="100">Increase Square Size</button> <button id="color" data-state="#ffff00">Change Color</button> <div id="link"></div> <div id="square"> <p>This is the object</p> </div> </body>
Figure 20-1 shows the web page after several changes to the square element, and after the page is reloaded using the link.
See Also
Recipes 8.7 and 8.8
demonstrate how to use the location.hash
property to store state in the
URL and return to that state when the page is reloaded. The query string
routine in the example is described at http://www.netlobo.com/url_query_string_javascript.html.
20.2. Creating a Cookie to Persist Information Across Pages
Problem
You want to persist some information about or for the user within the existing browser session.
Solution
If the amount of data is less than 4k in size, use a browser
cookie. A cookie is a patterned line of text that you assign to the
document.cookie
property:
document.cookie="cookiename=cookievalue; expires=date; path=path";
Discussion
Cookies are still one of the easiest, simplest, and most widely used persistent storage technique for web pages today. They’re easy to set, safe, well understood by most people who browse web pages, and require little overhead.
People can also turn cookie support off in their browsers, which means your application has to work, regardless. In addition, the amount of data stored is small—less than 4k—and doesn’t support complex data structures. There’s also the security restrictions associated with cookies: they’re domain-specific. Despite the restriction, though, cookies are also insecure, as any data stored is done so in plain text. You don’t want to use cookies to store passwords.
Note
Read the article “Improving Persistent Login Cookie Best Practice” if you’re interested in implementing “Remember Me” using cookies on your site.
Cookies are created by assigning the cookie to the document.cookie
property. The cookie consists of a name/value pair,
separated by an equal sign:
document.cookie="cookiename=cookievalue";
There are parameters that can follow the cookie/value pair, all
separated by semi-colons (;
). The
parameters are:
path=
path
(such as/
or/subdir
)domain=
domain
(such asburningbird.net
, for all subdomains, or a given subdomain,missourigreen.burningbird.net
)max-age=
maximum age in seconds
expires=
date in GMTString-format
secure
Here’s an example. This cookie keyword is Language
, with a value of JavaScript
, set to expire at a given time with
expires
, and with path
set to the top-level domain:
Language=JavaScript; expires=Mon, 22 Feb 2010 01:00:59 GMT; path=/
If we wanted the cookie to expire when the browser closes, all we
need do is leave off the expires
.
To retrieve the cookie, we have to retrieve the entire document.cookie
property, which returns all
cookies set on the domain. The application needs to find all of the
cookies, and then look for a specific one. To erase the cookie, all we
need do is set it with a past date.
To demonstrate, Example 20-2 contains a web page that has two input fields, one for the cookie name and one for the value. Clicking the Set Cookie button creates the cookie; clicking the Get Cookie retrieves the value for the given cookie; clicking the Erase Cookie button erases the cookie.
<!DOCTYPE html> <html dir="ltr" lang="en-US"> <head> <title>Persisting via Cookies</title> <style> div { margin: 5px; } </style> <script> // if cookie enabled window.onload=function() { if (navigator.cookieEnabled) { document.getElementById("set").onclick=setCookie; document.getElementById("get").onclick=readCookie; document.getElementById("erase").onclick=eraseCookie; } } // set cookie expiration date in year 2010 function setCookie() { var cookie = document.getElementById("cookie").value; var value = document.getElementById("value").value; var futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 10); var tmp=cookie + "=" + encodeURI(value) + "; expires=" + futureDate.toGMTString() + "; path=/"; document.cookie=tmp; } // each cookie separated by semicolon; function readCookie() { var key = document.getElementById("cookie").value; var cookie = document.cookie; var first = cookie.indexOf(key+"="); // cookie exists if (first >= 0) { var str = cookie.substring(first,cookie.length); var last = str.indexOf(";"); // if last cookie if (last < 0) last = str.length; // get cookie value str = str.substring(0,last).split("="); alert(decodeURI(str[1])); } else { alert("none found"); } } // set cookie date to the past to erase function eraseCookie () { var key = document.getElementById("cookie").value; var cookieDate = new Date(); cookieDate.setDate(cookieDate.getDate() - 10); document.cookie=key + "= ; expires="+cookieDate.toGMTString()+"; path=/"; } </script> </head> <body> <form> <label for="cookie"> Enter cookie:</label> <input type="text" id="cookie" /> <br /> <label for="value">Cookie Value:</label> <input type="text" id="value" /><br /> </form> <div> <button id="set">Set Cookie</button> <button id="get">Get Cookie</button> <button id="erase">Erase Cookie</button> </div> </body>
See Also
See Recipe 3.6 for how to access a future date.
20.3. Persisting Information Using the History.pushState Method and window.onpopevent
Problem
You’ve looked at all the ways of handling the back button and controlling page state for an Ajax application, and you’re saying to yourself, “There has to be a better way.”
Solution
There is a better way, a much better way...but it’s going to be
some time before you’ll be able to incorporate the technique into your
applications: using HTML5’s new history.pushState
and history.replaceState
methods to persist a state object, and the window.onpopevent
:
window.history.pushState({ page : page}, "Page " + page, "?page=" + page); ... window.onpopstate = function(event) { // check for event.state, if found, reload state if (!event.state) return; var page = event.state.page; }
Discussion
Addressing the significant problems Ajax developers have had with
trying to persist state through back button events or page reloads,
HTML5 has new history
object methods,
pushState
and replaceState
, to persist state information,
and then an associated window.onpopevent
that can be used to restore
the page state.
In the past, we had the ability to persist information regarding the page state, though we’ve had to be conservative in how much data we persist. A popular approach, and one demonstrated in Recipe 20.1, is to store the data in the page URL hash, which updates the page history and can be pulled via JavaScript.
The problem with this approach is that updating the hash may update the history. If you hit the back button, the URL with the hash shows in the location bar, but no event is triggered so you can grab the data and restore the page. The workaround was to use a timer to check for the new hash and then restore the page if a new hash was found. Not an attractive solution, and one most of us decided just wasn’t worth trying.
Now, you can easily store any object that can be passed to
JSON.stringify
. Since the data is
stored locally, the early implementor, Firefox, limits the size of the
JSON representation to 640k. However, unless you’re recording the state
of every pixel in the page, 640k should be more than sufficient.
To see how the new event and methods work, Example 20-3 is a recreation of
Example 8-2, from Recipe 8.8. The changes to the application
include the removal of the use of the hash location fragment, which is
replaced by history.pushState
, and
the window.onpopstate
event handler, both of
which are highlighted in the code. There’s one other minor change—in the
functionOne
function, also
highlighted—and I’ll get into the reason why after the example.
<!DOCTYPE html> <head> <title>Remember me--new, and improved!</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <script> window.onload=function() { document.getElementById("next").onclick=nextPanel; } window.onpopstate = function(event) { // check for event.state, if found, reload state if (!event.state) return; var page = event.state.page; switch (page) { case "one" : functionOne(); break; case "two" : functionOne(); functionTwo(); break; case "three" : functionOne(); functionTwo(); functionThree(); } } // display next panel, based on button's class function nextPanel() { var page = document.getElementById("next").getAttribute("data-page"); switch(page) { case "zero" : functionOne(); break; case "one" : functionTwo(); break; case "two" : functionThree(); } } // set both the button class, and create the state link, add to page function setPage(page) { document.getElementById("next").setAttribute("data-page",page); window.history.pushState({ page : page}, "Page " + page, "?page=" + page); } // function one, two, three - change div, set button and link function functionOne() { var square = document.getElementById("square"); square.style.position="relative"; square.style.left="0"; square.style.backgroundColor="#ff0000"; square.style.width="200px"; square.style.height="200px"; square.style.padding="10px"; square.style.margin="20px"; setPage("one"); } function functionTwo() { var square = document.getElementById("square"); square.style.backgroundColor="#ffff00"; square.style.position="absolute"; square.style.left="200px"; setPage("two"); } function functionThree() { var square = document.getElementById("square"); square.style.width="400px"; square.style.height="400px"; square.style.backgroundColor="#00ff00"; square.style.left="400px"; setPage("three"); } </script> </head> <body> <button id="next" data-page="zero">Next Action</button> <div id="square" class="zero"> <p>This is the object</p> </div> </body>
In this example, the state object that is stored is extremely
simple: a page property and its associated value. The history.pushState
also takes a title
parameter, which is used for the session
history entry, and a URL. For the example, I appended a query string
representing the page. What is displayed in the location bar is:
http://somecom.com/pushstate.html?page=three
The history.replaceState
method
takes the same parameters, but modifies the current history entry
instead of creating a new one.
When using the browser back button to traverse through the created
history entries, or when hitting the page reload, a window.onpopstate
event is fired. This is
really the truly important component in this new functionality, and is
the event we’ve needed for years. To restore the web page to the stored
state, we create a window.onpopstate
event handler function, accessing the state object from the event passed
to the window handler function:
window.onpopstate = function(event) { // check for event.state, if found, reload state if (!event.state) return; var page = event.state.page; ... }
In the example, when you click the button three times to get to
the third “page,” reload the page, or hit the back button, the window.onpopstate
event handlers fires.
Perfect timing to get the state data, and repair the page. Works
beautifully, too. In the Firefox Minefield edition, that is.
One other change that had to be made to the older example, is that
functionOne
had to be modified and
the following style settings added:
square.style.position = "relative"; square.style.left = "0";
The reason is that unlike Example 8-2, which goes
through a complete page reload, the new state methods and event handler
actually do preserve the state in-page. This means
the changes going from step one to step two (setting position to
absolute
and moving the div
element) have to be canceled out in the
first function in order to truly restore the page state. It’s a small
price to pay for this lovely new functionality.
Again, the example only worked with the Firefox nightly. However, the back button did seem to work with the WebKit nightly.
20.4. Using sessionStorage for Client-Side Storage
Problem
You want to easily store session information without running into the size and cross-page contamination problems associated with cookies, and prevent loss of information if the browser is refreshed.
Solution
Use the new DOM Storage sessionStorage
functionality:
sessionStorage.setItem("name", "Shelley"); sessionStorage.city="St. Louis"; ... var name = sessionStorage,getItem("name"); var city = sessionStorage.city; ... sessionStorage.removeItem("name"); sessionStorage.clear();
Discussion
One of the constraints with cookies is they are domain/subdomain-specific, not page-specific. Most of the time, this isn’t a problem. However, there are times when such domain specificity isn’t sufficient.
For instance, a person has two browser tabs open to the same shopping site and adds a few items to the shopping cart in one tab. In the tab, the shopper clicks a button to add an item because of the admonishment to add the item to the cart in order to see the price. The shopper decides against the item and closes the tab page, thinking that action is enough to ensure the item isn’t in the cart. The shopper then clicks the check-out option in the other opened tag, assuming that the only items currently in the cart are the ones that were added in that browser page.
If the users aren’t paying attention, they may not notice that the cookie-based shopping cart has been updated from both pages, and they’ll end up buying something they didn’t want.
While it is true that many web users are savvy enough to not make
this mistake, there are many who aren’t; they assume that persistence is
browser-page-specific, not necessarily domain-specific. With sessionStorage
(to paraphrase the famous quote
about Las Vegas), what happens in the page, stays in the page.
As an example of the differences between the cookies and the new
storage option, Example 20-4
stores information from a form in both a cookie and sessionStorage
. Clicking the button to get the
data gets whatever is stored for the key in both, and displays it in the
page: the sessionStorage
data in the
first block, the cookie data in the second. The remove button erases
whatever exists in both.
<!DOCTYPE html> <html dir="ltr" lang="en-US"> <head> <title>Comparing Cookies and sessionStorage</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" > <style> div { background-color: #ffff00; margin: 5px; width: 100px; padding: 1px; } </style> <script> window.onload=function() { document.getElementById("set").onclick=setData; document.getElementById("get").onclick=getData; document.getElementById("erase").onclick=removeData; } // set data for both session and cookie function setData() { var key = document.getElementById("key").value; var value = document.getElementById("value").value; // set sessionStorage var current = sessionStorage.getItem(key); if (current) { current+=value; } else { current=value; } sessionStorage.setItem(key,current); // set cookie current = getCookie(key); if (current) { current+=value; } else { current=value; } setCookie(key,current); } function getData() { try { var key = document.getElementById("key").value; // sessionStorage var value = sessionStorage.getItem(key); if (!value) value =""; document.getElementById("sessionstr").innerHTML="<p>" + value + "</p>"; // cookie value = getCookie(key); if (!value) value=""; document.getElementById("cookiestr").innerHTML="<p>" + value + "</p>"; } catch(e) { alert(e); } } function removeData() { var key = document.getElementById("key").value; // sessionStorage sessionStorage.removeItem(key); // cookie eraseCookie(key); } // set session cookie function setCookie(cookie,value) { var tmp=cookie + "=" + encodeURI(value) + ";path=/"; document.cookie=tmp; } // each cookie separated by semicolon; function getCookie(key) { var cookie = document.cookie; var first = cookie.indexOf(key+"="); // cookie exists if (first >= 0) { var str = cookie.substring(first,cookie.length); var last = str.indexOf(";"); // if last cookie if (last < 0) last = str.length; // get cookie value str = str.substring(0,last).split("="); return decodeURI(str[1]); } else { return null; } } // set cookie date to the past to erase function eraseCookie (key) { var cookieDate = new Date(); cookieDate.setDate(cookieDate.getDate() - 10); var tmp=key + "= ; expires="+cookieDate.toGMTString()+"; path=/"; document.cookie=tmp; } </script> </head> <body> <form> <label for="key"> Enter key:</label> <input type="text" id="key" /> <br /> <br /> <label for="value">Enter value:</label> <input type="text" id="value" /><br /><br /> </form> <button id="set">Set data</button> <button id="get">Get data</button> <button id="erase">Erase data</button> <div id="sessionstr"><p></p></div> <div id="cookiestr"><p></p></div> </body>
Load the example page (it’s in the book examples) in Firefox 3.5 and up. Add one or more items to the same key value, and then click the button labeled “Get data”, as shown in Figure 20-2.
Now, open the same page in a new tab window, and click the “Get data” button. The activity results in a page like that shown in Figure 20-3.
In the new tab window, the cookie
value persists because the cookie
is session-specific, which means it
lasts until you close the browser. The cookie lives beyond the first tab
window, but the sessionStorage
, which
is specific to the tab window, does not.
Now, in the new tab window, add a couple more items to the key value, and click “Get data” again, as shown in Figure 20-4.
Return to the original tab window, and click “Get data”. As you
can see in Figure 20-5,
the items added in the second tab are showing with the cookie, but not
the sessionStorage
item.
Lastly, in the original tab window, click the “Erase data” button.
Figure 20-6 shows the
results of clicking “Get data” on the original window, while Figure 20-7 shows the results
when clicking “Get data” in the second tab window. Again, note the
disparities between the cookie and sessionStorage
.
The reason for all of these images is to demonstrate the
significant differences between sessionStorage
and cookies
, aside from how they’re set and
accessed in JavaScript. Hopefully, the images and the example also
demonstrate the potential hazards involved when using sessionStorage
, especially in circumstances
where cookies have normally been used.
If your website or application users are familiar with the
cookie
persistence across tabbed
windows, sessionStorage
can be an
unpleasant surprise. Along with the different behavior, there’s also the
fact that browser menu options to delete cookies probably won’t have an
impact on sessionStorage
, which could
also be an unwelcome surprise for your users. Use sessionStorage
with caution.
The sessionStorage
object is
currently supported in Firefox 3.5 and up, Safari 4.x and up, and IE 8.
There are some implementation differences for sessionStorage
, but the example shown in this
recipe is consistently implemented across all environments.
One last note on sessionStorage
, as it relates to its
implementation. Both sessionStorage
and localStorage
, covered in the next
recipe, are part of the new DOM Storage specification, currently under
development by the W3C. Both are window
object properties, which means they can
be accessed globally. Both are implementations of the Storage
object, and changes to the prototype
for Storage
result in changes to both the sessionStorage
and localStorage
objects:
Storage.prototype.someMethod = function (param) { ...}; ... localStorage.someMethod(param); ... sessionStorage.someMethod(param);
Aside from the differences, covered in this recipe and the next,
another major difference is that the Storage
objects don’t make a round trip to the
server—they’re purely client-side
storage techniques.
See Also
For more information on the Storage
object, sessionStorage
, localStorage
, or the Storage DOM, consult the
specification.
See Recipe 20.5 for a
different look at how sessionStorage
and localStorage
can be set and
retrieved, and other supported properties on both.
20.5. Creating a localStorage Client-Side Data Storage Item
Problem
You want to shadow form element entries (or any data) in such a way that if the browser crashes, the user accidentally closes the browser, or the Internet connection is lost, the user can continue.
Solution
You could use cookies if the data is small enough, but that
strategy doesn’t work in an offline situation. Another, better approach,
especially when you’re persisting larger amounts of data or if you have
to support functionality when no Internet connection is present, is to
use the new localStorage
:
var value = document.getElementById("formelem").value; If (value) { localStorage.formelem = value; } ... // recover var value = localStorage.formelem; document.getElementById("formelem").value = value;
Discussion
Recipe 20.4
covered sessionStorage
, one of the
new DOM Storage techniques. The localStorage
object interface is the same,
with the same approaches to setting the data:
// use item methods sessionStorage.setItem("key","value"); localStorage.setItem("key","value"); // use property names directly sessionStorage.keyName = "value: localStorage.keyName = "value"; // use the key method sessionStorage.key(0) = "value"; localStorage.key(0) = "value:
and for getting the data:
// use item methods value = sessionStorage.getItem("key"); value = localStorage.getItem("key"); // use property names directly value = sessionStorage.keyName: value = localStorage.keyName; // use the key method value = sessionStorage.key(0); value = localStorage.key(0):
Both also support the length
property, which provides a count of stored item pairs, and the clear
method (no parameters), which clears out
all Storage
(but Firefox only
supports clearing storage for localStorage
). In addition, both are scoped to
the HTML5 origin, which means that the data storage is shared across all
pages in a domain, but not across protocols (e.g., http
is not the same as https
) or ports.
The difference between the two is how long data is stored. The
sessionStorage
object only stores
data for the session, but the localStorage
object stores data on the client
forever, or until specifically removed.
The sessionStorage
and localStorage
objects also support one event:
the storage
event. This is an
interesting event, in that it fires on all pages when changes are made
to a localStorage
item. It is also an
area of low-compatibility among browsers: you can capture the event on
the body
or document
elements for Firefox, on the body
for IE, or on the document
for Safari.
Example 20-5
demonstrates a more comprehensive implementation than the use case
covered in the solution for this recipe. In the example, all elements of
a small form have their onchange
event handler method assigned to a function that captures the change
element name and value, and stores the values in the local storage via
localStorage
. When the form is
submitted, all of the stored form data is cleared.
When the page is loaded, the form elements onchange
event handler is assigned to the
function to store the values, and if the value is already stored, it is
restored to the form element. To test the application, enter data into a
couple of the form fields—but, before clicking the submit button,
refresh the page. Without the use of localStorage
, you’d lose the data. Now, when
you reload the page, the form is restored to the state it was in before
the page was reloaded.
<!DOCTYPE html> <html dir="ltr" lang="en-US"> <head> <title>localstore</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" > <style> </style> <script> window.onload=function() { try { var elems = document.getElementsByTagName("input"); // capture submit to clear storage document.getElementById("inputform").onsubmit=clearStored; for (var i = 0; i < elems.length; i++) { if (elems[i].type == "text") { // restore var value = localStorage.getItem(elems[i].id); if (value) elems[i].value = value; // change event elems[i].onchange=processField; } } catch (e) { alert(e); } } // store field values function processField() { localStorage.setItem(window.location.href,"true"); localStorage.setItem(this.id, this.value); } // clear individual fields function clearStored() { var elems = document.getElementsByTagName("input"); for (var i = 0; i < elems.length; i++) { if (elems[i].type == "text") { localStorage.removeItem(elems[i].id); } } } </script> </head> <body> <form id="inputform"> <label for="field1">Enter field1:</label> <input type="text" id="field1" /> <br /> <br /> <label for="field2">Enter field2:</label> <input type="text" id="field2" /><br /><br /> <label for="field3">Enter field1:</label> <input type="text" id="field3" /> <br /> <br /> <label for="field4">Enter field1:</label> <input type="text" id="field4" /> <br /> <br /> <input type="submit" value="Save" /> </body>
The size alloted for localStorage
varies by browser, and some
browsers, such as Firefox, allow users to extend the Storage
object limits.
The localStorage
object can be
used for offline work. For the form example, you can store the data in
the localStorage
and provide a button
to click when connected to the Internet, in order to sync the data from
localStorage
to server-side
storage.
See Also
See Recipe 20.4
for more on the Storage
object, and
on sessionStorage
and
local
Storage
.
20.6. Persisting Data Using a Relational Data Store
Problem
You want a more sophisticated data store on the client than what’s
provided with other persistent storage methods, such as localStorage
. You’d also like to use your mad SQL skills.
Solution
You can use SQL in client applications, with some significant limitations. There is a W3C Web SQL Database Working Draft for using a relational database such as SQLite on the client, but support for the specification is provided only in WebKit browsers (Safari and Chrome) and the latest Opera (10.5):
You can create a database:
var db = openDatabase("dbname","1.0", "Bird Database", 1024 * 1024);
and you can create tables, within a transaction:
db.transaction(function(tx)) { tx.executeSQL('CREATE TABLE birdTable(birdid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, birdname VARCHAR(20) NOT NULL)');
Then query on the tables:
db.transation(function(tx)) { tx.executeSQL('SELECT * birdTable', [], sqlFunction, sqlError); var sqlFunction = function(tx,recs) { var rows = recs.rows; for (var i = 0; i < rows.length; i++) { alert(recs.rows.item(i).text); }
Discussion
I hesitated to cover the Web SQL Database specification because the implementation is limited, and the specification is currently blocked at the W3C. Only WebKit (and Chrome and Safari) and Opera have made any progress with this implementation, and there’s no guarantee that Mozilla or Microsoft will pick up on it, especially since the specification is blocked.
It is an interesting concept, but it has significant problems. One is security, naturally.
In our current applications, the client part of the applications handles one form of security, and the server component handles the other, including database security and protection against SQL injection: attaching text on to a data field value that actually triggers a SQL command—such as drop all tables, or expose private data. Now, with client-side relational database support, we’re introducing a new set of security concerns on the client.
Another concern is the increasing burden we put on client-side storage. Each new innovation exposes new vulnerabilities, increases the size and complexity of our browsers, and embeds all sorts of data on our machines. Yet the vast majority of JavaScript applications don’t need to create their own version of Gmail, or a Photoshop-like paint program.
Then there’s the separation of a skill set. While it’s true that in small-to-medium shops, the same people who develop the frontend application probably participate in the server-side development, it’s not unusual in many shops to have the tasks handled by different groups. So people who may not be terribly experienced at relational database management might be tossing around SQL transactions (and believe me, relational database application development is a dedicated skill).
From an implementation point of view, only SQLite is currently supported. One difference in implementations, if you have worked with server-side SQL applications, is that client-side SQL is asynchronous by default. This means your application is not blocked until the transaction completes, which provides its own challenge.
However, there’s nothing wrong with playing around with something new. If you’re interested, pick up a browser that supports the functionality and give it a shot. However, I would avoid production use until we’re sure this technology is going to have a life beyond 2010.
See Also
The Web SQL Database specification can be found at http://dev.w3.org/html5/webdatabase/.
Get JavaScript Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.