The JavaScript Apostle

Client-Side Persistence Without Cookies

By Danny Goodman


Send comments and questions about this article to View Source. 


I don't know if this happens to you, but my blood pressure jumps a few points every time I read an article in the mainstream press about browser cookies being used for all kinds of dastardly tracking of web surfer habits (the same kind of tracking that server logs have been doing forever). While I'm not aware of existing sites that hack their way into cookie data they shouldn't be seeing, the tone of all those news reports makes it seem as though you'd better turn off the cookie feature in your browser before some nerdy stalker in West Foofraw, New Jersey, finds out you have holes in your underwear as you sit in front of your computer.

This all-too-common user disconnection of cookies makes life more difficult for those of us who employ cookies as a kind of super-global variable that persists across all navigation around our sites. Pages can come and go, browser windows can be resized, and the user can click the Reload button all she wants without destroying a valuable variable needed by a script somewhere. What we need, then, is a way to maintain some data during a surfing session without resorting to cookies.

There are two techniques in common use today. One is to encode the global data as the search segment (following a ? symbol) in the current URL of the document. I've experienced inconsistent behavior with this approach in some non-Netscape browsers. The appended values also appear in the Location bar of the browser window, exposing them to user manipulation -- intentional or accidental. The other technique, detailed in this article, uses what amounts to one or more text input elements positioned out of sight. By way of example, I'll provide a first pass at a simple shopping cart that works on a wide range of scriptable browsers.


WHY WE NEED PERSISTENCE

If you've done much application building with client-side JavaScript, you know that the most global scope of a variable is the document in which it is defined. A global variable disappears from a window or frame when the document unloads. Therefore, if your goal is to pass a value from the script of one document to a script in the next document that loads into the same window or frame, you need some other place to store that value temporarily. A cookie is certainly convenient. Even though a cookie is accessed as a property of a document object, all cookie values generated by documents from the same domain and server are available to all documents served from that same server. Turn off cookies, however, and that route is no longer available.

Appending persistent data to the URL of the next document (assuming the next document is accessed from a link within your own document collection) can work. Some scripters assign the value as a search suffix (with the ? symbol divider) or a hash suffix (with the # symbol divider). At the destination, a script can extract the location.search or location.hash property to grab the data. But I've seen applications of this scheme that get confused when the user is left to his own devices for navigating around the site. Sometimes even an inopportune click of the Back button can cause the appended data to be garbled or lost. And, because I'm a geek, if I see such extra data in the Location bar, I get distracted as I try to figure out what the appended data is doing or what information is being carried around.

I'd rather hide that extra data from the user, and let it work like magic. Unfortunately, some convenient ways of hiding data, such as the hidden input element or global variables stored in other frames, are not persistent enough. Resizing the window, clicking the Reload button, or clicking the Reset form button can destroy data that's plugged into a hidden element after it loads. And global variable values can also disappear upon reload and (in some browser versions) upon resize. But the next best and most persistent HTML element available is the text input element or its compatriot, the textarea element. Except in Internet Explorer 3, a document reload does not alter the contents of a text input element. And if you can place that element in a form by itself, there will be no Reset form element to wipe out its content.

The trick with the text input element is hiding it from the user without resorting to version 4-specific Dynamic HTML element positioning. More to the point, you want the document holding the field to remain in the browser window all during the user's visit. The way I go about this is to create a (nearly) hidden frame for the persistent document as part of a frameset. I'll come back to the frameset issue later.


ESTABLISHING A DATA STRUCTURE

Perhaps the most challenging part of implementing persistent data for a text field is designing a data structure for your data. If the data is a single value, it really isn't a problem: simply plant the data into the text field and retrieve or modify it as needed during site navigation. But for a more complex application, such as a shopping cart, you have the responsibility of determining how the data must be formatted. For example, if the data you want to preserve is from a form, you must generate a string version of what might otherwise be considered a database record. It's up to you to establish a delimiter structure. If your data may include a variable number of such records, you must write even more elaborate scripts to replicate the management of this "database" data.

One tactic is to mimic the way JavaScript exposes cookie data. Each cookie consists of a string of name/value pairs. All scriptable Navigator versions can store up to 20 name/value pairs in a cookie associated with a server. The name and value portions of the script representation of a cookie are separated by an equals sign (=); multiple name/value pairs are delimited by semicolons:

name1=value1;name2=value2;name3=value3

Unfortunately, since you aren't truly using the document.cookie object for text field data, you don't have the benefit of the browser's cookie management facilities for updating the value of a particular name/value pair. It will be up to your scripts to manage the name/value pairs.

Despite the convenience of JavaScript arrays for storing the equivalent of fields and records, they don't help you with the persistence issue. Whether you intend to use cookies or the text input element tactic, all persistent data must be in string form. You cannot store JavaScript arrays in cookies or text fields without first converting them to strings. While newer versions of JavaScript simplify these conversions (with the stringObj.split() and arrayObj.join() methods), if you want your persistent data structures to work with older scriptable browser versions, you'll have to roll your own conversion routines.

As demonstrated in the shopping cart example later, I chose to build a variable-length string that acts as a database file. Each record has four fields: product name, stock number, price, and quantity. Fields are colon-delimited; records are semicolon-delimited. As a user shops through the catalog, new entries are appended to the "database." When it's time to review the shopping cart contents, scripts extract the records and fields. The pieces are distributed into elements of a dynamically generated form. With the form in view, users can make changes to the quantity of an item. For each change to the form, a script reconstructs the database data from the visible and hidden form element values. If the user sets the quantity of an item to 0, the item is not rebuilt into the "database."

A variant of this scenario is to use many text fields rather than just one. In other words, the hidden frame document essentially contains an array of text input elements that mimics the item entries of the shopping cart or order form. You can get away with this provided you create enough of these fields to accommodate the most ambitious customer of your site. Or if you have a limited selection of products, you can maintain the array of fields such that there is one row of fields for each of the products in your online catalog.

It should be clear that there is no one best way to structure your data for persistence. The type and amount of data drive the design decisions.


DESIGNING THE FRAMESET

Due to the ways browsers render framesets, even a zero-sized frame shows a sliver of itself in the browser window. But I suggest you create these kinds of framesets without any borders so that the sliver is less noticeable. Moreover, you need to make the frame that holds the persistent data input field nonresizable and nonscrollable. The frameset definition for the sample online catalog is shown in Listing 1. You can view the frameset here. 


Listing 1

<HTML>

<HEAD>

<TITLE>Welcome to Widget World</TITLE>

</HEAD>

<FRAMESET ROWS="0,100%" BORDER=0 FRAMEBORDER="no">

  <FRAME NAME="cartFrame" SRC="cart.html" NORESIZE SCROLLING="no">

  <FRAMESET COLS="20%,80%">

    <FRAME NAME="toc" SRC="toc.html">

    <FRAME NAME="display" SRC="widget1.html">

  </FRAMESET>

</FRAMESET>

</HTML>


For the demonstration, I've chosen a fairly traditional navigation frameset. A left-hand column that acts as a table of contents occupies 20 percent of the window width, while the content display area occupies the other 80 percent. I've placed the "hidden" frame in its own row above the frames that users will be working with. During development, let more of the "hidden" frame be visible so that you can monitor the contents of the text field as you test the application. If you assign a background image or color to the viewable frames, you may want to carry the same color or image to the document in the "hidden" frame to make the sliver of frame blend with the rest of the content.

If you don't want to use multiple frames for the visible portion of the page, you can simply create a two-row frameset. Even a frame-averse visitor won't be the wiser.


INSIDE THE SHOPPING CART

Rather than go through a tedious line-by-line explication of the code involved in the sample shopping cart, I'll provide an overview of the architecture and basic operation. You can download a complete set of files and explore the well-commented code at your leisure. It will be helpful to open the catalog and navigate through it while reading about its structure.

The frameset shown in Listing 1 is contained by the index.html page. This is where users start to view the catalog. In a real environment, there would probably be a more "warm and fuzzy" opening page welcoming the visitor to the site and explaining the online shopping that's available.

Almost all of the scripting is contained by the cart.html file, which loads into the "hidden" frame. Buttons that appear in the other frames frequently reference functions in this top row frame. Again, this frame also contains the text input field that stores the data while the user navigates freely through the catalog.

The left visible frame holds a document named toc.html. For the demonstration, it's nothing more than a simple navigation aid to reach any of the three products featured in this abbreviated catalog of widgets.

Each of the three widget models in the demo product catalog has its own HTML page. You also get a demonstration of a technique that works for small online catalogs whose audience is likely to be using more modern browsers. Information about the products is stored as objects in the products.js file that's linked into each of the product pages. Bill Dortch of hIdaho Design is a pro at this kind of catalog and has implemented very sophisticated online catalogs that are easy to maintain. Of course, the browser must be modern enough to be able to link in external JavaScript (.js) files. On the other hand, if you're trying to appeal to the widest audience, your product pages will either be dedicated HTML pages or be served up from a database on the server via a CGI program.


CONTROLLING THE SHOPPING CART

On each product page there is not only information about the widget model, but three buttons. One button adds the item to the shopping cart. The user can enter the quantity on the product page and adjust it later. A second button navigates the user to a page that displays the contents of the shopping cart. And the third button goes to the checkout page, which shows the cart contents and provides fields for entry of name, address, and other order information.

Adding an entry to the shopping cart simply appends a formatted record to the hidden text field. In other words, the button's event handler passes product information to a function in cart.html that adds the record to the "database" text field:

function addToCart(title, stock, price, qty) {
  var output = title + ":" + stock + ":" + price + ":" + qty + ":;"
  document.cart.cartData.value += output
}

I've chosen to create a data structure that uses colons to delimit fields within the semicolon-delimited record. This demonstration (along with most client-side shopping carts I've seen) does no validation with regard to existing entries of the same item. It wouldn't be all that difficult to add that level of validation, and in a production environment I would do so.

When the user asks to review the shopping cart contents, a script in the hidden frame document converts the string in the text field to an array for scripting convenience. The array is passed to the function that assembles the table (makeTable()). This function breaks up each "database record" into another array of database fields (via the itemSplit() function). The script uses the data from the database field array to assemble the table. Listing 2 shows the main function that creates the table.


Listing 2

function makeTable(cartArray) 

{

    var item, extension

    var runningTotal = 0

    var table = "<TABLE BORDER=1>\n"

    

    table += "<TR><TH>Quantity<TH>Stock No.<TH>Item Name<TH>Price<TH>Total</TR>\n"

   

    // Loop through array to generate one table row per array entry.

    for (var i = 0; i < cartArray.length; i++) {

       // Extract one record at a time and convert it to an array.

       item = itemSplit(cartArray[i])

       

       // The quantity field, including an event handler to recalculate the table

       table += "<TR><TD><INPUT TYPE='text' NAME='qty" + i + "' SIZE=4 VALUE='" 

          + item[3] + "'"

       table += "onChange='parent.cartFrame.recalc(this.value," + item[2] + "," 

           + i + ")'>"

       

       // Copy three parts of the record to hidden fields for data integrity.

       table += "<INPUT TYPE='hidden' NAME='stock" + i + "' VALUE='" + item[1] 

          + "'></TD>"

       table += "<INPUT TYPE='hidden' NAME='title" + i + "' VALUE='" + item[0] 

          + "'></TD>"

       table += "<INPUT TYPE='hidden' NAME='price" + i + "' VALUE='" + item[2] 

          + "'></TD>"

       

       // Hard-wire the product stock num, title, and price.

       table += "<TD>" + item[1] + "<TD>" + item[0] + "<TD>$" + item[2]

      

       // Calculate the extension for the item and format it for dollars and cents.

       extension = format((item[3] * item[2]),2)

      

       // Accumulate running total.

       runningTotal += parseFloat(extension)

      

       // Put extension into a field; the onFocus event handler inhibits user 

       // modification.

       table += "<TD>$<INPUT TYPE='text' NAME='extend" + i + "' SIZE=8 VALUE='"

          + extension + "'"

       table += "onFocus='this.blur()'></TR>\n"

   }

   // Keep a hidden number of rows.

   table += "<TR><TD COLSPAN=3>&nbsp;<INPUT TYPE='hidden' NAME='itemCount' 

      VALUE='" + i + "'><TD>Order Total:<TD>$"

  

   // Display grand total in a text field.

   table += "<INPUT TYPE='text' NAME='grandTotal' SIZE=10

      VALUE='" + format(runningTotal, 2) + "' onFocus='this.blur()'></TR>"

   table += "</TABLE>"

   return table

}


All product information is displayed in a table, with interactive text fields for the item quantities, price extensions, and grand total. The table is "live" in that any change to a quantity automatically recalculates the extension and grand total. It also reconstitutes the entire database by recombining all the data components previously written to hidden input elements in the table. This assures that the persistent data is always up to date and always reflects what the user sees in the table. If the user clicks in the navigation frame or on the Back button to get to a product page, the shopping cart data is intact.

Note: As you'll surely notice in the code, the table consists of form elements whose names contain index values to indicate which row the element is in (for example, quantity0, quantity1, and so on). In the for loop that iterates through the rows of elements, the code uses the eval() function to create object references from string equivalents. If it weren't for an oversight in Internet Explorer 3, it would be more convenient to simply treat multiple instances of the like-named group of elements as an array (for example, quantity[0], quantity[1], and so on). All scriptable versions of Navigator and IE 4 turn like-named elements into an array of that name; IE 3 does not. To accommodate that browser, I use the longer way around. In practice, the explicit naming might also help the server deal with incoming data when the order form is submitted.

Included on the shopping cart review page is a button labeled Refresh. This button simply reruns the routines that assemble the review page, deriving data from the shopping cart text field again. If the user entered 0 for the quantity of an item earlier, that item is no longer in the cart and does not appear in the refreshed cart review table.

At checkout time, the same string-parsing routine builds the table to display the contents. In the demonstration, another table of fields provides entry space for name and address info. In a real order form, other controls would provide places to indicate payment method, shipping costs, sales tax, and the like. For the convenience of a CGI program processing the order, the table includes all product info, most of which is in hidden input elements. So that you can see how the form elements are organized and the type of data stored in each element, I've created a special demo version that lets you e-mail the form's contents to yourself. If you're using Navigator, the form data (as name/value pairs) is embedded in an e-mail message body.

As long as the page remains in the browser cache during the current browser session, Navigator will restore the shopping cart data even if the user navigates to other web sites and unloads the online catalog frameset. Thus, just as with a cookie, the user can go elsewhere and then return to the site and continue shopping.


OTHER APPLICATIONS

I used a shopping cart example in this article because e-commerce is all the rage. But the same techniques can be used in other applications that require persistent data across pages within a web site. At my site, you can look at an extensive client-side application, called Decision Helper, which is implemented in two ways: with and without cookies. The user can scarcely tell the difference, if at all.

Without the cookie feature turned on in the visitor's browser, there is no way to silently (without signed script permission dialogs, for example) save information between browser sessions. A cookie bearing an expiration date in the future will stick around on the user's hard disk until the next session following that date. Users who have irrational fears of cookie crimes may be missing convenience features at their favorite sites (such as flags that indicate what's new since their last visit). It's their loss. At least your Web site can pass variables between pages even if visitors have cookies turned off.


View Source wants your feedback! Write to us and let us know what you think of this article.


Author and consultant Danny Goodman's JavaScript Bible new 3rd edition (IDG Books) is now in stores. Look for Dynamic HTML: The Definitive Reference coming in July 1998 from O'Reilly & Associates.

(5.98)

For more Internet development resources, try Netscape TechSearch


Copyright © 1999 Netscape Communications Corporation.
This site powered by: Netscape Enterprise Server and Netscape Compass Server.