reptile7's JavaScript blog
Sunday, October 24, 2010
 
More on Query String Migration
Blog Entry #194

We return now to our discussion of the "Limitations" section of HTML Goodies' "A Quick Tutorial on JavaScript Variable Passing".

More than two name/value pairs

Let's add some more controls to the jspass1.html form, shall we? Between the FirstName/LastName text inputs and the submit button, let's insert a selection list and a set of radio buttons:

In what month were you born?

In what month were you born?
<select name="Month">
<option>January</option>
<option>February</option>
<!-- Etc. -->


At a Chinese restaurant, you are most likely to order:
Beef with Broccoli
Sweet and Sour Pork
General Tso's Chicken
Shrimp with Garlic Sauce

At a Chinese restaurant, you are most likely to order:<br>
<input type="radio" name="Entree" value="Beef with Broccoli"> Beef with Broccoli<br>
<input type="radio" name="Entree" value="Sweet and Sour Pork"> Sweet and Sour Pork<br>
<!-- Etc. -->


Next, let's fill out and submit the form.
(1-2) Suppose we respectively enter Joe and Burns into the FirstName and LastName fields as we did in the previous post.
(3) We select the October option of the Month menu.
(4) We choose the Beef with Broccoli Chinese Entree.
(5) Finally, we click the button to go to the jspass2.html page.

At the jspass2.html page, here's the query string that we see at the end of the URL in the browser window's address bar:

?FirstName=Joe&LastName=Burns&Month=October&Entree=Beef+with+Broccoli

(The value attribute of the option element defaults to element content if it is not specified.)

Now, how do we extract the user's jspass1.html inputs out of this thing? Joe's delineate( ) function will return the Joe value if we switch its str.lastIndexOf("&") operation to a str.indexOf("&") operation; verbatim, Joe's delineate2( ) function will return the final Beef+with+Broccoli value. But what about the Burns and October values, huh?

Split it

The final paragraph of the "Limitations" section reads in part:
I saw a suggestion on doing this by setting the answers to an array. The array method allows for endless variables being passed, but older browsers would not be able to use it properly. My version allows for the most number of browsers to be able to play with the code but only uses two variables. You decide.
In previous editions of the tutorial - as late as 08 June 2008 - the words "answers to an array" in the above quote linked to a "Passing Data Between Pages via URLs" article authored by JavaScript guru Danny Goodman. The article's "Listing 1" details the "array method" that Joe is talking about; in particular, here's the part of the Listing 1 code that we're interested in (I've modified it slightly):
var results = new Array( );
if (location.search) {
	// Unescape and strip away leading question mark.
	var input = unescape(location.search.substring(1));
	input = input.replace(/\+/g, " ");
	// Divide long string into array of name/value pairs.
	var srchArray = input.split("&");
	var tempArray = new Array( );
	for (i = 0; i < srchArray.length; i++) {
		// Divide each name/value pair temporarily into a two-entry array.
		tempArray = srchArray[i].split("=");
		// Use temp array values as index identifier and value.
		results[tempArray[0]] = tempArray[1]; } }
/* These statements are functionized in Listing 1 but they can also be run as top-level code without incident. */
• In Listing 1, the preceding code is prefaced by some browser-sniffing statements that are no longer relevant in this day and age; the statements were put there to weed out users without support for the split( ) method of the String object, which was implemented in Netscape 3 and MSIE 4.

The search property of the Location object returns the query string portion of the current URL, including the question mark, which is removed for the input string by the location.search.substring(1) operation.

• We briefly discussed the unescape( ) function and its deprecated status in the previous post. I've kept the unescape( ) line because
(a) had we chosen General Tso's Chicken for our Entree, we'd need it to unescape the %27 to which the apostrophe would be percent-encoded, and
(b) modern browsers all support the unescape( ) function - its use throws no errors or warnings as of this writing.
(But if you insist on alternatively working with the encodeURI( )/encodeURIComponent( )/decodeURI( )/decodeURIComponent( ) functions and a homemade query string, then more power to you.)

• The input.replace(/\+/g, " ") operation replaces Beef+with+Broccoli's plus signs with spaces as described in the previous post.

• The above code uses the aforementioned split( ) method to fragment the post-? query string into its constituent parts*, which are organized as an associative array having a results identifier, as though we had written:

var results = new Object( );
results["FirstName"] = "Joe";
results["LastName"] = "Burns";
results["Month"] = "October";
results["Entree"] = "Beef with Broccoli";


(*As we saw in the "Extracting the value value, take 2" section of Blog Entry #83, Microsoft uses this approach to retrieve values from a document.cookie string.)

We can use JavaScript's for...in statement to iterate over the results array and print out its data on the jspass2.html page:

for (var item in results) document.write(item + " is: " + results[item] + "<br>");
/* Output:
FirstName is: Joe
LastName is: Burns
Month is: October
Entree is: Beef with Broccoli */


In each for...in iteration, a results index string is assigned to the variable item, i.e., item is set to FirstName in the first iteration, to LastName in the second iteration, etc. Each item index and its corresponding value, results[item], are then written to the page in a normal manner by a document.write( ) command.

Walking our way across the URL

The array approach of the preceding subsection works very nicely with all of the GUI browsers on my computer (I did encounter one glitch in testing it: the replace( ) operation threw an error with Netscape 3.04, which lacks support for regular expressions). Nonetheless, it's not necessary to use arrays to get our hands on all four jspass1.html form values, as Joe's String method approach is also up to the task of doing so.

In the delineate( ) function, Joe uses the indexOf( ) method to delimit the FirstName field's value in str, the page URL string. (OK, he actually uses the lastIndexOf( ) method to nail down the theright index, but as noted earlier, he should use indexOf( ) for this purpose.) We can similarly use the indexOf( ) method to delimit the LastName field's value in our longer-form query string if we set the second, fromIndex indexOf( ) parameter to the index one past that of the first & in str, i.e., if we begin searching for the second = and the second & in str from the L of LastName (the first character of the second name in the query string):

name2char1 = theright + 1;
theleft2 = str.indexOf("=", name2char1) + 1; // Locates the B of Burns in str
theright2 = str.indexOf("&", name2char1); // Locates the second & in str
document.write("<p>LastName is: " + str.substring(theleft2, theright2)); // Writes LastName is: Burns to the page


Analogous commands can be written to extract the Month value (and subsequent values up to the penultimate value if we were dealing with a larger form) from str.

More than two pages

So, if we wanted to take the jspass1.html form data to a third, jspass3.html page (or beyond), how would we do that? In the tutorial's concluding "That's That" section, Joe says:
You can carry the same info across many pages, but they all must be in order. If you take these variables to yet a third page, all this coding must be redone on the new page. The variable does not carry across pages. The text in the URL carries. Once it gets to the new page, it has to be assigned a new variable...each time.
It is of course true that the jspass2.html data-extraction code must also be placed in the jspass3.html source for the jspass3.html page to make use of the jspass1.html form data. It is also true that the various jspass2.html data-extraction variables (text, str, etc.) will not be carried from jspass2.html to jspass3.html - indeed, we wouldn't need to be doing any of this if a given Web page recognized the variables of its referring page.

However, Joe does not discuss how to take the jspass2.html URL query string to a jspass3.html page. Looking at the tutorial's "Placing the Value" section, my guess is that he would equip the name="bull" form with an action="jspass3.html" attribute and then add a submit button to the form, and thus link jspass2.html to jspass3.html as jspass1.html is linked to jspass2.html. This will work, but the aforecited "Passing Data Between Pages via URLs" article (cf. the "The Search String Is Always Passed From Page to Page" subsection of the article's "The Session Preferences Example" section) describes a better approach to query string migration: use a link that appends location.search to the jspass3.html target anchor URL.

<a href="jspass3.html" onclick="location.href='jspass3.html' + location.search; return false;">Link to jspass3.html</a>

Per the onclick event handler, JavaScript-enabled users will be taken to a 'jspass3.html' + location.search page; per the href attribute, users without JavaScript support or who have disabled JavaScript will also be taken to the jspass3.html page but the jspass2.html location.search query string will not come along for the ride.

And why stop at three pages? Suppose that we have a ten-page Web site - jspass1.html, jspass2.html, ... jspass10.html - and we want the jspass1.html form data to be available to all ten pages. On each page we could have a link menu that links and passes the page's query string to any other page of the site via one of the mechanisms above. In using a form and the HTTP get transaction to move a query string around, it might initially seem that we are limited to sending the form's data to the page specified by the form's action attribute, but this is not so; we can easily overwrite the action value, and thus send the data anywhere we like, as follows:
function passValues2(destination) {
	document.forms[i].action = destination;
	document.forms[i].submit( ); }
...
<a href="jspass#.html" onclick="passValues2('jspass#.html'); return false;">Link to jspass#.html</a>
Backing up

Consider a user who begins touring our ten-page site at jspass1.html, fills out the jspass1.html form, submits the form data and goes to jspass5.html, and then returns to jspass1.html via the browser's Back button or History menu/pane as opposed to using jspass5.html's link to jspass1.html. In the second paragraph of the "Limitations" section, Joe warns, Returning and backing up can harm the information being carried, and Joe is correct in this case in the sense that the jspass5.html query string will not be carried back to jspass1.html (jspass1.html didn't have a query string to begin with, after all). But it's not as though the user's data just goes up in smoke, either; back at the jspass1.html page it'll still be there, loaded into the form's controls, from which it can be extracted to do whatever it is that you want to do with it.

The "Passing Data Between Pages via URLs" article sports a Session Preferences example that would be worth dissecting, but I think I've had enough of this topic for the time being.

In the next entry, we'll take stock of where we are in the Beyond HTML : JavaScript sector and then pick another sector tutorial to check over, probably "How to Populate Fields from New Windows Using JavaScript".

reptile7

Tuesday, October 12, 2010
 
Check Your Values at the URL
Blog Entry #193

Previously we have discussed two Beyond HTML : JavaScript sector tutorials whose scripts set and act on an HTTP cookie:
(1) "So, You Want To Set A Cookie, Huh?", which we checked over in Blog Entries #144 and #145, and
(2) "So You Want A Cookie Counter, Huh?", which was covered in Blog Entry #185.
The "Cookie Counter" script's cookie is not really a typical cookie in that it is only meant for one Web page and contains no user-specific data. In contrast and more normally, the "Set A Cookie" script's cookie is meant to be carried across Web pages and its value attribute is set via user input into a text box.

Today we will take up the sector's "A Quick Tutorial on JavaScript Variable Passing", which complements the "Set A Cookie" tutorial by offering a non-cookie way to carry information across Web pages. In sum, the "JavaScript Variable Passing" tutorial code
(a) sets up a link between two pages - let's say from jspass1.html to jspass2.html - via a form and its action attribute;
(b) uses the form's method="get" attribute to load the form's data - i.e., its control values, which are specified by the user - into a query string tacked onto the action URL; and
(c) extracts the user's inputs at the action destination via standard String object methods.

Let's begin our analysis with a look at the jspass1.html form code:

<form method="link" action="jspass2.html">
<b>Type your first name:</b> <input type="text" name="FirstName"><br>
<b>Type your last name:</b> <input type="text" name="LastName"><br>
<input type="submit" value="Click and See">
</form>


The form's code contains one 'red flag', that being the form element's method="link" attribute. The method attribute of the form element has two valid values: get and post; link isn't one of them. Proprietarily, neither Netscape nor Microsoft lists link as a possible value for the method attribute. The HTML 4.01 Specification's "Notes on invalid documents" section stipulates, If a user agent encounters an attribute value it doesn't recognize, it should use the default attribute value. Get is the default method value and thus method="link" should be functionally equivalent to method="get", and this is indeed what I see on my computer.

And what does method="get" do for us? The above form holds a pair of labeled text boxes plus a submit button. Let's say we enter Joe into the FirstName field and Burns into the LastName field, and then click the button. The browser subsequently appends to the action URL a query string comprising
(a) a question mark (?) followed by
(b) a FirstName=Joe&LastName=Burns string encoding the name/value data for the form's successful controls (the submit button is not a successful control because it doesn't have a name attribute); an equals sign (=) separates each name from its value whereas an ampersand (&) separates the two name/value pairs.
This is all discussed in more detail in the HTML 4.01 Specification's "Form submission" section. Finally, the browser sends off a request for the jspass2.html?FirstName=Joe&LastName=Burns resource, which in practice simply takes us to the jspass2.html page - as Joe puts it, the query string doesn't harm the movement to another page - even though the full URL of the page we arrive at does include the query string as is shown by the browser window's address bar at that page.

At this point, we have accessed the jspass2.html?FirstName=Joe&LastName=Burns resource and are now ready to fish the jspass1.html form values out of its URL. Here's the code Joe uses to grab the value of the FirstName field:
<form name="joe">
<input type="hidden" name="burns">
</form>

<script type="text/javascript">
var locate = window.location;
document.joe.burns.value = locate;
var text = document.joe.burns.value;

function delineate(str) {
	theleft = str.indexOf("=") + 1;
	theright = str.lastIndexOf("&");
	return str.substring(theleft, theright); }

document.write("First Name is " + delineate(text));
</script>
Deconstruction

The location property of the window object is given a locate identifier. However, window.location is not merely a property but is also a reference for the client-side Location object. The Location object stringifies to the current page's full URL via its toString( ) method but is not itself a string - typeof window.location returns object - and calling a String object method on a Location object will throw an error. Joe deals with this situation by assigning locate to the value of a name="burns" hidden control in a name="joe" form. The value property of the client-side Hidden object has a string data type and therefore the document.joe.burns.value = locate; assignment implicitly calls locate.toString( ), which returns the http://hostname/[pathname/]jspass2.html?FirstName=Joe&LastName=Burns page URL as a string. As the burns value, the URL string is subsequently assigned to a text variable.

In the tutorial comment thread, "openstud" correctly points out that a

var locate = window.location.toString( );

statement would give a stringified locate without the need for a hidden field and on which String object methods could be called; even more straightforwardly, a location.href expression would directly return the full URL as a string.

Anyway, the text string is next passed to a delineate( ) function, whose call is nested in a document.write( ) command; the delineate( ) declaration gives text a(n) str identifier. Here's the delineate( ) play-by-play:
(1) The index of the starting J of Joe in the str string is located and assigned to a theleft variable.
(2) On the following line, the index of str's & is located and assigned to a theright variable. Joe uses the lastIndexOf( ) method for this operation, which is OK because there's only one & in str, but the indexOf( ) method is what we should really be using here (and would have to use if the query string held more than two name/value pairs).
(3) Finally, str.substring(theleft, theright) (Joe) is returned to the delineate( ) function call and First Name is Joe is written to the page.

The value of the LastName field is extracted via an analogous delineate2( ) function:
function delineate2(str) {
	point = str.lastIndexOf("=");
	return str.substring(point + 1, str.length); }
document.write("Last Name is " + delineate2(text));
The tutorial's "Placing the Value" section shows how to load the delineate( ) and delineate2( ) returns into text boxes - I trust I don't need to go over this code for you. Of course, the DOM today allows us to load these returns into any can-contain-#PCDATA element (back when the tutorial was written (late 1999/early 2000), MSIE users could have done this via a document.all("elementID") or document.getElementById("elementID") command, but Netscape users were out of luck in this regard).

Limitations, or not

The tutorial introduction features a script demo that works fine as far as it goes. However, the tutorial also sports a "Limitations" section in which Joe identifies three limitations of his value-passing code:

(1) To begin with, the information is not saved after each surfing like it is with a cookie. True enough. Joe continues, In order for this to work, you must ensure the user moves in a linear fashion. The "linear" part is not right, but moving a query string from page to page does require
(a) a site to provide its own navigation and
(b) the user to use that navigation.

(2) Next, the way I have this set up, you can only transfer two [values] from page to page. In fact, Joe's code is easily retooled so as to carry multiple values across pages.

(3) Also, the method I have written here isn't very friendly to spaces. If the user puts a space in either of their two text boxes, that space will be replaced by a plus sign.

We'll take these limitations on in order of increasing difficulty.

Spaces to plus signs to spaces

Suppose that a Mary Ellen Le Blanc visits the jspass1.html page. She types Mary Ellen into the FirstName field and Le Blanc into the LastName field, and then clicks the button. At the jspass2.html page, she'll see a http://hostname/[pathname/]jspass2.html?FirstName=Mary+Ellen&LastName=Le+Blanc URL in the browser window's address bar and on the jspass2.html page itself she'll see:

First Name is Mary+Ellen
Last Name is Le+Blanc
First Name:
Last Name:

For an HTTP get transaction, space characters in form name/value data are conventionally escaped to plus signs (cf. the aforecited HTML 4.01 "Form submission" section) - spaces can be escaped in other ways but for now we're dealing with +s. URLs cannot validly contain literal space characters - the use of spaces in URLs is "unsafe" for reasons detailed in Section 2.2 of RFC 1738 - and thus the +s in the address bar URL are there to stay. As for the jspass2.html page display, however, we can easily convert the page URL's +s back to spaces via:

var text = location.href.replace(/\+/g, " ");

The replace( ) method of the String object is documented here. The above command runs through location.href and replaces each instance of /\+/, a regular expression representation of a literal + character, with a space. The resulting string is assigned to text, which can be fed to delineate( ) and delineate2( ) as in the original script.

Regular expression notes:
+ is a regular expression metacharacter - it's a quantifier signifying one or more occurrences of its preceding operand - and must be literalized via preceding it with a backslash (a /+/ pattern throws a syntax error, BTW).
• Without the g flag, only the first + in location.href will be replaced by a space.

Lagniappe for our diacritical friends

The W3C notes, The get method restricts form data set values to ASCII characters. Hmmm... Suppose that a Günther Schmidt visits the jspass1.html page. He types Günther into the FirstName field and Schmidt into the LastName field, and then clicks the button. What happens?

The ü in Günther lies outside the ASCII range but is still a "Latin-1" character. For the get query string, the ü will be percent-encoded, i.e., replaced by its two-digit hexadecimal ISO-8859-1 code position - FC in this case - preceded by a percent sign (%). Accordingly, at the jspass2.html page Günther will see a
?FirstName=G%FCnther&LastName=Schmidt query string
at the end of the URL in the browser window's address bar and the
document.write("First Name is " + delineate(text)); output on the page will be
First Name is G%FCnther.

How might we convert %FC back to ü at the jspass2.html page? Classical JavaScript offered a top-level unescape( ) function that would allow us to do just that:

var text = unescape(location.href);

Unfortunately, Netscape deprecated unescape( ) and its complementing escape( ) function for JavaScript 1.5; superseding unescape( ) and escape( ) is a set of four functions - decodeURI( ), decodeURIComponent( ), encodeURI( ), and encodeURIComponent( ) - that decode/encode URIs or the components thereof in accord with the UTF-8 character encoding scheme. It gets worse: the decodeURI( ) and decodeURIComponent( ) functions cannot be used to decode the http://hostname/[pathname/]jspass2.html?FirstName=G%FCnther&LastName=Schmidt URL because %FC by itself is an illegal UTF-8 escape sequence. (Cf. the "Codepage layout" table on Wikipedia's UTF-8 page: %FC is only legit if it appears at the start of a UTF-8-encoded 6-byte sequence. In corroboration, I find that decodeURIComponent("?FirstName=G%FCnther&LastName=Schmidt") throws a "malformed URI sequence" error when using Firefox.)

Now what? Mozilla's decodeURI( ) page states that decodeURI( ) [d]ecodes a Uniform Resource Identifier (URI) previously created by encodeURI( ) or by a similar routine. It follows that if we want to use decodeURI( ) or decodeURIComponent( ) to decode our 'Günther URL' at the jspass2.html page, we're going to have to circumvent the jspass1.html page's get method code in favor of a DIY approach that creates, UTF-8-encodes, and links to that URL from scratch. And this is not so difficult to do: simply add to the jspass1.html form a

<input type="button" value="UTF-8 Click and See" onclick="passValues(this.form.FirstName, this.form.LastName);" />

push button that when clicked calls the following function:
function passValues(field1, field2) {
	qString = "?" + field1.name + "=" +  field1.value + "&" + field2.name + "=" +  field2.value;
	location.href = encodeURI("jspass2.html" + qString); }
ü is UTF-8-encoded to %C3%BC, which is derived from a 11000011 10111100 bit pattern that is itself derived from ü's 11111100 ISO-8859-1 bit pattern. On the jspass2.html side, the %C3%BC in our homemade query string can be converted back to ü via:

var text = decodeURI(location.href);

Interestingly, upon UTF-8-encoding the URL, some browsers (Firefox, Safari, Chrome) display the unescaped ü in the address bar at the jspass2.html page whereas others (Opera, Netscape 9) display the %C3%BC escape sequence.

We'll sort out the remaining two code limitations in the following entry.

reptile7


Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)