One of the challenges of writing applications for the World Wide Web has been the inability of the Web to maintain state. That is, after a user sends a request to the server and a Web page is returned, the server forgets
all about the user and the page they have just downloaded.
If the user clicks on a link, the server doesn't have background information about what page the user is coming from and, more importantly, if the user returns to page at a later date, there is no information available to the server about the user's
previous actions on the page.
However, maintaining state can be important to developing complex interactive applications. Several sites work around this problem using complex server-end CGI scripts. However, Navigator 2.0 addresses
the problem with cookies: a method of storing information locally in the browser and sending it to the server whenever the appropriate pages are requested by the user.
JavaScript provides the capability to work with client-side state information stored as cookies.
In addition to cookies, JavaScript offers the navigator object, which provides information about the version of the browser a user has and, in the future, will likely include methods to customize the browser.
The information available in the navigator object can be useful for a number of purposes, including ensuring that users are using a version of the browser that supports all the features of a script.
In this chapter, we take a detailed look at using cookies in the JavaScript applications as well as how to use the navigator object, including
Cookies provide a method to store information at the client side and have the browser provide that information to the server along with a page request.
In order to understand how the mechanism works, it is important to have a basic understanding of how servers and clients communicate on the World Wide Web using the hypertext transfer protocol
(HTTP).
The hypertext transfer protocol is fairly simple. When a user requests a page, an HTTP request is sent to the server. The request includes a header that defines several pieces of information, including the page being requested.
The server returns an HTTP response that also includes a header. The header contains information about the document being returned, including its MIME type (such as text/html for a standard HTML page or image/gif for a GIF
file).
These headers all contain one or more fields of information in a basic format:
Field-name: Information
Cookie information is shared between the client browser and a server using fields in the HTTP headers. The way it works is fairly simplein theory.
When the user requests a page for the first time, a cookie (or more than one cookie) can be stored in the browser by a Set-Cookie entry in the header of the response from the server. The Set-Cookie field includes the information to be stored in the cookie along with several optional pieces of information, including an expiry date, path, and server information, and if the cookie requires security.
Then, when the user requests a page in the future, if a matching cookie is found among all the stored cookies, the browser sends a Cookie field to the server in a request header. The header will contain the information stored in that cookie.
The Set-Cookie and Cookie fields use a fairly simple syntax to transfer significant information between the client and server.
Set-Cookie takes the form
Set-Cookie: name=VALUE; expires=DATE; path=PATH; domain=DOMAIN; secure
The name=VALUE entry is the only required piece of information that must be included in the Set-Cookie field. This is simply a string of characters defining information to be stored in the cookie for later transmission back to the server. The
string cannot contain semi-colons, commas, or spaces.
All the other entries in the Set-Cookie field are optional and are outlined in Table 9.1.
Name |
Description |
expires=DATE |
Specifies the expiry date of a cookie. After this date the cookie will no longer be stored by the client or sent to the server (DATE takes the form Wdy, DD-Mon-YY HH:MM:SS GMTdates are only stored in Greenwich Mean Time). By default, the value of expires is set to the end of the current Navigator session. |
path=PATH |
Specifies the path portion of URLs for which the cookie is valid. If the URL matches both the path and domain, then the cookie is sent to the server in the request header. (If left unset, the value of path is the same as the document which set the cookie). |
domain=DOMAIN |
Specifies the domain portion of URLs for which the cookie is valid. The default value for this attribute is the domain of the current document setting the cookie. |
|
Specifies that the cookie should only be transmitted over a secure link (i.e. to HTTP servers using the SSL protocolknown as HTTPS servers) |
By comparison, the Cookie field in a request header contains only a set of name-value pairs for the requested URL:
Cookie: name1=VALUE1; name=VALUE2 ...
It is important to realize that multiple Set-Cookie fields can be sent in a single response header from the server.
There are some limitations on the use of cookies. Navigator 2.0 will only store 300 cookies in total. Within that 300, each cookie is limited to four kilobytes in length, including all the optional attributes, and only 20
cookies will be stored for each domain. When the number of cookies is exceeded, the browser will delete the least recently used and when the length of a cookie is too long, the cookie is trimmed to fit.
There are several ways that cookies can be used to enhance interactive applications.
For instance, there are sites using cookies to implement shopping carts. That is, a user traverses multiple pages at a site and selects items he wants to buy. The selections are stored in cookies until a JavaScript script or CGI script is executed to
total up the purchases.
Other applications that could use cookies include
In order for cookies to be useful, it is necessary for the server to be able to take advantage of the cookie information it receives and for the server to be able to generate cookie headers if they
are needed.
This is primarily done by using CGI scripts.
For instance, if you want to provide a custom search tool that would search World Wide Web indexes selected by the user, you would need to develop a system that follows this basic pattern:
This type of application could produce results similar to Figures 9.1 and 9.2.
Figure 9.1. If the user has never visited the URL, a page with a new form is sent to the user.
To implement this type of server-side processing for cookies may require significant increases in the load on a Web server. With this model most pages are being built dynamically based on receiving cookie information in the header.
This is in contrast to typical Web pages, which are static and all the server needs to do is send the correct file to the client without any additional processing.
In JavaScript, however, cookies become available for processing by the client.
JavaScript makes the cookie property of the document object available for processing. The cookie property exposes all the attributes of cookies
for the page to the script and enables the script to set new cookies. In this way much, if not all, of the server-end processing that would be done to take advantage of cookies can now be done by the client in a JavaScript script.
The cookie property simply contains a string with the value that would be sent out in a Cookie field for that page.
As a string, it can be manipulated like any other string literal or variable using the methods and properties of the string object.
By assigning values to document.cookie, it is possible to create new cookies. The value of the string assigned to the cookie property should be the same as what would be
sent by the server in the Set-Cookie header field.
For instance, if you create two cookies named cookie1 and cookie2 as follows:
document.cookie = "cookie1=First_cookie"; document.cookie = "cookie2=Second_cookie";
Then document.write(document.cookie) would produce output that looks such as this:
cookie1=First_cookie; cookie2=Second_cookie
If you want to set optional properties such as the expiry date or path, you can use a command like:
document.cookie = 'cookie1=First_cookie; expires=Mon, 01-Jul-95 12:00:00 GMT; path="/"';
This would create a cookie named cookie1 that expires at noon on 1 July 1995 and is valid for all documents in the default domain because the path is set to the top level directory for the domain.
Of course, times are likely to be set using offsets from the current time. For instance, you may want an expiry date one day or one year from the current date. You can use the methods of the Date object to achieve this:
expires = new Date(); expires.setTime (expires.getTime() + 24 * 60 * 60 * 365 * 1000); document.cookie = "cookie2=Second_cookie; expires=" + expires.toGMTString();
These commands use the Date object to set a time one year after today by adding 24x60x60x365x1000 (the number of milliseconds in one year) to the current date and time. You can then use expires.toGMTString() to return the date string in GMT time as
required by the cookie.
In this example, you are going to use cookies to expand the functionality of the script you created in Exercise 8.4 in the previous chapter.
In Exercise 8.4, you extended the simple color testing application to include the capability for the user to select a URL to test the colors with.
In this example you further extend the script so that if a user has entered a URL, it is stored in a cookie, as are the colors. The next time the user returns to the page, the URL is recalled, loaded, and displayed with the stored colors. The expiry
date for a cookie should be 30 days from the current date.
In order to achieve this, you need to do several things. You need to save the colors and URLs as cookies whenever they are changed. You also need
a function that can decode the cookie when the page is loaded for the first time.
Listing 9.1 includes these additions.
Input
<HTML> <HEAD> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FORM OTHER BROWSERS var expires = new Date(); expires.setTime (expires.getTime() + 24 * 60 * 60 * 30 * 1000); var expiryDate = expires.toGMTString(); function display(form) { parent.output.document.bgColor = form.bg.value; parent.output.document.fgColor = form.fg.value; parent.output.document.linkClor = form.link.value; parent.output.document.alinkColor = form.alink.value; parent.output.document.vlinkColor = form.vlink.value; } function loadPage(url) { var toLoad = url.value; if (url.value == "") toLoad = "sample.htm"; open (toLoad,"output"); } function newCookie(name,value) { document.cookie = name + "=" + value + "; expires=" + expiryDate; } function getCookie(name) { var cookieFound = false; var start = 0; var end = 0; var cookieString = document.cookie; var i = 0; // SCAN THE COOKIE FOR name while (i <= cookieString.length) { start = i; end = start + name.length; if (cookieString.substring(start,end) == name) { cookieFound = true; break; } i++; } // IS name FOUND? if (cookieFound) { start = end + 1; end = document.cookie.indexOf(";",start); if (end < start) end = document.cookie.length; return document.cookie.substring(start,end); } return ""; } // STOP HIDING SCRIPT --> </SCRIPT> </HEAD> <BODY onLoad="loadPage(document.forms[0].url); display(document.forms[0]);"> <CENTER> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS document.write('<H1>The Colour Picker</H1>'); document.write('<FORM METHOD=POST>'); document.write('Enter Colors:<BR>'); var thisCookie = ((document.cookie != "") && (document.cookie != null)); var bg = (thisCookie) ? getCookie("bg") : document.bgColor; var fg = (thisCookie) ? getCookie("fg") : document.fgColor; var link = (thisCookie) ? getCookie("link") : document.linkColor; var alink = (thisCookie) ? getCookie("alink") : document.alinkColor; var vlink = (thisCookie) ? getCookie("vlink") : document.vlinkColor; var url = (thisCookie) ? getCookie("url") : "sample.htm"; document.write('Background: <INPUT TYPE=text NAME="bg" VALUE="' + bg + '" onChange="newCookie(this.name,this.value);"> ... '); document.write('Text: <INPUT TYPE=text NAME="fg" VALUE="' + fg + '"onChange="newCookie(this.name,this.value);"><BR>'); document.write('Link: <INPUT TYPE=text NAME="link" VALUE ="' + link + '" onChange="newCookie(this.name,this.value);"> ...'); document.write('Active Link: <INPUT TYPE=text NAME="alink" VALUE="' + alink + '"onChange="newCookie(this.name,this.value);"><BR>'); document.write('Followed Link: <INPUT TYPE="text" NAME="vlink" VALUE ="' + vlink + '"onChange="newCookie(this.name,this.value);"><BR>'); document.write('Test URL: <INPUT TYPE="text" SIZE=40 NAME="url" VALUE="' + url + '"onChange="newCookie(this.name,this.value); loadPage(this);"><BR>'); document.write('<INPUT TYPE=button VALUE="TEST" onClick="display(this.form);">'); document.write('</FORM>'); // STOP HIDING FROM OTHER BROWSERS --> </SCRIPT> </CENTER> </BODY> </HTML>
In order to use cookies to store the current state for the color tester application, you have to add two new functions (newCookie() and getCookie()) to the header, as well as alter the script which dynamically generates the HTML form in the upper frame.
The other two functions remain unchanged.
The newCookie() function is the simpler of the two new functions. It stores a cookie given a name and value as arguments. The expiryDate variable is a global variable created by taking the current
date, adding 30 days and then converting it to a string in Greenwich Mean Time using toGMTString():
var expires = new Date(); expires.setTime (expires.getTime() + 24 * 60 * 60 * 30 * 1000); var expiryDate = expires.toGMTString();
The getCookie() function is designed to return a particular cookie value. What makes this somewhat complicated is that document.cookie contains a string of name-value pairs separated by a semi-colon
followed by a space.
In order to find the particular value you want and return it, you need to do some relatively sophisticated processing on the document.cookie string.
function getCookie(name) { var cookieFound = false; var start = 0; var end = 0; var cookieString = document.cookie;
You start by declaring the variables. cookieFound is a boolean variable which you use to keep track of whether a name-value pair matching the argument has been found. start and end are used to hold
indexes for the substring() function and cookieString holds the value of document.cookie simply because it is a little easier to readit's my personal preference.
var i = 0; // SCAN THE COOKIE FOR name while (i <= cookieString.length) { start = i; end = start + name.length; if (cookieString.substring(start,end) == name) { cookieFound = true; break; } i++; }
The loop is fairly simple. You use i as the counter. You simply loop through cookieString character by character and check the substring starting at i that is the length of the name you're looking for. If there is a match, you set cookieFound to true
and break out of the loop.
// IS name FOUND? if (cookieFound) { start = end + 1; end = document.cookie.indexOf(";",start); if (end < start) end = document.cookie.length; return document.cookie.substring(start,end); }
If you've found a cookie that matches the name you are looking for, then you set start to the value of end + 1this means you are starting after the equal sign which follows the name. Next you use indexOf() to look for the semi-colon that may be
ending the cookie (unless it is the last cookie in the string). You store the value in end.
We will see more of the string.indexOf() method in Chapter 10 when we discuss the string object in more detail. Basically, the method takes two
argumentsindexOf(string,startIndex) and starts searching for string from the index startIndex. The value returned is the index where string first occurs. If indexOf() doesn't find the character it is looking
for, it returns a value of zero.
Once you have a value for end, you check if a semicolon was found. If not, you know the name-value pair is the last in the list and you can set end to the last character in cookieString, which is the value of cookieString.length.
Finally, you return the substring indicated by start and end.
return ""; }
If you haven't found a cookie, you simply return an empty string.
All of the HTML output is done from a JavaScript script in the body of the document.
<BODY onLoad="loadPage(document.forms[0].url); display(document.forms[0]);"> <CENTER> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS document.write('<H1>The Colour Picker</H1>'); document.write('<FORM METHOD=POST>'); document.write('Enter Colors:<BR>');
Here you output the title and set up the form.
var thisCookie = ((document.cookie != "") && (document.cookie != null)); var bg = (thisCookie) ? getCookie("bg") : document.bgColor; var fg = (thisCookie) ? getCookie("fg") : document.fgColor; var link = (thisCookie) ? getCookie("link") : document.linkColor; var alink = (thisCookie) ? getCookie("alink") : document.alinkColor; var vlink = (thisCookie) ? getCookie("vlink") : document.vlinkColor; var url = (thisCookie) ? getCookie("url") : "sample.htm";
The form elements are built dynamically so that the contents of each field matches any existing cookies. If there is no cookie, then the contents of the color fields will match the defaults for the browser.
You do this by checking if the cookie exists and then using conditional expressions to assign values to several variables which will be used later to build the actual form elements. If cookies exist, you get the values by calling getCookie(). If not,
you use the appropriate color properties of the document object, except in the case of url which you assign to a default URL.
document.write('Background: <INPUT TYPE=text NAME="bg" VALUE="' + bg + '"onChange="newCookie(this.name,this.value);"> ... ') document.write('Text: <INPUT TYPE=text NAME="fg" VALUE="' + fg + '" onChange="newCookie(this.name,this.value);"><BR>'); document.write('Link: <INPUT TYPE=text NAME="link" VALUE ="' + link + '" onChange="newCookie(this.name,this.value);"> ...'); document.write('Active Link: <INPUT TYPE=text NAME="alink" VALUE="' + alink + '"onChange="newCookie(this.name,this.value);"><BR>'); document.write('Followed Link: <INPUT TYPE="text" NAME="vlink" VALUE ="' + vlink + '"onChange="newCookie(this.name,this.value);"><BR>'); document.write('Test URL: <INPUT TYPE="text" SIZE=40 NAME="url" VALUE="' + url + '"onChange="newCookie(this.name,this.value); loadPage(this);"><BR>'); document.write('<INPUT TYPE=button VALUE="TEST" onClick="display(this.form);">');
Once you have calculated the initial values for the form fields, you use document.write() to output the HTML for each field. In each text entry field you use onChange to store a new cookie when the user changes the value of the field. The button calls
display() on a click event.
End of Analysis
As I mentioned earlier, the information stored in the name-value pair of a cookie cannot contain any spaces. This poses something of a limitation because many applications will need to store in
cookies complete phrases or strings containing spaces.
The solution to this lies in encoding the illegal characters in a cookie. Netscape suggests using an encoding scheme such as that used in URL strings.
However, any coding scheme will work. For instance, alternative characters such as % or + could be used for spaces and a similar approach could be taken for other illegal characters, such as semi-colons.
Any script that is going to build cookies using white spaces and other illegal characters or that is going to read similar cookies will need to include methods for dealing with these characters.
JavaScript provides the escape() and unescape() methods which take a string as an argument. escape() returns the string encoded like an URL and
unescape() translates it back from this encoding.
You are now going to use cookies to develop a more sophisticated application.
Most users of the World Wide Web are well aware that the Web can be a great source of the latest news. However, finding just the right news can be a little daunting. The process can take loading a variety of news providers' Web pages to get all the
information you want.
Using a combination of cookies and frames, you are going to build an application that provides news from multiple sources in one browser window.
The concept is simple: The screen is divided into two main sectionsthe left side contains a form for manipulating the application, and the right side contains three frames displaying the news sources selected by the user.
In the control frame, users should be provided with three drop down selection lists to enable them to select the news sources for each of the three frames on the
right. In addition, users should be able to add news sources to the list, as well as delete sources from the list.
Cookies are used to store the list of news sources, as well as the currently selected sources for each frame.
The application is created using scripts in two files: the top-level frameset called news.htm (see Listing 9.2) and the main control file that you will call control.htm (see Listing 9.3). The file wait.htm is a place holder that is displayed when the
three news source frames on the right side are first created (see Listing 9.4).
Input
<!-- SOURCE CODE FOR TOP-LEVEL FRAMESET --> <HTML> <HEAD> <TITLE>Example 9.2</TITLE> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS // // WE NEED TO INCLUDE THE COOKIE FUNCTIONS // // // Cookie Functions - Second Helping (21-Jan-96) // Written by: Bill Dortch, hIdaho Design <bdortch@netw.com> // The following functions are released to the public domain. // // "Internal" function to return the decoded value of a cookie // function getCookieVal (offset) { var endstr = document.cookie.indexOf (";", offset); if (endstr == -1) endstr = document.cookie.length; return unescape(document.cookie.substring(offset, endstr)); } // // Function to return the value of the cookie specified by "name". // function GetCookie (name) { var arg = name + "="; var alen = arg.length; var clen = document.cookie.length; var i = 0; while (i < clen) { var j = i + alen; if (document.cookie.substring(i, j) == arg) return getCookieVal (j); i = document.cookie.indexOf(" ", i) + 1; if (i == 0) break; } return null; } // // Function to create or update a cookie. // function SetCookie (name, value) { var argv = SetCookie.arguments; var argc = SetCookie.arguments.length; var expires = (argc > 2) ? argv[2] : null; var path = (argc > 3) ? argv[3] : null; var domain = (argc > 4) ? argv[4] : null; var secure = (argc > 5) ? argv[5] : false; document.cookie = name + "=" + escape (value) + ((expires == null) ? "" : ("; expires=" + expires.toGMTString())) + ((path == null) ? "" : ("; path=" + path)) + ((domain == null) ? "" : ("; domain=" + domain)) + ((secure == true) ? "; secure" : ""); } // Function to delete a cookie. (Sets expiration date to current date/time) // function DeleteCookie (name) { var exp = new Date(); exp.setTime (exp.getTime() - 1); // This cookie is history var cval = GetCookie (name); document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString(); } // // END OF THE COOKIE FUNCTIONS. OUR SCRIPT STARTS HERE. // function getURL(frame) { var name = GetCookie(frame); return GetCookie(name); } function initialize() { if (GetCookie("sites") == null) { var expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000)); SetCookie("sites","CNN,USA-Today,Yahoo",expiryDate,"/"); SetCookie("CNN","http://www.cnn.com/",expiryDate,"/"); SetCookie("USA-Today","http://www.usatoday.com/",expiryDate,"/"); SetCookie("Yahoo","http://www.yahoo.com/headlines/news/",expiryDate,"/"); SetCookie("frameOne","CNN",expiryDate,"/"); SetCookie("frameTwo","USA-Today",expiryDate,"/"); SetCookie("frameThree","Yahoo",expiryDate,"/"); SetCookie("number","3",expiryDate,"/"); } } initialize(); var frameOne = getURL("frameOne"); var frameTwo = getURL("frameTwo"); var frameThree = getURL("frameThree"); // STOP HIDING HERE --> </script> </HEAD> <FRAMESET COLS="35%,*" onLoad="parent.frames['frameOne'].location=frameOne; parent.frames['frameTwo'].location=frameTwo; parent.frames['frameThree'].location=frameThree;"> <FRAME SRC="control.htm" NAME="control"> <FRAMESET ROWS="33%,33%,*"> <FRAME SRC="wait.htm" NAME="frameOne"> <FRAME SRC="wait.htm" NAME="frameTwo"> <FRAME SRC="wait.htm" NAME="frameThree"> </FRAMESET> </FRAMESET> </HTML>
Input
<!-- SOURCE CODE FOR control.htm --> <HTML> <HEAD> <TITLE>Example 9.2</TITLE> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS // // WE NEED TO INCLUDE THE COOKIE FUNCTIONS // // // Cookie Functions - Second Helping (21-Jan-96) // Written by: Bill Dortch, hIdaho Design <bdortch@netw.com> // The following functions are released to the public domain. // // "Internal" function to return the decoded value of a cookie // function getCookieVal (offset) { var endstr = document.cookie.indexOf (";", offset); if (endstr == -1) endstr = document.cookie.length; return unescape(document.cookie.substring(offset, endstr)); } // // Function to return the value of the cookie specified by "name". // function GetCookie (name) { var arg = name + "="; var alen = arg.length; var clen = document.cookie.length; var i = 0; while (i < clen) { var j = i + alen; if (document.cookie.substring(i, j) == arg) return getCookieVal (j); i = document.cookie.indexOf(" ", i) + 1; if (i == 0) break; } return null; } // // Function to create or update a cookie. // function SetCookie (name, value) { var argv = SetCookie.arguments; var argc = SetCookie.arguments.length; var expires = (argc > 2) ? argv[2] : null; var path = (argc > 3) ? argv[3] : null; var domain = (argc > 4) ? argv[4] : null; var secure = (argc > 5) ? argv[5] : false; document.cookie = name + "=" + escape (value) + ((expires == null) ? "" : ("; expires=" + expires.toGMTString())) + ((path == null) ? "" : ("; path=" + path)) + ((domain == null) ? "" : ("; domain=" + domain)) + ((secure == true) ? "; secure" : ""); } // Function to delete a cookie. (Sets expiration date to current date/time) // function DeleteCookie (name) { var exp = new Date(); exp.setTime (exp.getTime() - 1); // This cookie is history var cval = GetCookie (name); document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString(); } // // END OF THE COOKIE FUNCTIONS. OUR SCRIPT STARTS HERE. // function getURL(frame) { var name = GetCookie(frame); return GetCookie(name); } var expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000)); var number = parseInt(GetCookie("number")); var siteList = GetCookie("sites"); var sites = new createArray(number); sites = extractSites(siteList,number); function createArray(num) { for (var i=1; i <= num; i++) this[i] = ""; this.length = num; } function extractSites(list,num) { var results = new createArray(num); var first = 0; var last = 0; for (var i = 1; i <= num; i ++) { first = (i == 1) ? 0 : last+1; last = (i == num) ? list.length : list.indexOf(",",first+1); results[i] = list.substring(first,last); } return results; } function makeList() { var result = ""; for (var i = 1; i <= number; i++) { result += sites[i]; result += (i == number) ? "" : ","; } return result; } function getList(frame) { var result = '<SELECT NAME="' + frame + '" onChange="loadURL(this);">'; for (var i = 1; i<=number; i++) { result += '<OPTION'; result += (GetCookie(frame) == sites[i]) ? " SELECTED" : ""; result += ">" + sites[i] + "\n"; } result += "</SELECT>"; return result; } function addURL(form) { if ((form.name.value == "") || (form.name.value == null)) { returnl } var name = form.name.value; var url = form.url.value; SetCookie(name,url,expiryDate,"/"); sites[++number] = name; SetCookie("sites",makeList(),expiryDate,"/"); SetCookie("number",number,expiryDate,"/"); window.open("control.htm","control"); } function deleteURL(form) { var name = form.name.value; var gone = false; for (var i=1; i<=number; i++) { if (sites[i] == name) { gone = true; number--; for (var j=i; j<=number; j++) { sites[j] = sites[j+1]; } sites[number+1] = null; break; } } if (gone) { SetCookie("number",number,expiryDate,"/"); SetCookie("sites",makeList(),expiryDate,"/"); var today = new Date(); SetCookie(name,GetCookie(name),today,"/"); } window.open("control.htm","control"); } function loadURL(field) { var frame = field.name; var index = field.selectedIndex; var name = field.options[index].text; var url = GetCookie(name); window.open(url,frame); SetCookie(frame,name,expiryDate,"/"); } // Set things up before building forms var oneList = ""; var twoList = ""; var threeList = ""; oneList = getList("frameOne"); twoList = getList("frameTwo"); threeList = getList("frameThree"); // STOP HIDING HERE --> </script> </HEAD> <BODY> <H1>The<BR>News<BR>Source</H1> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS document.write("<FORM METHOD=POST>"); document.write("Source One:"); document.write(oneList); document.write("</FORM>"); document.write("<FORM METHOD=POST>"); document.write("<BR>"); document.write("Source Two:") document.write(twoList); document.write("</FORM>"); document.write("<FORM METHOD=POST>"); document.write("<BR>"); document.write("Source Three:"); document.write(threeList); document.write("</FORM>"); // STOP HIDING --> </SCRIPT> <BR> <FORM METHOD=POST> Name: <INPUT TYPE="text" NAME="name"> <BR> URL: <INPUT TYPE="text" NAME="url"> <BR> <INPUT TYPE="button" VALUE="Add URL" onClick="addURL(this.form);"> <BR> <INPUT TYPE="button" VALUE="Delete URL" onClick="deleteURL(this.form);"> </FORM> </BODY> </HTML>
Input
<!-- SOURCE CODE FOR wait.htm --> <HTML> <BODY> <H1>Please Wait ...</H1> </BODY> </HTML>
Output
The results should look like Figures 9.3 and 9.4.
Figure 9.3. Using cookies to create a user-oriented custom news sources Web page.
Figure 9.4. Users can add a new URL and automatically update selection lists.
End of Output
Analysis
This program is somewhat complex in that it uses dynamically generated HTML, both in the parent frameset and the main HTML file in the control frame. Both files include Bill Dortch's cookie functions because the scripts in both files need to access the
cookies.
In order to understand how the application works you need to understand how you are using cookies to store all of the information.
You need to keep track of the following information:
All this information is stored in cookies. You keep an optionName=url cookie for each option. In addition, you have three cookies of the form frameName=optionName for each of the frames. The list of all names is stored in the
form sites=optionName1,optionName2,optionName3,etc. where the list is comma-separated and encoded using Bill Dortch's functions. The number of options is stored in the cookie number=numberOfOptions.
In the parent frameset, you have written two functions of your own, as well as building the HTML for the entire frameset using the document.write() method.
function getURL(frame) { var name = GetCookie(frame); return GetCookie(name); }
The getURL() function accepts a frame name as a parameter. It first gets the name of the option for the frame from the appropriate cookie and then gets the URL for that option name with another
call to GetCookie(). The URL is returned.
function initialize() { if (GetCookie("sites") == null) { var expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000)); SetCookie("sites","CNN,USA-Today,Yahoo",expiryDate,"/"); SetCookie("CNN","http://www.cnn.com/",expiryDate,"/"); SetCookie("USA-Today","http://www.usatoday.com/",expiryDate,"/"); SetCookie("Yahoo","http://www.yahoo.com/headlines/news/",expiryDate,"/"); SetCookie("frameOne","CNN",expiryDate,"/"); SetCookie("frameTwo","USA-Today",expiryDate,"/"); SetCookie("frameThree","Yahoo",expiryDate,"/"); SetCookie("number","3",expiryDate,"/"); } }
The initialize() function is the first function called in the script. It simply checks whether any sites are currently stored as cookies using if (GetCookies("sites") == null). If there
are no sites stored, the function defines an initial list of three options and stores all the relevant information in the appropriate cookies.
After calling initialize() to ensure you have sites stored in cookies, you use getURL() to extract the URLs for the three news frames from the cookies.
Finally, the three URLs are loaded into their respective frames by using an onLoad event handler in the FRAMESET tag. This ensures that all the frames are loaded and
ready when you attempt to open the URLs in them. The parent frameset itself divides the right-hand column into three frames where you load wait.htm as a place holder until the various news sources begin to load. In the left-column frame you load
control.htm, which is the main application:
<FRAMESET COLS="35%,*" onLoad="parent.frames['frameOne'].location=frameOne; parent.frames['frameTwo'].location=frameTwo; parent.frames['frameThree'].location=frameThree;"> <FRAME SRC="control.htm" NAME="control"> <FRAMESET ROWS="33%,33%,*"> <FRAME SRC="wait.htm" NAME="frameOne"> <FRAME SRC="wait.htm" NAME="frameTwo"> <FRAME SRC="wait.htm" NAME="frameThree"> </FRAMESET> </FRAMESET>
Once the frameset is built, you use getURL() to extract the URLs for the three news frames from the cookies. Then you use window.open() to open the appropriate URL in each frame.
The file control.htm (refer back to Listing 9.3) is where all the interactive work of the application takes place. The file includes several HTML forms that provide the three drop-down selection listsone for each of the three news framesand
a simple form to add and remove URLs from the list.
In order to implement all of the functionality you need, control.htm includes several additional functions in the header of the file.
var expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000)); var number = parseInt(GetCookie("number")); var siteList = GetCookie("sites"); var sites = new createArray(number); sites = extractSites(siteList,number); function createArray(num) { for (var i=1; i <= num; i++) this[i] = ""; this.length = num; }
You start by setting up all the global variables you are going to need throughout the script. expiryDate is set to one year after the current date and is used whenever you create new cookies or update existing ones.
The variable number contains the number of options in the selection lists and is initially extracted from the appropriate cookie using GetCookie(). siteList is the string of option names from the sites cookie. It is passed to extractSites to fill up the
array sites, which is used throughout the script to reference option names.
You have used a standard createArray() type function to build the array sites.
function extractSites(list,num) { var results = new createArray(num); var first = 0; var last = 0; for (var i = 1; i <= num; i ++) { first = (i == 1) ? 0 : last+1; last = (i == num) ? list.length : list.indexOf(",",first+1); results[i] = list.substring(first,last); } return results; }
The extractSites() function accepts two arguments: the string of comma-separated option names and the number of options in the list. It returns an array of option names.
The real work of the function all takes place in the for loop. The loop is repeated once for each of the options in the list. The first step is to figure out the index of the first character of the next option name in the list. The command first = (i ==
1) ? 0 : last+1; does this by checking if the loop counter is still at the start (equal to 1). If it is, the value of first is zero; otherwise, it is one character past the last character of the previous name, which was stored in last.
The index of the last character is calculated in a similar way using the command
last = (i == num) ? list.length : list.indexOf(",",first+1);
This command checks if the counter is at its final value. If it is, then last should be set to the last character in the string using list.length. Otherwise, the indexOf() method is used to find the next comma in the string.
These two lines are a good example of using conditional expressions to write what would otherwise be a set of bulkier if-else statements.
Once first and last are calculated, then the next entry in the array is set using list.substring() to extract the option name from the string.
Finally, the array results is returned as the result of the function.
function makeList() { var result = ""; for (var i = 1; i <= number; i++) { result += sites[i]; result += (i == number) ? "" : ","; } return result; }
makeList() is used to perform exactly the opposite function of extractSites(). Given the array of option names stored in the global array sites, it returns a single string containing a
comma-separated list of options.
This is done, again, in a single for loop that loops through each entry in the array and adds it to the result variable using the += concatenation operator. The command
result += (i == number) ? "" : ",";
uses a conditional expression to make sure that a comma is only added after an entry from the array if it is not the last entry in the array.
function getList(frame) { var result = '<SELECT NAME="' + frame + '" onChange="loadURL(this);">'; for (var i = 1; i<=number; i++) { result += '<OPTION'; result += (GetCookie(frame) == sites[i]) ? " SELECTED" : ""; result += ">" + sites[i] + "\n"; } result += "</SELECT>"; return result; }
The getList() function is used to build the drop-down selection menus for each of the three frames based on the current cookie settings. The function accepts a frame name as an argument and
returns, as a single string, the SELECT HTML container, complete with all options set and the appropriate one pre-selected.
The function uses the frame argument to correctly set the NAME attribute of the SELECT tag. The onChange event handler contains the function call to load a new URL when the user chooses a new option.
The for loop in the function builds the list of options. It does this by looping through each option and adding an OPTION tag to the string. It then uses sites[i] to add the name to be displayed
for each option. The command
result += (GetCookie(frame) == sites[i]) ? " SELECTED" : "";
checks if the current option is stored in the cookie for the frame. If it is, then the SELECTED attribute is added to the OPTION tag so that that option will appear selected initially.
function addURL(form) { if ((form.name.value == "") || (form.name.value == null)) { returnl } var name = form.name.value; var url = form.url.value; SetCookie(name,url,expiryDate,"/"); sites[++number] = name; SetCookie("sites",makeList(),expiryDate,"/"); SetCookie("number",number,expiryDate,"/"); window.open("control.htm","control"); }
addURL() is invoked when the user clicks on the Add URL button near the bottom of the frame. It adds a new entry to the selection lists for each frame.
Given the form object as an argument, the addURL() function extracts the name and URL from the fields in the form and then sets the cookie for that name, as well as
updating the sites array and storing new sites and number cookies.
Notice the use of the unary increment operator in sites[++number] = name; to increment the value of the number variable before using it as an index to sites. This effectively adds a new entry to
sites before the next line:
SetCookie("sites",makeList(),expiryDate,"/");
This line updates the sites cookie by calling makeList() to build a comma-separate string out of the updated array.
Finally, the function reloads control.htm into control to rebuild the selection menus.
function deleteURL(form) { var name = form.name.value; var gone = false; for (var i=1; i<=number; i++) { if (sites[i] == name) { gone = true; number--; for (var j=i; j<=number; j++) { sites[j] = sites[j+1]; } sites[number+1] = null; break; } } if (gone) { SetCookie("number",number,expiryDate,"/"); SetCookie("sites",makeList(),expiryDate,"/"); var today = new Date(); SetCookie(name,GetCookie(name),today,"/"); } window.open("control.htm","control"); }
In this excerpt, the deleteURL() function removes an entry from the list of options.
This process is actually a bit more complicated than adding a new URL to the list because a little bit of work needs to be done to remove an entry from the middle of the array and then close up the hole that this creates in the array.
The function uses a for loop to move through the array. The if statement checks if the current entry matches the one you want to delete.
If there is a match, then the work begins. gone is set to true so that later in the function you know a match was foundafter all, the user could incorrectly type the name of the entry to delete. Next, number is decreased to reflect the fact that
the number of entries in the list will decrease by one. Another for loop is used to count from the current index of the entry you are deleting to the new value of number (that is, one before the current last entry in the array).
The command sites[j] = sites[j+1]; copies the array entry immediately following the current entry into the current entry. In this way, you fill in the hole created by removing an entry.
Finally, after the for loop finishes, you set the previous last entry to the null value with sites[number+1] = null;.
Once you finish the for loop, if you have found an entry to delete, you update the sites and number cookies just as you did in addURL() and then you remove the cookie for the deleted entry by updating it with an expiry date equal to the current date and
time.
function loadURL(field) { var frame = field.name; var url = GetURL(frame); open(url,frame); SetCookie(frame,name,expiryDate,"/"); }
The loadURL() function is invoked when the user changes the value of one of the selection lists. It receives the field object for the selection list as an argument.
Based on this information, it can extract the frame name, which is actually the NAME value of the selection element. Using this information, you can call getURL() to get the URL for that frame.
Once you have this information, you can open the URL in the frame and then update the cookie for that frame to reflect the new selection by the user.
// Set things up before building forms var oneList = ""; var twoList = ""; var threeList = ""; oneList = getList("frameOne"); twoList = getList("frameTwo"); threeList = getList("frameThree");
The last thing you do in the header of the document is set up three variables containing the selection lists for the three different news source frames by calling getList().
document.write("<FORM METHOD=POST>"); document.write("Source One:"); document.write(oneList); document.write("</FORM>"); document.write("<FORM METHOD=POST>"); document.write("<BR>"); document.write("Source Two:") document.write(twoList); document.write("</FORM>"); document.write("<FORM METHOD=POST>"); document.write("<BR>"); document.write("Source Three:"); document.write(threeList); document.write("</FORM>");
In the body of the document, you use scripts to build each of the three drop-down selection lists. Each list is a separate form, but could just as easily have been a single form.
<FORM METHOD=POST> Name: <INPUT TYPE="text" NAME="name"> <BR> URL: <INPUT TYPE="text" NAME="url"> <BR> <INPUT TYPE="button" VALUE="Add URL" onClick="addURL(this.form);"> <BR> <INPUT TYPE="button" VALUE="Delete URL" onClick="deleteURL(this.form);"> </FORM>
The last element in the document is the form used to add and delete entries from the list of options. The form contains two text entry fields and two buttons that invoke either addURL() or deleteURL() in their onClick event handlers.
As mentioned at the beginning of this chapter, the navigator object makes information about the current version of Navigator available to scripts.
The navigator object is currently one of the most poorly documented objects in JavaScript. Although there are indications that the available properties, and perhaps
methods, will expand with future versions of JavaScript, today it offers four properties that are outlined in Table 9.2.
Name |
Description |
appName |
The name of the application in which the page is loaded represented as a string (i.e. "Netscape") |
appVersion |
The version information of the current browser as a string in the form "2.0 (Win16; I)" where 2.0 is the version number, Win16 is the platform and I indicates the international version (as opposed to U for the domestic version). |
appCodeName |
The code name of the current browser (i.e. "Mozilla"). |
|
The user agent for the current browser as a string in the form "Mozilla/ 2.0 (Win16; I)". |
In order to understand the significance of this information, it is important to understand the concept of the user agent. In the initial communication between the client and the server during an HTTP request, the browser
sends the user agent string to the server. That information becomes available on the server for a number of uses, including processing by CGI scripts or for delivering specific versions of the pages based on the nature of the client browser.
The user agent information has two parts separated by a slash: a code name for the browser and the version information for the browser. This is the information stored in the appCodeName and appVersion properties. For instance, "TOME" would
represent the current version of Navigator 2.0 for Windows 3.1.
At first, it may seem like this information serves little practical usebut it can be very useful.
For instance, during the beta development of the Navigator 2.0 browser, releases of new versions of the beta were quite frequent. Each version supported new features of JavaScript and fixed problems with earlier implementations, which sometimes created
incompatibilities.
Many page authors are now using the properties of the navigator object to check if the browsers being used will support the features used in the script. If not, users are alerted so that they don't try to run the script and get errorsor even find
Netscape, or their PC, crashing.
For example, if a page should only be run with Navigator 2.0.0 beta 6a on any platform and beta 6a is the latest version, the HTML file should look like this:
<HTML> <HEAD> <TITLE>navigator Example</TITLE> <SCRIPT LANGUAGE="JavaScript"> <!-- HIDE FROM OTHER BROWSERS function checkBrowser() { if ((navigator.appVersion.substring(0,6) != "2.0b6a") && (navigator.appName != "Netscape)) alert("Please use version 2.0b6a of the Netscape Navigator web browser with this page."); } Rest of script // STOP HIDING FROM OTHER BROWSERS --> </SCRIPT> </HEAD> <BODY onLoad="checkBrowser();"> HTML code </BODY> </HTML>
Similarly, in the future, if different browsers support JavaScript, they may offer different features or additional objects and the use of the navigator object can help script authors ensure that their scripts run on the largest number of browsers while
also taking advantage of the unique features of each.
In this chapter, you learned about an extremely useful feature of JavaScript: the cookie property.
Using the cookie property, scripts can set and read cookies which store state information in the client browser. Using cookies, you can retain information between sessions to produce applications that outlive the currently loaded document and even the
current browser session.
Once again, Bill Dortch has provided the JavaScript community with a set of freely-available functions that make setting and retrieving cookies easier.
In addition to cookies, you took a look at the navigator object and how it can be used to ensure that users are using the appropriate browser for your scripts.
In the next chapter, we take a look at a variety of objects and features of JavaScript which we haven't covered in detail yet, including the string object, the Math object, and the history object.
Command/Extension |
Type |
Description |
Set-Cookie |
HTTP header |
Sets cookies in the client browserpart of an HTTP response header |
Cookie |
HTTP header |
Returns cookies to the serverpart of an HTTP request header |
expires |
Set-Cookie attribute |
Indicates the expiry date for a cookie (in GMT) |
path |
Set-Cookie attribute |
Used to set the path for files applicable to a cookie |
domain |
Set-Cookie attribute |
Used to set the domain for files applicable to a cookie |
secure |
Set-Cookie attribute |
Specifies that a cookie should only be transmitted on secure links |
cookie |
JavaScript property |
String containing the value of cookies for the current document |
indexOf() |
JavaScript method |
A method of the string object which returns the index of the next occurrence of a substring in a string |
escape() |
JavaScript method |
Encodes a string using URL encoding |
unescape() |
JavaScript method |
Decodes a string encoded using URL encoding |
appName |
JavaScript property |
The name of the browser application as a string |
appVersion |
JavaScript property |
The version number and platform of the browser as a string |
appCodeName |
JavaScript property |
The code name of the browser as a string |
userAgent |
JavaScript property |
The user agent string for the browser |
link() |
JavaScript method |
Method of the string object which encloses the string in an <A> HTML tag |
|
|
Method of the string object which sets the HTML font color for the string |
Q: How can I be sure that other people's applications and scripts
won't overwrite the cookies which I have created?
A: If you are setting cookies with the specific path of your
document, then it is possible for other documents to create
cookies with the same name, domain, and path. However, these
cookies do not overwrite your cookies. Instead,
document.cookies will contain two entries with the same name,
but different values. Only a script in the file in the same
original location can overwrite cookies created by that file.
Q: Can I rely on cookies to store information vital to my
application between sessions?
A: Not really. While most users will not delete their browsers or
change browsers between sessionswhich would mean the loss
of cookie informationthere are too many variables to be
sure that your cookies will still be present when the user
next returns to your page. For instance, cookies could have
been added exceeding the limits, and your cookies could be the
ones which get deleted. It is generally good to write your
scripts in such a way that if the cookies you are looking for
no longer exist, you can perform alternate actions and
recreate the cookies.
Q: Why does the navigator object have both the appName and
appCodeName properties? They seem to be pretty much the same
thing in different forms.
A: While it's true that generally you can glean the same
information from both properties (such as Mozilla for
Netscape, and so on), it is not always the case that you will
know that the code name for Lynx or the application name for a
less well-known browser. Having both properties, you have the
maximum information available to determine the client browser.
The following script achieves the desired effect. The output looks like Figures 9.5 and 9.6.
<HTML>
<HEAD>
<TITLE>Exercise 9.3</TITLE>
<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS
//
// WE NEED TO INCLUDE THE COOKIE FUNCTIONS
//
//
// Cookie Functions - Second Helping (21-Jan-96)
// Written by: Bill Dortch, hIdaho Design <bdortch@netw.com>
// The following functions are released to the public domain.
//
// "Internal" function to return the decoded value of a cookie
//
function getCookieVal (offset) {
var endstr = document.cookie.indexOf (";", offset);
if (endstr == -1)
endstr = document.cookie.length;
return unescape(document.cookie.substring(offset, endstr));
}
//
// Function to return the value of the cookie specified by "name".
//
function GetCookie (name) {
var arg = name + "=";
var alen = arg.length;
var clen = document.cookie.length;
var i = 0;
while (i < clen) {
var j = i + alen;
if (document.cookie.substring(i, j) == arg)
return getCookieVal (j);
i = document.cookie.indexOf(" ", i) + 1;
if (i == 0) break;
}
return null;
}
//
// Function to create or update a cookie.
//
function SetCookie (name, value) {
var argv = SetCookie.arguments;
var argc = SetCookie.arguments.length;
var expires = (argc > 2) ? argv[2] : null;
var path = (argc > 3) ? argv[3] : null;
var domain = (argc > 4) ? argv[4] : null;
var secure = (argc > 5) ? argv[5] : false;
document.cookie = name + "=" + escape (value) +
((expires == null) ? "" : ("; expires=" + expires.toGMTString())) +
((path == null) ? "" : ("; path=" + path)) +
((domain == null) ? "" : ("; domain=" + domain)) +
((secure == true) ? "; secure" : "");
}
// Function to delete a cookie. (Sets expiration date to current date/time)
//
function DeleteCookie (name) {
var exp = new Date();
exp.setTime (exp.getTime() - 1); // This cookie is history
var cval = GetCookie (name);
document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString();
}
//
// END OF THE COOKIE FUNCTIONS. OUR SCRIPT STARTS HERE.
//
var expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (30 * 24 * 60 * 60 * 1000));
var food = new createArray(3);
food[1] = "Beets";
food[2] = "Jello-Pudding";
food[3] = "Cockroaches";
function createArray(num) {
this.length = num;
for (var i = 1; i <= num; i++)
this[i] = "";
}
function listArray(stuff) {
var result = "";
for (var i = 1; i <= stuff.length; i++)
result += i + ". " + stuff[i] + "/";
return result;
}
var color = "";
var favFood = "";
function initialize() {
if (GetCookie("color") == null) {
color = prompt("Enter your favourite Netscape color.","A Color");
SetCookie("color",color,expiryDate);
var foodNum = prompt("Food: - " + listArray(food),"0");
favFood = food[foodNum];
SetCookie("food",favFood,expiryDate);
} else {
color = GetCookie("color");
SetCookie("color",color,expiryDate);
favFood = GetCookie("food");
SetCookie("food",favFood,expiryDate);
}
}
// STOP HIDING HERE -->
</SCRIPT>
</HEAD>
<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS
initialize();
document.write('<BODY BGCOLOR="' + color + '">');
document.write("<CENTER>")
document.write('<IMG SRC="' + favFood + '.gif">');
// STOP HIDING -->
</SCRIPT>
<H1>Welcome to the favorite food and color page</H1>
</CENTER>
</BODY>
</HTML>
Figure 9.5. The script prompts the first-time visitor for the color and food information.
Figure 9.6. On subsequent visits, information stored in the cookies automatically formats the page.
The logic behind this script is rather simple, but it provides an example of how to rebuild cookies after they expire and how to keep cookies current when it is relevant to do so.
The createArray() and listArray() functions should be obvious. listArray() returns a string containing all the items in an array numerically listed by their index numbers in the array. This is used to prompt users for their food choices.
The initialize() function is where all the work takes place. The function checks for the existence of the color cookie. If the cookie doesn't exist, the program prompts users for the food and color information and stores it in the appropriate cookies,
as well as setting the color and favFood global variables for use in the body of the document.
If the cookies exist, the values are loaded into color and favFood and then the cookies are updated so that their expiry dates gets reset to 30 days into the future.
In the body of the document, a script is used to set the BGCOLOR attribute of the BODY tag and the SRC attribute of the IMG tag based on the color and favFood variables.