Buy The Book
This example is posted here for the convenience of my readers.
Tip the Author
Found a helpful example, but don't own the book?
Advertising
Table of Examples

/**
 * PObject.js: JavaScript objects that persist across browser sessions and may
 *   be shared by web pages within the same directory on the same host.
 *
 * This module defines a PObject() constructor to create a persistent object.
 * PObject objects have two public methods.  save() saves, or "persists" the
 * current properties of the object, and forget() deletes the persistent
 * properties of the object.  To define a persistent property in a PObject,
 * simply set the property on the object as if it were a regular JavaScript
 * object and then call the save() method to save the current state of 
 * the object.  You may not use "save" or "forget" as a property name, nor
 * any property whose name begins with $. PObject is intended for use with
 * property values of type string.  You may also save properties of type 
 * boolean and number, but these will be converted to strings when retrieved.
 * 
 * When a PObject is created, the persistent data is read and stored in the
 * newly created object as regular JavaScript properties, and you can use the
 * PObject just as you would use a regular JavaScript object.  Note, however
 * that persistent properties may not be ready when the PObject() constructor
 * returns, and you should wait for asynchronous notification using an onload
 * handler function that you pass to the constructor.
 *
 * Constructor:
 *    PObject(name, defaults, onload):
 * 
 * Arguments:
 *
 *    name       A name that identifies this persistent object. A single pages
 *               can have more than one PObject, and PObjects are accessible
 *               to all pages within the same directory, so this name should
 *               be unique within the directory. If this argument is null or
 *               is not specified, the filename (but not directory) of the
 *               containing web page is used.
 * 
 *    defaults   An optional JavaScript object. When no saved value for the
 *               persistent object can be found (which happens when a PObject
 *               is created for the first time), the properties of this object
 *               are copied into the newly created PObject.
 *
 *    onload     The function to call (asynchronously) when persistent values
 *               have been loaded into the PObject and are ready for use.
 *               This function is invoked with two arguments: a reference
 *               to the PObject and the PObject name.  This function is
 *               called *after* the PObject() constructor returns.  PObject
 *               properties should not be used before this.
 *
 * Method PObject.save(lifetimeInDays):
 *   Persist the properties of a PObject.  This method saves the properties of
 *   the PObject, ensuring that they persist for at least the specified
 *   number of days.  
 *
 * Method PObject.forget():
 *   Delete the properties of the PObject.  Then save this "empty" PObject to
 *   persistent storage and, if possible, cause the persistent store to expire.
 *
 * Implementation Notes:
 * 
 * This module defines a single PObject API but provides three distinct
 * implementations of that API. In Internet Explorer, the IE-specific
 * "UserData" persistence mechanism is used.  On any other browser that has a
 * Macromedia Flash plugin, the Flash SharedObject persistence mechanism is
 * used. Browsers that are not IE and do not have Flash available fall back on
 * a cookie-based implementation. Note that the Flash implementation does not
 * support expiration dates for saved data, so data stored with that
 * implementation persists until deleted.
 *
 * Sharing of PObjects:
 *
 * Data stored with a PObject on one page is also available to other pages
 * within the same directory of the same web server. When the cookie 
 * implementation is used, pages in subdirectories can read (but not write)
 * the properties of PObjects created in parent directories. When the Flash
 * implementation is used, any page on the web server can access the shared
 * data if it cheats and uses a modified version of this module.
 * 
 * Distinct web browser applications store their cookies separately and 
 * persistent data stored using cookies in one browser is not accessible using
 * a different browser.  If two browsers both use the same installation of
 * the Flash plugin, however, these browsers may share persistent data stored
 * with the Flash implementation.
 * 
 * Security Notes:
 *
 * Data saved through a PObject is stored unencrypted on the user's hard disk.
 * Applications running on the computer can access the data, so PObject is
 * not suitable for storing sensitive information such as credit card numbers,
 * passwords, or financial account numbers.
 */

// This is the constructor
function PObject(name, defaults, onload) {
    if (!name) { // If no name was specified, use the last component of the URL
        name = window.location.pathname;
        var pos = name.lastIndexOf("/");
        if (pos != -1) name = name.substring(pos+1);
    }
    this.$name = name;  // Remember our name

    // Just delegate to a private, implementation-defined $init() method.
    this.$init(name, defaults, onload);
} 

// Save the current state of this PObject for at least the specified # of days.
PObject.prototype.save = function(lifetimeInDays) {
    // First serialize the properties of the object into a single string
    var s = "";                           // Start with empty string
    for(var name in this) {               // Loop through properties
        if (name.charAt(0) == "$") continue;  // Skip private $ properties
        var value = this[name];               // Get property value
        var type = typeof value;              // Get property type
        // Skip properties whose type is object or function
        if (type == "object" || type == "function") continue;
        if (s.length > 0) s += "&";           // Separate properties with &
        // Add property name and encoded value
        s += name + ':' + encodeURIComponent(value);
    }

    // Then delegate to a private implementation-defined method to actually
    // save that serialized string.
    this.$save(s, lifetimeInDays);
};

PObject.prototype.forget = function() {
    // First, delete the serializable properties of this object using the
    // same property selection criteria as the save() method.
    for(var name in this) {
        if (name.charAt(0) == '$') continue;
        var value = this[name];
        var type = typeof value;
        if (type == "function" || type == "object") continue;
        delete this[name];  // Delete the property
    }

    // Then erase and expire any previously saved data by saving the 
    // empty string and setting its lifetime to 0.
    this.$save("", 0);
};

// Parse the string s into name/value pairs and set them as properties of this.
// If the string is null or empty, copy properties from defaults instead
// This private utility method is used by the implementations of $init() below.
PObject.prototype.$parse = function(s, defaults) {
    if (!s) {  // If there is no string, use default properties instead
        if (defaults) for(var name in defaults) this[name] = defaults[name];
        return;
    }

    // The name/value pairs are separated from each other by ampersands, and
    // the individual names and values are separated from each other by colons.
    // We use the split() method to parse everything.
    var props = s.split('&'); // Break it into an array of name/value pairs
    for(var i = 0; i < props.length; i++) { // Loop through name/value pairs
        var p = props[i];
        var a = p.split(':');     // Break each name:value pair at the colon
        this[a[0]] = decodeURIComponent(a[1]); // Decode and store property
    }
};

/* 
 * The implementation-specific portion of the module is below.
 * For each implementation, we define an $init() method that loads 
 * persistent data and a $save() method that saves it.
 */ 

// Determine if we're in IE, and, if not, whether we've got a Flash
// plugin installed and whether it has a high enough version number
var isIE = navigator.appName == "Microsoft Internet Explorer";
var hasFlash7 = false;
if (!isIE && navigator.plugins) { // If we use the Netscape plugin architecture
   var flashplayer = navigator.plugins["Shockwave Flash"];
   if (flashplayer) {    // If we've got a Flash plugin
       // Extract the version number
       var flashversion = flashplayer.description; 
       var flashversion = flashversion.substring(flashversion.search("\\d"));
       if (parseInt(flashversion) >= 7) hasFlash7 = true;
   }
}

if (isIE) {  // If we're in IE
    // The PObject() constructor delegates to this initialization function
    PObject.prototype.$init = function(name, defaults, onload) {
        // Create a hidden element with the userData behavior to persist data
        var div = document.createElement("div");  // Create a <div> tag
        this.$div = div;                          // Remember it
        div.id = "PObject" + name;                // Name it
        div.style.display = "none";               // Make it invisible

        // This is the IE-specific magic that makes persistence work.
        // The "userData" behavior adds the getAttribute(), setAttribute(),
        // load() and save() methods to this <div> element. We use them below.
        div.style.behavior = "url('#default#userData')";  

        document.body.appendChild(div);  // Add the element to the document

        // Now we retrieve any previously saved persistent data.
        div.load(name);  // Load data stored under our name
        // The data is a set of attributes.  We only care about one of these
        // attributes.  We've arbitrarily chosen the name "data" for it.
        var data = div.getAttribute("data");

        // Parse the data we retrieved, breaking it into object properties
        this.$parse(data, defaults);

        // If there is an onload callback, arrange to call it asynchronously
        // once the PObject() constructor has returned.
        if (onload) {
            var pobj = this;  // Can't use "this" in the nested function
            setTimeout(function() { onload(pobj, name);}, 0);
        }
    }

    // Persist the current state of the persistent object
    PObject.prototype.$save = function(s, lifetimeInDays) {
        if (lifetimeInDays) { // If lifetime specified, convert to expiration
            var now = (new Date()).getTime();
            var expires = now + lifetimeInDays * 24 * 60 * 60 * 1000;
            // Set the expiration date as a string property of the <div>
            this.$div.expires = (new Date(expires)).toUTCString();
        }

        // Now save the data persistently
        this.$div.setAttribute("data", s); // Set text as attribute of the <div>
        this.$div.save(this.$name);        // And make that attribute persistent
    };
}
else if (hasFlash7) { // This is the Flash-based implementation
    PObject.prototype.$init = function(name, defaults, onload) {
        var moviename = "PObject_" + name;    // id of the <embed> tag
        var url = "PObject.swf?name=" + name; // url of the movie file

        // When the Flash player has started up and has our data ready,
        // it notifies us with an FSCommand.  We must define a
        // handler that is called when that happens
        var pobj = this;  // for use by the nested function
        // Flash requires that we name our function with this global symbol
        window[moviename + "_DoFSCommand"] = function(command, args) {
            // We know Flash is ready now, so query it for our persistent data
            var data =  pobj.$flash.GetVariable("data")
            pobj.$parse(data, defaults);    // Parse data or copy defaults
            if (onload) onload(pobj, name); // Call onload handler, if any
        };

        // Create an <embed> tag to hold our Flash movie.  Using an <object>
        // tag is more standards-compliant, but it seems to cause problems
        // receiving the FSCommand.  Note that we'll never be using Flash with
        // IE, which simplifies things quite a bit.
        var movie = document.createElement("embed");  // element to hold movie
        movie.setAttribute("id", moviename);          // element id
        movie.setAttribute("name", moviename);        // and name
        movie.setAttribute("type", "application/x-shockwave-flash"); 
        movie.setAttribute("src", url);  // This is the URL of the movie
        // Make the movie inconspicuous at the upper-right corner
        movie.setAttribute("width", 1);  // If this is 0, it doesn't work.
        movie.setAttribute("height", 1);
        movie.setAttribute("style", "position:absolute; left:0px; top:0px;");

        document.body.appendChild(movie);  // Add the movie to the document
        this.$flash = movie;               // And remember it for later
    };

    PObject.prototype.$save = function(s, lifetimeInDays) {
        // To make the data persistent, we simply set it as a variable on
        // the Flash movie.  The ActionScript code in the movie persists it.
        // Note that Flash persistence does not support lifetimes.
        this.$flash.SetVariable("data", s); // Ask Flash to save the text
    };
}
else { /* If we're not IE and don't have Flash 7, fall back on cookies */
    PObject.prototype.$init = function(name, defaults, onload) {
        var allcookies = document.cookie;             // Get all cookies
        var data = null;                              // Assume no cookie data
        var start = allcookies.indexOf(name + '=');   // Look for cookie start
        if (start != -1) {                            // Found it
            start += name.length + 1;                 // Skip cookie name
            var end = allcookies.indexOf(';', start); // Find end of cookie
            if (end == -1) end = allcookies.length;
            data = allcookies.substring(start, end);  // Extract cookie data
        }
        this.$parse(data, defaults);  // Parse the cookie value to properties
        if (onload) {                 // Invoke onload handler asynchronously
            var pobj = this;
            setTimeout(function() { onload(pobj, name); }, 0);
        }
    };

    PObject.prototype.$save = function(s, lifetimeInDays) {
        var cookie = this.$name + '=' + s;          // Cookie name and value
        if (lifetimeInDays != null)                 // Add expiration
            cookie += "; max-age=" + (lifetimeInDays*24*60*60);
        document.cookie = cookie;                   // Save the cookie
    };
}
Table of Examples