Friday, April 8, 2016

Web Storage API

The Web Storage API (localStorage,sessionStorage)

client side storage

INTRODUCTION

The Web storage API (see the related W3C specification) introduces "two related mechanisms, similar to HTTP session cookies, for storing structured data on the client side".
Indeed, Web Storage provides two interfaces called sessionStorage andlocalStorage, whose main difference is data longevity. This specification defines an API for persistent data storage of key-value pair data in Web clients.
With localStorage the data will remain until it is deleted, whereas with sessionStoragethe data is erased when the tab/browser is closed.
For convenience, we will mainly illustrate the localStorage object. Just change "local" by "session" and it should work (this time with a session lifetime).key value pairs

SIMPLE KEY-VALUE STORES, ONE PER DOMAIN (FOLLOWING THE SAME ORIGIN POLICY)!

localStorage is a simple key-value store, in which the keys and values are strings. There is only one store per domain. This functionality is exposed through the globally available localStorage object. The same applies to sessionStorage.
Example:
  1. // Using localStorage
  2. // store data
  3. localStorage.lastName = "Bunny";
  4. localStorage.firstName = "Bugs";
  5. localStorage.location = "Earth";
  6. // retrieve data
  7. var lastName = localStorage.lastName;
  8. var firstName = localStorage.firstName;
  9. var location = localStorage.location;

This data is located in a store attached to the origin of the page. We created a JS Bin example in which we included the above code.
Once opened in your browser, the JavaScript code is executed. With the browser dev. tools, we can check what has been stored in the localStorage for this domain:
example of localStorage
dev tools can be used to show what is in the local storage

DIFFERENCES WITH COOKIES?

Cookies are also a popular way to store key-value pairs. Web Storage however, is a more powerful technique than cookies. The latter are limited in size (a few KBytes for cookies (compared to several MBytes for Web Storage) and they generate HTTP traffic for each additional request (whether to request a Web page, an image, a stylesheet, a JavaScript file, etc.).
Objects managed by Web Storage are no longer carried on the network and HTTP, and are easily accessible (read, change and delete) from JavaScript, using the Web Storage API.

Example 1: a form that never loses what you entered, even if you reload the page, or press "backspace" by mistake

form screenshot, a form that saves/restores its content as we type
You can start filling this form and come back another day and complete it. It doesn't matter if you closed your browser before coming back.
In this example, we use the most simple way to uselocalStorage:
    • Save with the localStorage.key = value syntax. For example localStorage.firstName = 'Michel' will save the value "Michel" with the access key being 'firstName'
    • Restore with the var value = localStorage.key syntax. For example var fn = localStorage.firstName; will set fn with the value 'Michel' if this value has been previously saved as in the example from the line above.

SAVING THE FORM CONTENT ON THE FLY

Open this online example at JS Bin, and use F12 or cmd-alt-i (Mac OS) to look at the dev. tools. As you type in the different input fields, their content is updated in the localStorage.
We just added input event listeners to each input field. For example, in order to save the first name input field's content, we just added:
  1. oninput="localStorage.firstName=this.value;"
Where firstName in red is the key and this.value the current value of the input field.
Form that saves as we type.
We added the same way an input listener to all the input fields in this example's form.

RESTORING THE FORM CONTENT ON PAGE LOAD/RELOAD

This time, we want the form content to be restored on page load/reload. We will add arestoreFormContent() function in the JavaScript code that will be called each time the page is loaded. In this function, we will read the saved data and set the input fields' values.
Complete example on JS Bin: enter data and press reload at any time. The form content is restored!
Source code extract (only addition to the previous example):
  1. // Called when the page is loaded
  2. window.onload = restoreFormContent;
  3.  
  4. function restoreFormContent() {
  5.    console.log("restoring form content from localStorage");
  6.    if(localStorage.firstName !== undefined)
  7.      document.getElementById("firstName").value = localStorage.firstName;
  8.    if(localStorage.lastName !== undefined)
  9.      document.getElementById("lastName").value = localStorage.lastName;
  10.    if(localStorage.email !== undefined)
  11.      document.getElementById("email").value = localStorage.email;
  12.  
  13.    if(localStorage.age !== undefined)
  14.      document.getElementById("age").value = localStorage.age;
  15.    if(localStorage.date !== undefined)
  16.      document.getElementById("date").value = localStorage.date;
  17. }
The tests at lines 7, 10, 13, etc., verify that a data has been saved, before trying to restore it. Without these tests, it would put the "undefined" string as the value of input fields with no corresponding data to restore.

More methods from localStorage/sessionStorage

funny picture of a big storeage houseThis time we will look at another example that uses new methods from the API:
    • localStorage.setItem(...),
    • localStorage.getItem(...),
    • localStorage.removeItem(...),
    • localStorage.clear().

GETTING/SETTING VALUES USING THE GETITEM(KEY) AND SETITEM(KEY, VALUE)METHODS

If you want to keep a simple counter of the number of times a given user has loaded your application, you can use the following code (just to show how to use setItem/removeItem methods):
  1. var counter = localStorage.getItem("count") || 0;
  2. counter++;
  3. localStorage.setItem("count", counter);
As you can easily guess from the above, we use var value = getItem(key) to retrieve a key's value andsetItem(key, value) to set it. This is similar to what we saw in the examples of the page above, except that this time:
    • The key can contain spaces, for example we can write: localStorage.setItem("Instructor's name", "Michel"); and var name =  localStorage.getItem("Instructor's name");, whilevar name = localStorage.Instructor's name; will not work!
    • In a loop or in an iterator, sometimes we need to set/get localStorage values using this syntax, for example: 
  1. var inputField = document.getElementById("firstName");
  2. saveInputFieldValue(inputField);
  3. ... 
  4. function saveInputFieldValue(field) {
  5.     localStorage.setItem(field.id, field.value);
  6. }

DELETING A KEY WITH REMOVEITEM(KEY), OR ALL KEYS WITH CLEAR()

Deleting a key can be performed through removeItem(). And if you wish to reset the entire store, simply calllocalStorage.clear().
Note that it may be quite rare that you will want the entire store to be cleared by the user in production software (since that effectively deletes their entire data). However, it is a rather a common operation needed during development, since bugs may store faulty data the persistence of which can break your application, since the way you store data may evolve over time, or simply because you also need to test the experience of the user when first using the application.
One way of handling that is to add a user interface button that calls clear() when clicked, but you then you need not to forget to remove it when you ship! The recommended approach  to use (whenever possible) is to simply open the dev. tool's console and type localStorage.clear() there — it's safer and works just as well.

ITERATING LOCAL STORES

Local stores (localStorage or sessionStorage) can also be iterated through in order to list all the content that they contain. The order is not guaranteed, but this may be useful at times (if only for debugging purposes!). The following code lists everything in the current store:
  1. for (var i = 0, n = localStorage.length; i < n; i++) {
  2.     var k = localStorage.key(i);
  3.     console.log(+ ": " + localStorage[k]); // get the ith value, the one with a key that is in the variable k.
  4. }
Smart students will note something off in the example above: instead of calling localStorage.getItem(k),we simply access localStorage[k]. Why? Because keys in the local store can also be accessed as if the store were a simple JavaScript object. So instead of localStorage.getItem("foo") andlocalStorage.setItem("foo", "bar"), one can write localStorage.foo and localStorage.foo = "bar". Of course there are limitations to this mapping: any string can serve as a key, so thatlocalStorage.getItem("one two three") works, whereas that string would not be a valid identifier after the dot (but it could still work as localStorage["one two three"]).

example with buttons that shown how to iterate on localStorage, clear it etc.EXAMPLE THAT SHOWS ALL THE METHODS OF THE LOCAL STORAGE API IN ACTION

Online example at JS Bin, run it, then click on the first button to show all key/values in the localStorage. Open the URL in another tab, and see that the data is shared between tabs, as local stores are attached to an origin.
Then click on the second button to add some data in the store, click on the third to remove some data. Finally, the last one clears the whole data store.
Source code:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset=utf-8 />
  5. <title>Example of localStorare API use</title>
  6. <script>
  7.    // Using localStorage
  8.    var counter = localStorage.getItem("count") || 0;
  9.    counter++;
  10.    localStorage.setItem("count", counter);
  11.    function getCountValue() {
  12.       // retrieve data
  13.       document.querySelector("#counter").innerHTML = localStorage.count;
  14.    }
  15.    function seeAllKeyValuePairsStored() {
  16.       // clear list first
  17.       document.querySelector('#list').innerHTML="";
  18.       for (var i = 0, n = localStorage.length; i < n; i++) {
  19.          var key = localStorage.key(i);
  20.          var value = localStorage[key];
  21.          console.log(key + ": " + value);
  22.          var li = document.createElement('li');
  23.          li.innerHTML = key + ": " + value;
  24.          document.querySelector('#list').insertBefore(li, null);
  25.       }
  26.    }
  27.    function resetStore() {
  28.         // erase all key values from store
  29.         localStorage.clear();
  30.         // reset displayed list too
  31.        document.querySelector('#list').innerHTML="";
  32.    }
  33.    function addSomeData() {
  34.       // store data
  35.       localStorage.lastName = "Buffa";
  36.       localStorage.firstName = "Michel";
  37.       // refresh display
  38.       seeAllKeyValuePairsStored();
  39.    }
  40.    function removeSomeData() {
  41.       // store data
  42.       localStorage.removeItem("lastName");
  43.       localStorage.removeItem("firstName");
  44.       // refresh display
  45.       seeAllKeyValuePairsStored();
  46.    }
  47. </script>
  48. </head>
  49. <body onload="getCountValue()">
  50.    <h1>Number of times this page has been seen on this browser: <spanid="counter"></span></h1>
  51.    <button onclick="seeAllKeyValuePairsStored()">Show all key value pairs stored in localStorage</button><br/>
  52.    <output id="list"></output>
  53.  
  54.    <button onclick="addSomeData()">Add some data to the store</button><br/>
  55.    <button onclick="removeSomeData()">Remove some data</button><br/>
  56.    <button onclick="resetStore()">reset store (erase all key/value pairs)</button>
  57. </body>
  58. </html>
 You can check in the Chrome dev. tools user interface that the content of the localStorage changes as you click on the buttons.

Practical example 2: save/restore user's preferences

INTRODUCTION

Local stores are also useful for saving/restoring user preferences of Web Applications. For example, the JS Bin tool you have been using since the beginning of this course uses localStorage to store the list of tabs you open and their width:
Like that, the next time you come back to JS Bin, "it will remember your last settings".
Another example is a guitar FX processor / amp simulator your instructor is writing with some of his students. It uses localStorage to save/restore presets values:
guitar fx processor uses localStorage

PRACTICAL EXAMPLE: SAVE/RESTORE PREFERENCES OF AN EXAMPLE YOU HAVE ALREADY SEEN

animated rectangle with GUIOriginal example on JS Bin: we can change the color, size and speed of the animated rectangle. However, each time we come back to the page default values are restored.
We would like to save the current values and find them back as they were when we come back to the page.
Here is a modified example that saves/restores its state, you can try it at JS Bin. In this modified version of the animated rectangle example, you can set the color, size, speed, etc. And if you reload the page, the state of the different input field is restored, but also the internal variables. Check the source code in the JS Bin example and also read the following explanations.
We used the same generic code for saving/restoring input fields' values we saw in the first example that usedlocalStorage. The only difference is that we renamed the two generic functions so that they correspond better to their role here (instead of saveFormContent we called the function restorePreferences).
The function initPreferences is executed when the page is loaded.
Source code extract:
  1. function initPreferences() {
  2.    console.log("Adding input listener to all input fields");
  3.    // add an input listener to all input fields
  4.    var listOfInputsInForm = document.querySelectorAll("input");
  5.    for(var i= 0; i < listOfInputsInForm.length; i++) {
  6.       addInputListener(listOfInputsInForm[i]);
  7.    }
  8.    // restore preferences
  9.    restorePreferences();
  10.    applyGUIvalues(); // Use the input fields' values we just restored to set internal 
  11.                      // size, incX, color, lineWidth variables
  12. }
  13.  
  14. function addInputListener(inputField) {
  15. // same as before
  16. }
  17.  
  18. function restorePreferences() {
  19. // same as old restoreFormContent
  20. }
  21.  
  22. function applyGUIvalues() {
  23.    // Check restored input field content to set the size of the rectangle
  24.    var sizeWidget = document.getElementById("size");
  25.    size = Math.sign(incX)*parseInt(sizeWidget.value);
  26.    // update also the outline element's value
  27.    document.getElementById("sizeValue").innerHTML = size;
  28.    // Check restored input field content to set the color of the rectangle
  29.    var colorWidget = document.getElementById("color");
  30.    ctx.fillStyle = colorWidget.value;
  31.    // Check restored input field content to set the speed of the rectangle
  32.    var speedWidget = document.getElementById("speed");
  33.    incX = Math.sign(incX)*parseInt(speedWidget.value);
  34.    // update also the outline element's value
  35.    document.getElementById("speedValue").innerHTML = Math.abs(incX);
  36.    // Check restored input field content to set the lineWidth of the rectangle
  37.    var lineWidthWidget = document.getElementById("lineWidth");
  38.    ctx.lineWidth = parseInt(lineWidthWidget.value);
  39. }

Practical example 3: example 1 on steroids

form that saves/restore its content using generic functionsTRY THE EXAMPLE!

This time, using the setItem and getItem method we saw in the previous page of the course, we could write some generic functions for saving/restoring input fields's content, without knowing in advance the number of fields in the form, their types, their ids, etc.
Furthermore, we removed all input listeners in the HTML, making it cleaner (no more oninput="localStorage.firstName = this.value;'...)

DEFINE LISTENERS + RESTORE OLD VALUES AFTER THE PAGE IS LOADED, USE GENERIC FUNCTIONS

We start writing an init() function called when the page is loaded. This function will:
    1. Define input listeners for all input fields
    2. Restore the last saved value for each input field, if present.
Source code:
  1. // Called when the page is loaded
  2. window.onload = init;
  3.  
  4. function init() {
  5.    console.log("Adding input listener to all input fields");
  6.    // add an input listener to all input fields
  7.    var listOfInputsInForm = document.querySelectorAll("input");
  8.    for(var i= 0; i < listOfInputsInForm.length; i++) {
  9.       addInputListener(listOfInputsInForm[i]);
  10.    }
  11.    // restore form content with previously saved values
  12.    restoreFormContent();
  13. }
And here is the addInputListener(inputField) function. It takes an input field as parameter and attaches an oninput listener to it, that will save the field's content each time a value is entered. The key will be the id of the input field (line 3):
  1. function addInputListener(inputField) {
  2.     inputField.addEventListener('input', function(event) {
  3.         localStorage.setItem(inputField.id, inputField.value);
  4.      }, false);
  5. }
Note that at line 2 we use addEventListener  (that is not using the oninput property here). addEventListener will not replace existing oninput definitions and keep all existing listeners unchanged.

RESTORE ALL INPUT FIELDS' CONTENT USING A GENERIC FUNCTION

We saw how save all input fields' content on the fly. Now, let's see how we can restore saved values and update the form. This is done by the function restoreFormContent():
  1. function restoreFormContent() {
  2.    console.log("restoring form content from localStorage");
  3.    // get the list of all input elements in the form
  4.    var listOfInputsInForm = document.querySelectorAll("input");
  5.    // For each input element,
  6.    // - get its id (that is also the key for it's saved content
  7.    // in the localStorage)
  8.    // - get the value associated with the id/key in the local
  9.    // storage
  10.    // - If the value is not undefined, restore the value
  11.    // of the input field
  12.    for(var i= 0; i < listOfInputsInForm.length; i++) {
  13.      var fieldToRestore = listOfInputsInForm[i];
  14.      var id = fieldToRestore.id;
  15.      var savedValue = localStorage.getItem(id);
  16.      if(savedValue !== undefined) {
  17.         fieldToRestore.value = savedValue;
  18.      }
  19.    }
  20. }
In this function, we first get the list of input fields (line 5), then iterate on it (line 14). For each input field, we get its id, which value is the key in localStorage for the previous data saved for this field (lines 15-16). Then if the value is not undefined, we restore it by setting the value of the input field (lines 19-20).

THESE GENERIC FUNCTION CAN BE USED IN MANY DIFFERENT PROJECTS

Indeed, if you look carefully, you will see that these functions are really useful. You may embed them easily in your own projects, maybe adapt them to some particular needs (i.e. for saving input type="checkboxes" that work a bit differently), etc. Later in the course, we will show how to reuse them with another example: the animated red rectangle.

Size limitation, security, localStorage orsessionStorage?

We already talked about client-side storage limit when we studied the HTML5 cache. We saw that the limit varies with lots of parameters. However, it's good to recall a few things from the specification:
    • User agents (browsers) should limit the total amount of space allowed for storage areas.
    • User agents may prompt the user when quotas are reached, allowing the user to grant more space to a site. This enables sites to store many user-created documents on the user's computer, for instance.
    • User agents should allow users to see how much space each domain is using.
    • A mostly arbitrary limit of five megabytes per origin is recommended (translation: give at least 5Mb per origin).
In many cases, local storage is all that your application will need for saving/loading data on demand. More complex ways to do it exist, such as IndexedDB, a No SQL database, that proposes transactions and comes usually with much bigger available space than local storage. IndexedDB usage is for advanced users and will be covered in the HTML5 part-2 course.
Additionally, there will be a limit on the amount of data that you can store there. Browsers enforce quotas that will prevent you from cluttering your users' drives excessively. Those quotas can vary from platform to platform, but are usually reasonably generous for simple cases (around 5MB), so if you are careful not to store anything huge there, you should be fine.
Finally, keep in mind that this storage is not necessarily permanent. Browsers are inconsistent in how they allow for it to be wiped, but in several cases it gets deleted with cookies — which is logical when you think of how it can be used for tracking in a similar fashion.
For serious applications, you might want to synchronize data that is there with the server on a regular basis, in order to avoid data loss (and in general, because users enjoy using the same service from multiple devices at once). This is rather complex to do, and frameworks such as Firebase can help here. Such techniques are beyond the scope of this course and will not be covered.

SESSIONSTORAGE KEY/VALUES INSTEAD OF COOKIES?

Note that if all you need is to store session-based data in a manner that is more powerful than cookies, you can use the sessionStorage object which works the exact same way as localStorage, but the lifetime is limited to a single browser session (lifetime of your tab/window).
Also note that in addition to being more convenient and capable of storing more data than cookies, it also has the advantage of being scoped to a given browser tab (or similar execution context).
Cookies' big danger: if a user has two tabs open to the same site, they will share the same cookies. Which is to say that if you are storing information about a given operation using cookies in one tab, that information will leak to the other side — something that can be confusing if the user is performing different tasks in each.
By using sessionStorage, the data you store will be scoped and therefore not leak across tabs!

Storing more than strings? Use JSON!

INTRODUCTION

Storing strings is all nice, but quickly limiting: you may want to store more complex data with at least a modicum of structure.
There are some simple approaches, such as creating your own minimal record format (e.g. a string with fields separated with a given character, using join() on store and split() upon retrieval) or using multiple keys (e.g.post_17_title, post_17_content, post_17_author, etc.). But these are really hacks. Thankfully, there's a better way,  JSON.stringify() and JSON.parse() methods.
JSON provides a great way of encoding and decoding data that is a really good match for JavaScript. You have to be careful not to use circular data structures or non-serializable objects, but in the vast majority of cases, plugging JSON support into your local store is straightforward.

TYPICAL USAGE

  1. locaStorage.key = JSON.stringify(object); // or...
  2. localStorage.setItem(key, JSON.stringify(object));
Let's try a simple toy example (online at JS Bin). Here, this example saves a JavaScript object in JSON, then restores it and checks that the object properties are still there!
JSON save / load in localStorage
Source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset=utf-8 />
  5. <title>Storing JSON Objects with Local Storage</title>
  6. <script>
  7.     var personObject= {'firstName': 'Michel', 'lastName': 'Buffa'};
  8.     // Store the object as a JSON String
  9.     localStorage.setItem('testObject', JSON.stringify(personObject));
  10.     // Retrieve the object from storage
  11.     var retrievedObject = JSON.parse(localStorage.getItem('testObject'));
  12.     console.log(retrievedObject.firstName + " " + retrievedObject.lastName);
  13.    // then you can use retrievedObject.firstName, retrievedObject.lastName...
  14. </script>
  15. </head>
  16. <body>
  17. </body>
  18. </html>
Explanations:
    • Line 7: we built a JavaScript object that contains a person.
    • Line 10: we store it in localStorage as a JSON string object, with a key equal to testObject.
    • Line 13: we restore it from localStorage as a string, and the JSON.parse methods turns it back into a JavaScript object.
    • Line 15: we print the values of the object properties.

MORE COMPLETE EXAMPLE THAT SHOWS HOW WE CAN SAVE A FORM'S CONTENT IN JSON

localStorage JSON

MORE COMPLETE EXAMPLE: A FORM AND A TABLE THAT DISPLAYS THE CONTACTS STORED IN LOCALSTORAGE

Example on JS Bin (uses summary/details so use a browser that supports it or add a polyfill, as seen in Week 1).
Add contacts using the form, see how the HTML table is updated. Try to reload the page: data are persisted in localStorage. 
serverless contact manager
Examine the localStorage:
localStorage view in devtools shows the data
The source code of this example is a bit long, we propose that you examine it in the JS Bin tool, we extensively commented it. It uses:
    • Well structured page with the new elements seen during Week 1 (section, article, nav, aside, etc.)
    • HTML5 form elements with builtin and custom validation (the date cannot be in the past, the firstName and lastName fields do not accept &, #, ! or $ characters),
    • localStorage for saving / restoring an array of contacts in JSON
    • It shows how to use the DOM API for updating dynamically the page content (build the HTML table from the array of contacts, add a new line when a new contact is submitted, etc.)
  • Here is the discussion forum for this part of the course. You can post your comments and share your creations here, and of course ask questions.
    Let us suggest some topics of discussion and optional projects:

SUGGESTED TOPICS

    • The last example showed a contact manager that can work without a server. There are frameworks such as Firebase that help synchronizing local data and remote data. A bit like Google photos... Did you hear about them? Did you already try to address such problems?
    • Did you try browser extensions for managing localStorage and sessionStorage? 

OPTIONAL PROJECTS

  • Here are a few project ideas. Your classmates and the team who prepared the course will all be glad to try them and give some feedback. Please post URLs in this discussion forum. These projects are optional, meaning that they won't be graded.
    • Project 1 (easy): Try to save / load the current state of a form you used. Or maybe, if you added a GUI to a canvas animation, try to reproduce what has been done in an example of the course: save/restore the GUI state and preferences (color, speed, etc., of the animation).
    • Project 2 (harder): Try to complete the last example presented in the course - the contact manager - with features such as: possibility to delete entries in the table (and also on the localStorage), add a picture (and save it as dataURL in localStorage. Data URLs are strings, so it's possible to save/restore them in localStorage), etc.

EXTERNAL RESOURCES

No comments:

Post a Comment