Tuesday, January 4, 2011

HTML5 Local Storage: Web Storage

Cookie Monster is not the only one looking past cookies. The Web Storage specification aims to define "an API for persistent data storage of key-value pair data in Web clients." Like the Geolocation API specification I discussed in a previous blog post, this Web Storage specification is separate and distinct from the HTML5 specification (though Web Storage was once part of HTML5). As do many, I lump it in under the "greater" meaning of HTML5 (a bunch of cool new web stuff) rather than strictly adhering to whether it's covered in the HTML5 specification or not. Throughout this post, I may refer to this standardized approach for storing data on the web client as either HTML5 Storage or HTML5 Local Storage, but its official name is Web Storage and it is often referred to as DOM Storage as well.

As stated already, Web Storage (AKA "HTML5 Storage") is designed to store name/value (it calls them key/value) pairs on the web client. The closest analogy may be cookies, but Web Storage (AKA "HTML5 Local Storage") offers numerous advantages over cookies. In this blog post, I look at using Web Storage in Chrome 8, Safari 5, Internet Explorer 8, and Firefox 3.6. Amazingly, even shockingly, the same code used in my example works across all four browsers!

Here is the HTML/JavaScript code listing for my example (it's all encapsulated in a single file called Storage.html).

Storage.html
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>HTML5/Web/DOM Storage Demonstrated</title>
  <script language="javascript">
  /*
   * Indicate if this browser supports local storage.
   */
  function html5StorageSupported()
  {
     return ('localStorage' in window) && window['localStorage'] !== null;
  }
  /*
   * Provide number of elements in storage.
   */
  function determineNumberStoredElements()
  {
     return html5StorageSupported() ? localStorage.length : "Not Applicable";
  }
  /*
   * Provide indication on web page of whether this browser supports local storage.
   */
  function initialize()
  {
     document.form1.supported.value = html5StorageSupported() ? "Yes!" : "No (:";
     document.form1.numStored.value = determineNumberStoredElements();
  }
  /*
   * Save favorite movies to local storage.
   */
  function persistFavoriteMovies()
  {
     if (html5StorageSupported())
     {
        for (var i = 1; i <= 10; i++)
        {
           var movie = document.form1["movie" + i].value;
           var storageIndex = "rmoug.td2011.movie." + i;
           //alert("Movie #" + i + " [" + storageIndex+ "]: " + movie);
           localStorage[storageIndex] = movie;
        }
        document.form1.numStored.value = determineNumberStoredElements();
     }
     else
     {
        alert("Cannot save to local storage because it's not supported.");
     }
  }
  /*
   * Load favorite movies from local storage.
   */
  function loadFavoriteMovies()
  {
     if (html5StorageSupported())
     {
        for (var i = 1; i <= 10; i++)
        {
           document.form1["movie" + i].value = localStorage["rmoug.td2011.movie." + i];
        }
     }
  }
  /*
   * Clear favorite movies from both local storage and from form fields.
   */
  function clearFavoriteMovies()
  {
     if (html5StorageSupported())
     {
        // Clear favorite movies from local storage
        localStorage.clear();

        // Clear fields of movies content
        for (var i = 1; i <= 10; i++)
        {
           document.form1["movie" + i].value = null;
        }
        document.form1.numStored.value = determineNumberStoredElements();
     }
  }
  </script>
</head>

<body onload="initialize()">

  <h1>HTML5/Web/DOM Storage</h1>

  <form name="form1">
     <table>
        <tr>
           <td>Does this browser support local storage?</td>
           <td><input type="text" name="supported"></td>
        </tr>
        <tr>
           <td>Number of Stored Elements</td>
           <td><input type="text" name="numStored"></td>
        </tr>
     </table>
     <table>
        <tr>
           <td colspan="2" style="font-weight: bold;" align="center">Favorite Movies</td>
        </tr>
        <tr>
           <td>#1</td>
           <td><input type="text" name="movie1"></td>
        </tr>
        <tr>
           <td>#2</td>
           <td><input type="text" name="movie2"></td>
        </tr>
        <tr>
           <td>#3</td>
           <td><input type="text" name="movie3"></td>
        </tr>
        <tr>
           <td>#4</td>
           <td><input type="text" name="movie4"></td>
        </tr>
        <tr>
           <td>#5</td>
           <td><input type="text" name="movie5"></td>
        </tr>
        <tr>
           <td>#6</td>
           <td><input type="text" name="movie6"></td>
        </tr>
        <tr>
           <td>#7</td>
           <td><input type="text" name="movie7"></td>
        </tr>
        <tr>
           <td>#8</td>
           <td><input type="text" name="movie8"></td>
        </tr>
        <tr>
           <td>#9</td>
           <td><input type="text" name="movie9"></td>
        </tr>
        <tr>
           <td>#10</td>
           <td><input type="text" name="movie10"></td>
        </tr>
        <tr>
           <td colspan="2">
              <input type="button" value="Load" onclick="loadFavoriteMovies()">
              <input type="button" value="Save" onclick="persistFavoriteMovies()">
              <input type="button" value="Clear" onclick="clearFavoriteMovies()">
           </td>
        </tr>
     </table>
  </form>

</body>
</html>

The code listing above is fairly straightforward, but it does demonstrate use of localStorage.getItem(key) (via array syntax in JavaScript function loadFavoriteMovies()), localStorage.setItem(key, value) (via array syntax in the JavaScript function persistFavoriteMovies()), localStorage.clear(), and localStorage.length. It also demonstrates how to check whether the browser supports this localStorage attribute.

For the WebKit-based browsers (Safari and Chrome), I can run this test locally using the file URI scheme and it works properly. For Internet Explorer and Firefox, I was only able to get the above example to work when the Storage.html page was accessed on a deployed server. It doesn't really matter which server, but I used GlassFish. Bugzilla@Mozilla's Bug 507361 ("localStorage doesn't work in file:/// documents") documents this issue for Firefox and it also appears to be a known issue for Internet Explorer (including version 9).

The example in the code above leads to a pretty simple page. The user can click the "Save" button to persist any typed-in movie titles to local persistence. This means that the data is persisted even if the user leaves the page or even closes the browser. Upon returning to the page, the user can click the "Load" button and the previously saved values will be available again. The "Clear" button clears the local persistence. During all of this, two fields at the top indicate whether the browser supports localStorage and the number of elements currently stored in local storage.

Static screen snapshots will always lack when compared to seeing dynamic behavior in action, but I attempt to demonstrate this flow through a series of screen snapshots here with the four browsers previously mentioned. The first set of screen snapshots show how the page appears when first presented in Chrome.

Chrome Initial Page Load


When the movie names are typed in and the "Save" button is clicked, the page appears in Chrome as shown next.

Chrome Data Saved to Local Storage

The number of elements in storage is updated to reflect that the ten movie titles have been persisted. When the browser is closed and then reopened, the movie titles are gone, but the number of elements in storage (10) indicates that they are still persisted.

Chrome Closed and Reopened

The user can now click on the "Load" button to have the movie titles reloaded into the fields.

Chrome: Data Reloaded

I have not demonstrated the "Clear" button. It simply clears the fields and clears the entries from the client storage, setting the number of stored elements back to zero. The above screen snapshots were for Chrome. In a very satisfying surprise, the other three browsers I'm covering in this post all behave remarkably consistently. They are shown next with the screen snapshots grouped by browser rather than by step in the demonstration.

Safari 5






Firefox 3.6






Internet Explorer 8







Disabling Web Storage

Local storage provided by standardized implementations of Web Storage provide tremendous potential benefit to users. However, as we have learned with cookies, JavaScript, and the Flash Player, not all users want these things turned on. In this section, I briefly summarize how to disable Web Storage in the browsers covered in this post.

Web Storage capability is disabled in Internet Explorer via "Tools -> Internet Options" ("Advanced Tab" option "Enable DOM Storage" in "Security" section) as shown in the next screen snapshot:


When that box for "Enable DOM Storage" is unchecked (it is checked/enabled by default), the example I have used in this post no longer behaves the same. Instead, it reports that the storage feature is not supported and that the count of stored elements is not applicable. It is shown in the next image.


It is worth noting here that explicitly disabling Internet Explorer's "Enable DOM Storage" support leads to the page reporting that the feature is not supported. I earlier mentioned that Internet Explorer local storage only worked properly when the page was hosted on a web server and accessed via HTTP rather than file://. However, in that case, there was no obvious error. In fact, the page did report that local storage was supported, but the functionality simply did not work. The page would act like it had saved the titles and even updated the number of elements persisted to "10," but then attempting to load them again after closing and opening the browser would fail. This same misbehavior was present in Firefox when the page was accessed via file://.

The dom.storage.enabled preference configuration controls support for DOM Storage in Firefox. This is changed in the same way that the Firefox geo.enabled preference is changed. To view or change this, type about:config in the field in which URLs are entered, agree to be careful when prompted, and then scroll down to the dom.storage preferences. DOM Storage in Firefox 3.6 is turned on by default as shown in the next screen snapshot. A user can right-click on that preference and choose "Toggle" to turn it to false (disabled).


When this preference is disabled in Firefox, the page shows the same error and "Not Applicable" warning as shown in the Internet Explorer snapshot when it's DOM Storage was disabled.


A similar approach as used to disable Chrome's geolocation support can be used to disable Chrome's DOM Storage support. In particular, the Chrome user can select the "Under the hood" tab from "Options" and click on the "Content settings..." button. Instead of selecting "Location" as is done for disabling geolocation support, "Cookies" should be selected and "Block sites from setting any data" bullet should be set. This appears to disable cookies as well as local storage. In fact, it gave me fits while trying to write this blog and test that out at the same time!

In Chrome's case, it doesn't report that local storage is not supported when the setting of data is disabled. Instead, it simply doesn't store the data no matter how many times I click on the "Save" button. This behavior looks and feels like the Internet Explorer and Firefox behavior when accessing the file via "file://".

A really easy way to disable local storage in Safari is to place it in "Private Browsing" mode. This is demonstrated in the next screen snapshot.


As with Chrome when it is instructed to block sites from setting data, Safari's Private Browsing mode does not lead to the example code knowing that local storage is not supported, but data in the fields is not saved.


Conclusion

Web Storage brings a standardized approach to storing data on the client side. This is another feature of modern web browsers that is bringing the web browser experience closer to the desktop application experience with the ability to store data exclusively on the client side. The standardization of the Web Storage specification can already be enjoyed in the major web browsers as shown with four browsers in this post. The Web Storage API is easy to apply and the consistency across browsers makes it even easier to use.

9 comments:

Team Member said...

Can you tell us how to dump the entire contents of the DB into one text field instead of each back into its own field? Preferably as key/value pairs separated by a colon, each on its own line.

Thanks,
Steve Husting

Dustin said...

Steve,

I'm not aware of any approach for doing this easily in a standard way because the Web Storage Specification clearly is intended to be used as name-value pairs and its Storage interface only supports methods on a per-property (name/value pair) basis. There may be less standard APIs supported in certain browsers, but the safer bet would be to write a bit of JavaScript to iterate over all entries and build up the string in the format you desire. The Storage interface provides a 'length' attribute that tells you the number of pairs and a 'key' method that returns the 'name' of each pair based on its numeric index that can be used with the 'getItem' method to get the corresponding value.

Dustin

Team Member said...

Thanks! I found a solution to this email issue.

Another question: Which part of the code is designating the actual storage name? That is, if I want to specify a different web storage space, what do I change?

Regards,
Steve Husting

Dustin said...

Steve,

As far as I know, you cannot name the local storage. The specification states, "Each top-level browsing context has a unique set of session storage areas, one for each origin." I believe that this means the only way you could differentiate local storage instances is by different origin schemes (different URLs).

Dustin

Team Member said...

Thanks for the time-saving reply!

Team Member said...

I'd like to give the user the ability to selective erase the content, and not erase the entire database. How do I code it to erase only the fields in form2 and not form1?

Thanks!

Dustin said...

The first thing that comes to mind is to use localstorage.removeItem(key) to selectively remove storage entries. One trick I have seen is to use a naming convention and then remove only items with keys/names matching that naming convention. For example, if your forms were named form1 and form2 and you had a city value in each form, you might name your localstorage keys form1.city and form2.city. Then you could loop through your localstorage instance and call removeItem(key) on any with keys beginning with "form1."

Dustin

Radu said...

Is there any possibility to disable DOM Storage in Opera browser?

Dustin said...

Radu,

There is a forum thread that talks about setting "Domain Quota For localStorage and Global Quota For localStorage both to 0." However, I seem to disable it in Opera 11.62 through the use of Preferences > Storage > Advanced and changing the setting of "Use application cache" to something other than "Yes."