reptile7's JavaScript blog
Saturday, July 29, 2006
See Me, Feel Me, Touch Me, Validate Me
Blog Entry #46

Suppose you maintain a Web site that solicits input from visitors via one or more forms. We discussed the determination of form control input in Blog Entry #17, but still, how do you ensure that a user interacts with your form(s) as you desire? For example, how do you ensure that a "required" control is not left blank, or that the various fields are filled out properly? HTML Goodies' JavaScript Primers #29, "Putting it all together: Form Field Validation," our focus today, takes us a small step towards sorting out this situation. Scopewise, Primer #29 limits itself to the validation of user data entered into <input type="text"> fields and addresses two aspects of that data:
(1) Some text fields require responses of a given length: has the user entered the correct number of characters?
(2) Some text fields require responses that are restricted to a particular character set; does the user's input conform to the character set on a character-by-character basis?

We know from Blog Entry #17 that a user's input into a text object corresponds to that text object's value property, and we noted in Blog Entry #22 that text object values function in effect as JavaScript String objects. Accordingly, the String object's length property and various methods will be our workhorses in parsing a user's text field input.

The Primer #29 Script and its first name field

Joe alternately judges the Primer #29 Script "a rough one, no doubt about it" and "quite basic" in the primer's "Concept" and script deconstruction, respectively; I myself vote the latter. As can be seen at Joe's demo page, the script codes a form that asks the user for a first name and a zip code; the script establishes certain criteria for the user's input and pops up various alert( ) messages if these criteria are not met. The script's code is given below:

<script type="text/javascript">
function validfn(fnm) {
if (fnlen == 0) {
alert("First name is required");
document.dataentry.fn.focus( ); } }
function validZip(zip) {
if (len != 5 && len != 10) {
alert("Zip is not the correct length"); ); }
for (i=0; i<5; i++) {
if (digits.indexOf(zip.charAt(i))<0) {
alert("First five digits must be numeric"); );
break; } } }
<form name="dataentry">
<h2>Form Field Validation</h2>
Enter First Name:<br>
<input type="text" name="fn" onblur="validfn(fn.value);">
<script type="text/javascript">
document.dataentry.fn.focus( );
Enter Zip Code (99999-9999):<br>
<input type="text" name="zip" size="10"><p>
<input type="button" value="Submit" onclick="validZip(zip.value);">
<!--The <form> tag is not closed in the primer.-->

When the document first loads, the following code in the document body:

<script type="text/javascript">
document.dataentry.fn.focus( );

puts an insertion point cursor (a blinking vertical line) in the "Enter First Name" text field; we previously demonstrated the focus( ) method of the text object in the "Method Examples" section of Blog Entry #22. It's important that this code is placed after the <input name="fn"> element in the document source; otherwise, you'll get an error.

Now, about that "Enter First Name" field: there is, to my knowledge, nothing we can do to stop the user from entering, say, Qwkopgg, Zxinmf, or some other obviously-not-a-name into the box, but we can at least make sure that the user doesn't leave it blank. Here's a sequence of events for Joe's approach thereto:

(1) If the user doesn't enter a first name and clicks outside the "Enter First Name" field or tabs to the "Enter Zip Code" field, then the onblur="validfn(fn.value);" code in the <input type="text" name="fn" onblur="validfn(fn.value);"> tag triggers the validfn(fnm) function at the start of the document head script. Joe's onBlur link (to in the script deconstruction's 2nd <li> is broken and in any case points to the wrong primer; the onBlur event handler was discussed in Primer #5, and we treated it here in Blog Entry #9.

(2) When validfn(fnm) is called, the user's (non)input, fn.value, is passed to validfn(fnm) and assigned to the variable fnm. We for our part first passed a text object value to a function in Blog Entry #34; Joe for his part did so in his Primer #28 Assignment answer script. I was unaware that an object, fn in this case, can be self-referenced with its name - I would have used the "this" keyword, i.e., onblur="validfn(this.value);" - but Joe's demo works OK, so fair enough.

Moving now to the validfn(fnm) function:

(3) The fnlen=fnm.length command line assigns the value of the length property of fnm - as noted above, fnm=fn.value is a string, an empty string ("") to be precise, although we noted in the "Literals and strings" section of Blog Entry #33 that an empty string is still a string - to the variable fnlen.

(4) The subsequent if statement:

if (fnlen == 0) {
alert("First name is required"); document.dataentry.fn.focus( ); }

then compares fnlen and 0, asking, "Are they equal?" The if condition returns true, so a "First name is required" alert( ) message pops up; after the user clicks the "OK" button on the alert( ) box, a document.dataentry.fn.focus( ) command returns focus to the "Enter First Name" field in most cases. (On my computer when using Netscape 7.02, if fn's original focus is blurred via the tab key, then the document.dataentry.fn.focus( ) command does not reroute the transfer of focus from fn to the "Enter Zip Code" field.)

(5) The if conditional does not have an else part, and no commands execute (nothing happens) if the user does type something in the fn field.

Other code formulations are possible here, of course. For example, instead of testing the length of fnm, the user's input, we can compare in the if condition fnm itself with an empty string:

if (fnm == "") {alert("First name is required"); document.dataentry.fn.focus( );}

In this regard, we can even remove the validfn( ) function altogether if we recode the fn field as:

<input type='text' name='fn'
onblur='if (this.value == "") {alert("First name is required"); this.focus( );}'>

Let's turn now to the "Enter Zip Code" field. If we wanted to, we could use the approach(es) above to ensure that the zip field isn't left blank either, but we have bigger fish to fry this time...

The zip code field

In the primer concept, Joe lists two basic data validation objectives for an "Enter Zip Code" field:
(1) Lengthwise, the user should enter an input of 5 or 10 characters corresponding to a ##### or #####-#### zip code, respectively.
(2) Characterwise, the user's first five input characters - and to be sure, most users will enter a 5-digit and not a 9-digit zip code - should be numeric digits and not include letters, nonalphanumeric (!, @, #, etc.) characters, nor whitespace.

Let's set the stage, then, for some zip code validation that satisfies these objectives. Having entered a name into the "Enter First Name" field, the user navigates to the "Enter Zip Code (99999-9999)" field and enters a zip code and then clicks the Submit button, whose code is:

<input type="button" value="Submit" onclick="validZip(zip.value);">

Proceeding as we did above, the onclick="validZip(zip.value);" code triggers the validZip(zip) function in the document head script. The user's input, zip.value, is passed to validZip(zip) and assigned to the variable zip. Moving to the validZip(zip) function, the len=zip.length command line assigns the value of the length property of zip to the variable len.

Two lines down, an if statement then tests if len is both (a) not equal to 5 and (b) not equal to 10:

if (len != 5 && len != 10) {
alert("Zip is not the correct length"); ); }

We've seen both the != comparison operator and the && logical/Boolean AND operator before. Joe first used the != operator in the Primer #26 Script (exasperatingly, he waits until the Primer #29 Assignment to comment directly on its meaning); the && operator briefly cropped up in the previous post.

If the user has entered a length-inappropriate zip code, then the if condition returns true, and a "Zip is not the correct length" alert( ) message pops up (contra the script deconstruction's 10th <li>, the alert( ) message does not announce that "the first five digits must be numeric"); after the user clicks the "OK" button on the alert( ) box, a ) command returns focus to the "Enter Zip Code" field. One might expect validZip(zip) to plug its zip argument into the ) command - such a substitution should throw an error, because the String object (zip is a string) does not have a focus( ) method - but this evidently doesn't happen.

Making sure that the user's first five characters are digits is a bit more involved. We begin by creating a character set of all ten digits - i.e., a set of the ten allowable numeric choices for each character that the user enters into the zip field - as a string and assigning it to the variable digits. The digits character set is declared locally on the second command line of the validZip( ) function, as shown above.

We then sequentially compare the first five characters of the user's zip input with digits via a for loop, which I've recast below in a somewhat expanded form:

for (i=0; i<5; i++) {
if (y<0) {alert("First five digits must be numeric"); ); break; } }

The loop runs for five iterations, specifically, for counter variable values of i=0, 1, 2, 3, and 4. The loop's increment-expression heralds the first use in the HTML Goodies JavaScript Primers series of the ++ increment operator, which Joe does not comment on but which we briefly discussed in Blog Entry #40.

The loop body introduces two new methods of the String object: charAt( ) and indexOf( ). Neither of these methods appears on the end-of-primer "Click Here For What You've Learned" page, but they are both listed in the HTML Goodies JavaScript methods keyword reference.

In JavaScript, "[t]he characters in a string are indexed from left to right with the first character indexed as 0 and the last as String.length-1," quoting DevGuru, and this indexing underlies both methods. The charAt(i) method takes an index number argument and returns the character corresponding to the ith position of the string on which it acts. Complementarily, the indexOf("some_string") method takes a string argument and returns (a) the index number of the first occurrence of "some_string" in the string on which it acts or (b) -1 if "some_string" is not found. Optionally, the indexOf("some_string", j) method can also take a second, index number argument that specifies a character position j at which the search for "some_string" begins.

Getting back to the expanded for loop above, suppose the user enters 92083 into the zip field; for the for loop's first iteration, we generate the following values of x and y:

For i=0: x=9 (9 is the 0th character in 92083), y=9 (9 appears at the 9th position in the digits character set);
For i=1: x=2, y=2;
For i=2: x=0, y=0; etc.

This brings us to the for loop's if statement; for y values of 9, 2, 0, 8, and 3, the if condition, y<0, is uniformly false and the browser thus skips over the subsequent if {commands}.

And what if the user's zip code were to contain one or more not-a-number characters? Ah, I'm sure you've got it sorted out...suppose the user enters 9w0u3 into the zip field. For the for loop's second iteration, x=w but y=-1 because w does not appear in the digits character set. The if condition returns true in this case, so a "First five digits must be numeric" alert( ) message pops up; after the user clicks the "OK" button on the alert( ) box, a ) command returns focus to the zip field. Without comment, Joe also tacks on a break statement, which terminates the for loop and which we discussed in Blog Entry #40; without the break statement, a 9w0u3 zip code would generate two alert( ) messages, one for the second for iteration and one for the fourth for iteration.

In the actual script, Joe combines the charAt( ) method, the indexOf( ) method, and the if condition all on one command line:

if (digits.indexOf(zip.charAt(i))<0)

but the effect is the same as that described above.

Names without numbers

Had Joe declared the digits character set globally, then he could have used it to ensure that the user's "Enter First Name" input contains no numbers by inserting the following code in the validfn(fnm) function:

for (i=0; i<fnlen; i++) {
if (charindex != -1) {
window.alert("Names do not contain numbers! Please enter a proper name."); document.dataentry.fn.focus( ); break; } }

The if condition returns true if any of fnm's characters appears in digits.

Forms, controls, and names

Finally, a nitpicking comment on something Joe says in the last deconstruction <li>: "When you use forms with JavaScript, each form item must be given a name that links it to the sections of the JavaScript that will act upon it." Actually, we learned in Blog Entry #16 that names are unnecessary for the referencing of forms and their controls, and we can certainly use document.forms[0].elements[0] and document.forms[0].elements[1] to respectively reference the fn and zip fields if we so choose.

I've got a bit more to say about data validation and I'll do that in the next entry; we also may take a stab at applying the data validation techniques of regular expressions to the Primer #29 Script - stay tuned!


Tuesday, July 18, 2006
An Image Loop: Allegro
Blog Entry #45

Having put together a slide show in our last episode, we turn our attention in this entry to another classic image-flip application - an animation - as we tuck into HTML Goodies' JavaScript Primers #28, "Putting it all together: Animation." The Primer #28 Script will look familiar - it does little more than add to the Primer #27 Script a pair of nested for loops (in an attempt) to automate the image-flip process.

In the "Script's Effect" section of the primer, Joe hints that the Primer #28 Script is not so generally useful when he says, "Due to changes in browsers, this script may not run on your MSIE browser." In practice, my varied attempts to execute the script - at least per its original, for-loop-based design - using either MSIE 5.1.6 or Netscape 7.02 were unsuccessful. So here's what we'll do:
(1) We'll first discuss how the script is supposed to work and why its execution is problematic; then,
(2) I'll give you a new-and-improved animation script that will work with any JavaScript-capable browser.

The Primer #28 Script

<script type="text/javascript">
var num=1;
img1 = new Image(150,150);
img1.src = "pic1.gif";
img2 = new Image(150,150);
img2.src = "pic2.gif";
img3 = new Image(150,150);
img3.src = "pic3.gif";
function startshow( ) {
for (i=1; i<20; i=i+1) { // for loop #1
for (x=1; x<800; x=x+1) { } // for loop #2
if (num==4) {num=1;} } // end of for loop #1
document.mypic.src=img1.src; }
<img src="pic1.gif" name="mypic" border="0" alt=""><p>
<a href="javascript:startshow( );">Display Animation</a>

There are no new concepts in the script above - we covered for loops in Blog Entry #40, and the remaining code (image preloading, the JavaScript URL, etc.) was addressed in the previous post - so let's get right to a rundown of what happens:

(1) When the document loads, the script in the document head preloads the pic#.gif images into the user's browser cache and the <img src="pic1.gif" name="mypic" border="0" alt=""> element in the document body displays the pic1.gif image as the first animation frame.

(2) When the user clicks the "Display Animation" link, then the href="javascript:startshow( );" code triggers the startshow( ) function in the document head script.

(3) The outer for loop #1 begins. Each iteration of for loop #1 displays an animation frame, and thus for loop #1's condition - i<20 in the primer script and source code, but i<21 in the 2nd and 6th <li>s of the script deconstruction - determines the total number of frames in the animation.

(4) At the start of for loop #1's first iteration, the value of the indexing variable num, unchanged since its global initialization on the first line of the document head script, is 1, and the document.mypic.src=eval("img"+num+".src") command thus reloads the pic1.gif image.

(5) The inner for loop #2 executes, "counting" to 800 and serving (in theory) as a time-delay between consecutive animation frames.

(6) (a) The num=num+1 statement increments num to 2.
(b) The condition of the if (num==4) {num=1;} statement is false, so the browser skips over the {num=1;} command.
(c) Lastly, for loop #1's counter variable i increments to 2 to wrap up for loop #1's first iteration.

(7) For for loop #1's second iteration:
(a) With num=2, assignment of "pic2.gif" to document.mypic.src displays the pic2.gif image as the second animation frame.
(b) The inner for loop #2 again counts to 800.
(c) The num=num+1 statement increments num to 3.
(d) The if condition is still false.
(e) The loop counter i increments to 3.

(8) For for loop #1's third iteration:
(a) With num=3, assignment of "pic3.gif" to document.mypic.src displays the pic3.gif image as the third animation frame.
(b) The inner for loop #2 again counts to 800.
(c) The num=num+1 statement increments num to 4.
(d) The if condition is now true and num is reset to 1.
(e) The loop counter i increments to 4.
At the start of for loop #1's fourth iteration:
(f) With num=1, assignment of "pic1.gif" to document.mypic.src redisplays the pic1.gif image as the fourth animation frame.

Steps (5)-(8) then repeat* until the end of the loop, i.e., when the value of i reaches 20 or 21 at the end of the 19th or 20th loop iteration, respectively. A 19- or 20-iteration loop will display not-quite-seven pic1.gif-pic2.gif-pic3.gif image cycles and will end on the pic1.gif and pic2.gif images, respectively.
(*Excluding steps (6)(c), (7)(e), and (8)(e) - i continues to increment, of course.)

(9) Finally, the document.mypic.src=img1.src statement following for loop #1 redisplays the pic1.gif image as the last animation frame.

So what's the problem?

We turn our attention to for loop #2, about which (in the script deconstruction's 4th <li>) Joe says:

"Notice that there are no statements in the 'for' loop. The loop itself handles counting from 1 to 800. That equals about 800/1000 of a second."

800 milliseconds?? Oh goodness - my 1999 iMac smokes through an 800-iteration for loop in about five milliseconds when using MSIE, as determined by:

d0 = new Date( );
for (x=1; x<=800; x=x+1) { if (x == 800) d1 = new Date( ); } // d1-d0 returns 5-6

However, ramping up the iteration count to 160,000 - in the hope of creating a 1-second time-delay - does not help. Here's what happens in this case when using MSIE: I first click the "Display Animation" link; after 2-3 seconds, the normal arrow cursor becomes a spinning wait cursor (I specifically see the spinning cursor in Figure 11-1 on this page), signifying that an operation is in progress; about 15 seconds later, the wait cursor reverts to the arrow cursor; from start to finish, all I see for an "animation" is an unvarying display of the pic1.gif image. Netscape 7.02 gives similar results, but much more slowly (startshow( ) executes in ≅130 seconds for x=160000).

Joe helpfully points out in the Primer #28 Assignment answer that "alert is a good tool for debugging [i.e., troubleshooting] your programs," and we can verify that the parts of the startshow( ) function are indeed running via the use of strategically inserted alert( ) commands at various points in the function, for example:

function startshow( ) {
window.alert("The startshow( ) function has been triggered.");
for (i=1; i<20; i=i+1) { if (i==5) window.alert("The 5th iteration of for loop #1 is underway.");
for (x=1; x<=160000; x=x+1) { if ((i==5) && (x==160000)) window.alert("Yes, for loop #2 really does count to 160,000."); }
// the && logical/Boolean AND operator is discussed here
if (num==4) {num=1;} }
document.mypic.src=img1.src; }

Our diagnosis, then, is that for loops #1 and #2 mesh in such a way that prevents the separative display of the pic#.gif images. At the least, it is clear that for loop #2 does not stall the operation of for loop #1 (even as we were somewhat successful in using bodyless for loops to stall background color changes for the Primer #24 Assignment, as discussed in Blog Entry #40).

To muddy the water a bit before moving on, I find when using Netscape 4.79 that the Primer #28 Script works just fine if for loop #2's iteration count (x) is set to 25,000.

A new-and-improved animation script

So the nested for loop business doesn't work very well. However, we can ditch the for loops altogether via a judicious use of the setTimeout( ) method of the window object, as follows:

<!--replace the document head script in the Primer #28 Script with the code below-->
<script type="text/javascript">
var num=1;
var i=1;
img1 = new Image(150,150); img1.src = "pic1.gif";
img2 = new Image(150,150); img2.src = "pic2.gif";
img3 = new Image(150,150); img3.src = "pic3.gif";
function startshow( ) {
if (num==4) num=1;
if (i<22) window.setTimeout("startshow( );",1000);
if (i==22) i=1; }

Try it out below:

Display Animation

The script above retains the features of the original for loop #1, namely; (i) a var i=1 initial-expression that precedes the startshow( ) function; (ii) an updated if (i<22) window.setTimeout("startshow( );",1000) condition placed in the startshow( ) body; and (iii) an i=i+1 increment-expression, which is also in the startshow( ) body. The window.setTimeout("startshow( );",1000) command of the if statement recursively calls the startshow( ) function after 1000 milliseconds, providing a ≅1-second delay between animation frames. Netscape notes that "setTimeout does not stall the script. The script continues immediately (not waiting for the timeout to expire). The call simply schedules a future event." The window.setTimeout("startshow( );",1000) command is thus placed near the end of the startshow( ) function so that it doesn't interfere with startshow( )'s other statements. The script concludes with an if (i==22) i=1 statement that resets i to 1 at the end of the animation in case the user wants to run the animation again. One would think that the if (i<22) window.setTimeout("startshow( );",1000) expression could be replaced with: if (i<22) { for (x=1; x<=160000; x=x+1) { if (x == 160000) startshow( ); } } but in the event this substitution gave the spinning-wait-cursor-unvarying-pic1.gif-image behavior described above for the execution of the nested for loops in the original script. The Primer #28 Assignment In the Primer #28 Assignment, Joe asks the reader to modify the Primer #28 Script so that a user can enter slow, medium, or fast into a text box and then run the pic#.gif animation at a corresponding speed. At this point, as you can imagine, my patience with for-loop-based animation scripts has run its course, and for this reason I'm not going to discuss Joe's answer script here. Here's a demo of my own assignment answer, which again uses a window.setTimeout( ) command to delay the display of the pic#.gif images and substitutes a set of radio buttons for the text box.
Choose an animation speed: Fast Medium Slow
Animate me!
At a non-Blogger page I would use this code for the above demo; its deconstruction is left to you.
In the next post, we'll look at HTML Goodies' JavaScript Primers #29, in which we'll get an introduction to the important topic of data validation.


Saturday, July 08, 2006
An Image Loop: Andante
Blog Entry #44

HTML Goodies' JavaScript Primers #27, "Putting it all together: A S[l]ide Show," comprises a reasonably straightforward application of the image-flip methodology of Primer #15, Primer #16, and the Primer #23 Assignment to the creation of a simple slide show. The Primer #27 Script enables a looplike, sequential display of the pic1.gif, pic2.gif, and pic3.gif images that we randomized in the Primer #23 Assignment, and is given below:

<script type="text/javascript">
var num=1;
img1 = new Image( );
img1.src = "pic1.gif";
img2 = new Image( );
img2.src = "pic2.gif";
img3 = new Image( );
img3.src = "pic3.gif";
function slideshow( ) {
if (num==4) {num=1;}
document.mypic.src=eval("img"+num+".src"); }
<img src="pic1.gif" name="mypic" border="0" alt=""><p>
<a href="JavaScript:slideshow( );">Click for the next picture</a>

Image preloading

Prior to the start of the slide show, the script in the document head preloads the pic1.gif, pic2.gif, and pic3.gif images into the user's browser cache. Let's look at the code that preloads the pic1.gif image:

img1 = new Image( );
img1.src = "pic1.gif";

Recalling the right-to-left character of assignment statements:

(1) The img1 = new Image( ); command line creates with a constructor statement an image object and assigns it to the identifier img1.

You won't find anything on new Image( ) constructor statements in present-day JavaScript; rather, you have to go back to the image object section of Netscape's "obsolete" JavaScript 1.3 Client-Side Reference for this information. Indeed, JavaScript no longer has an image object, which as a host object has been spun off to the DOM; in this regard, the Image( ) constructor is, to my understanding, a 'legacy feature' belonging to "Level 0" of the DOM.

As Joe notes in the fourth list item (<li>) of his script deconstruction, Image( ) can have two parameters specifying, in order, the width and height in pixels of the image, in this case:

img1 = new Image(150,150); // all three pic#.gif images are 150×150

(2) The img1.src = "pic1.gif"; command line writes the src property of the img1 image by assigning the value/file "pic1.gif" to img1.src.

Together, these two lines of code do not display the img1/pic1.gif image on the page*, but they do cache the image on the user's hard disk, making it easier to access than if it had to be 'downloaded on the spot,' as Joe explains in his old-school "So, You Want To Pre-Load, Huh?" tutorial.
(*The slide images are displayed by the <img name="mypic"> element in the document body.)

A question arises: "Is this image preloading business really necessary?"

I can't give you a one-size-fits-all answer here. In this particular case, perhaps not, given that the pic#.gif images are small, 32K gif files (BTW, they're easily shrinkable to 4K each using Adobe Photoshop's "Save for Web" feature). For larger images, preloading won't hurt and is probably a good idea, even for users with speed-optimized systems.

A nitpicking comment on something Joe says in the fifth deconstruction <li>: "By placing all these image objects outside a function, the program preloads the pictures." Actually, if we wanted to, we could put the first seven script statements in, say, a preload( ) {statements} function and call that function with an onLoad="preload( );" event handler in the <body> tag, and this would still preload the pic#.gif images, but then the subsequent slideshow( ) function would not recognize the now-nonglobal preload( ) function code.

Moving through the slides/JavaScript URLs

We move through the pic#.gif images that compose the slide show by repeatedly clicking the following link:

<a href="JavaScript:slideshow( );">Click for the next picture</a>

"That's a little new, huh?" Joe correctly points out in the last <li> of the script deconstruction. This is indeed our first encounter in the HTML Goodies JavaScript Primers series with a JavaScript URL; here is another legacy feature that you won't find in JavaScript 1.5. A JavaScript URL has the following syntax:


and is assigned as a string to the href attribute of an <a> tag in place of a 'normal' Web page URL. The href="javascript:executable_code;" attribute/value gives the same effect as, but is not replaceable by, an onClick="executable_code;" event handler.
(If you'd rather use an onClick event handler than a JavaScript URL, then you can replace the link with a button:
<input type="button" value="Click for the next picture" onclick="slideshow( );">.)

A few notes on JavaScript URLs:
• The "javascript" part is not case-sensitive, but the "executable_code" part is.
• The "executable_code" can be one or more commands and/or functions. notes that the void special operator should be applied to JavaScript URL commands that return values, e.g.:

href="javascript: slideshow( ); void(document.bgColor='blue'); void(window.defaultStatus='Hello');"

On my computer, I find that the 'voiding' of values is necessary when using Netscape but not when using MSIE.

Before we go any further, we need to address the rest of that last deconstruction <li>:

"See how the call is for the JavaScript rather than the function? That allows all parts to be used rather than just the function. If you use just the function with this format, you'll have no images because they will have been bypassed."

Joe is alleging here that if you leave out the "javascript" part of the JavaScript URL:

<a href="slideshow( );">Click for the next picture</a>

then clicking the link will call the slideshow( ) function 'in isolation,' i.e., slideshow( ) will not be able to make use of the global statements preceding it. This isn't what happens, folks; without the "javascript", the browser (a) sees href="slideshow( );" as a regular URL, (b) goes looking for a "http://hostname/pathname/slideshow( );" page, and - assuming that the calling document's directory doesn't contain a "slideshow( );" file - (c) pops up a "The file cannot be found" message box.

In sum, the "javascript" keyword in a JavaScript URL does not affect a function's recognition of global code but is merely a necessary part of the JavaScript URL syntax; also, the href="slideshow( );" attribute/value is not a legitimate function call and thus the slideshow( ) function is not executed.

The slideshow( ) function

The slideshow( ) function in the document head resembles a count-controlled loop; it has:
(i) a preceding initial-expression, var num=1;
(ii) a condition, if (num==4) num=1; and
(iii) an increment-expression, num=num+1.

The variable num is our counter variable and also serves as an index number for the pic#.gif image files; num is initialized with a value of 1 on the first line of the document head script. Strangely, the script deconstruction's second <li> avers that the external-to-a-function declaration of num is "something new." Our use of unfunctionized variables began as far back as Primer #3; moreover, we discussed the difference between global variables and local variables at the end of Blog Entry #33. It is in fact important, however, that num is declared globally; if the var num=1 statement were placed inside the slideshow( ) function, then num would be reset to 1 each time slideshow( ) is called and the slide show would not proceed. (For a count-controlled for or while loop, recall that the placement of the initial-expression inside the loop body sets up an infinite loop.)

We're ready now to run through a sequence of events for the slide show:

(1) When the document loads, the <img src="pic1.gif" name="mypic" border="0" alt=""> element displays the pic1.gif image as the first slide.

(2) Upon clicking the "Click for the next picture" link, the href="JavaScript:slideshow( );" code triggers the slideshow( ) function.

(3) The num=num+1 statement increments num to 2.

(4) The condition of the if (num==4) {num=1;} statement is false (num is not equal to 4), so the browser skips over the {num=1;} command (num is not reset to 1).

(5) Next, an image flip: with num=2, the right-hand side of the document.mypic.src=eval("img"+num+".src"); statement evaluates the "img"+2+".src" expression to give the string "pic2.gif", whose assignment to document.mypic.src displays the pic2.gif image as the second slide.

(6) Reclicking the "Click for the next picture" link recalls the slideshow( ) function, num increments to 3, the if condition is still false, and assignment of "pic3.gif" to document.mypic.src displays the pic3.gif image as the third slide.

(7) Reclicking the "Click for the next picture" link recalls the slideshow( ) function and num increments to 4; now, the if condition is true and num is reset to 1. Assignment of "pic1.gif" to document.mypic.src redisplays the pic1.gif image as the fourth slide.

Steps (2)-(7) repeat ad infinitum for subsequent link clicks, giving a cyclical slide show of the pic#.gif images.

"What about for a real slide show, with, like, 40 slides?"

No problem - name all of the slide images in order using the pic#.gif format and preload them as above, and then change slideshow( )'s if statement to if (num==41) {num=1;}, and you're good to go.

A bit more on eval( ), and other code possibilities

Joe offers the following description of the role of the eval( ) function in the image-flip statement:

"Notice that eval( ) causes img1.src to be the contents of img1.src, not the literal string. If it was seen as a literal string it would not be seen as a command but rather as just a run of letters."

Joe is on the right track here, but a bit of clarification is in order. When Joe says "literal string," he is referring to the string "img#.src" resulting from "img"+num+".src", the eval( ) argument. If we remove the eval( ) function from the image-flip statement, then we are left with:

document.mypic.src="img"+num+".src"; // or

which is still a command, to be sure, but not a meaningful one for the browser (unless the calling document's directory contains an "img#.src" image file). So what happens? You'll still see the first slide (step (1) above), but clicking the "Click for the next picture" link will not load subsequent slides because the "img#.src" "run[s] of letters" (unlike "pic#.gif") are not image file names.

You may recall that this same discussion, more or less, cropped up in Blog Entry #39 when we examined the following command in the "Or if You are REALLY Clever" answer script for the Primer #23 Assignment:

document.write("<img src=" + quot + eval("var"+num) + quot +">"); // quot="'";

A point worth reemphasizing: both eval("img"+num+".src") and eval("var"+num) return their respective values as strings; we can verify this via the typeof special operator, for example:

typeof(eval("img"+num+".src")) // returns string

At least for the Primer #27 image-flip statement, this is important because the use of an unstringified pic#.gif in the command:


would throw a "pic# is not defined" error.

If you'd rather not use the eval( ) function, then I can think of a couple of alternatives:

(1) Per the Blog Entry #39 discussion cited above, we can replace eval("img"+num+".src") with "pic"+num+".gif":

document.mypic.src="pic" + num + ".gif";

(2) With the JavaScript Array( ) object now under our belts, we can create an array of images and retool the document head script as follows:

<script type="text/javascript">
var num=0;
imgx = new Array( );
imgx[0] = new Image(150,150); imgx[0].src = "pic1.gif";
imgx[1] = new Image(150,150); imgx[1].src = "pic2.gif";
imgx[2] = new Image(150,150); imgx[2].src = "pic3.gif";
function slideshow( ) {
if (num==3) num=0;
document.mypic.src=imgx[num].src; }

(The coordination of the loop counter num with the imgx index numbers via the imgx[num].src expression was inspired by the Primer #26 Script.)

We'll convert the Primer #27 slide show into an animation of the pic#.gif images in Primer #28, which we'll take on in the next entry.


Powered by Blogger

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