facebook google plus twitter
Webucator's Free Ajax Tutorial

Lesson: Ajax Applications

Welcome to our free Ajax tutorial. This tutorial is based on Webucator's Ajax Training course.

In this lesson, you will apply the concepts you have learned so far.

Lesson Goals

  • Apply the concepts covered thus far to practical applications.

Login Form

Have you ever forgotten your username or password for a website? Have you waited 10 seconds for a full page to redraw while the only thing new is a message telling you that the username and password are not recognized. Ajax can make that process much less painful for the user.

Imagine a simple login form like the one shown below, but on a page with a lot of other content:Page with Content

When the user fills the form out with a bad password, an error message appears and the username is highlighted:Error Message

The rest of the page should stay the same. Ajax makes this possible without refreshing the entire page. The code for this is shown below; to run the demo, first:

  1. Open the command line (on a PC) or terminal (on a Mac), and navigate to the directory AjaxApplications/Demos/.
  2. Type npm install.
  3. Type npm start to start the Node.js server.
  4. Visit http://localhost:8080/Login.html in your browser to view the page.

Code Sample:

AjaxApplications/Demos/Login.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Login Form</title>
<link href="Login.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript">
	function login(form) {
		var un = form.Username.value;
		var pw = form.Password.value;
		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open("post", "Login", true);
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				loginResults();
			}
		}

		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("username=" + un + "&password=" + pw);

		function loginResults() {
			var loggedIn = document.getElementById("LoggedIn");
			var badLogin = document.getElementById("BadLogin");
			if (xmlhttp.responseText.indexOf("failed") == -1) {
				loggedIn.innerHTML = "Logged in as " + xmlhttp.responseText;
				loggedIn.style.display = "block";
				form.style.display = "none";
			} else {
				badLogin.style.display = "block";
				form.Username.select();
				form.Username.className = "Highlighted";
				setTimeout(function() {
					badLogin.style.display = 'none';
				}, 3000);
			}
		}
	}

	observeEvent(window, "load", function() {
		var loginForm = document.getElementById("LoginForm");
		observeEvent(loginForm, "submit", function() {
			login(loginForm);
		});
	});
</script>
</head>
<body>
<form id="LoginForm" onsubmit="return false">
	<h1>Login Form</h1>
	<div class="FormRow">
		<label for="Username">Username:</label>
		<input type="text" size="15" id="Username" name="Username">
	</div>
	<div class="FormRow">
		<label for="Password">Password:</label>
		<input type="password" size="15" id="Password" name="Password">
	</div>
	<div class="FormRow" id="LoginButtonDiv">
		<input type="submit" value="Login">
	</div>
	<div id="BadLogin">
		<p>The login information you entered does not match 
		an account in our records. Please try again.</p>
	</div>
</form>

<h1 id="LoggedIn"></h1>

</body>
</html>

The information is sent using a POST request, since the authentication information would be confidential - under HTTPS it would be automatically encrypted.

Quick Lookup Form

In some cases, you need to get quick information from a database, but you don't want to process an entire page. For example, you may have a form for requesting information about an order. The form might have multiple fields, one of which is the order id. The order id is not required, but if it's filled in it must match an existing order id in the database. Rather than waiting to check the order id until the user fills out and submits the entire form, you can use Ajax to flag a bad order id as soon as the user tabs out of that field. A simple sample interface is shown below:Simple Interface

When the user enters an order id that is not in the database, an error is displayed and the submit button becomes disabled:Submit Button Disabled

When the user enters a valid order id, an icon is displayed indicating that the order id exists and the submit button becomes enabled:Submit Button Enabled

The code is shown below. Remember to start the Node.js server, if it isn't already running:

  1. Open the command line (on a PC) or terminal (on a Mac), and navigate to the directory AjaxApplications/Demos/.
  2. Type npm start to start the Node.js server.
  3. View http://localhost:8080/Lookup.html in your browser to see the page.

Code Sample:

AjaxApplications/Demos/Lookup.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Order Lookup Form</title>
<link href="Lookup.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript">
	function lookup(order) {
		var orderNum = order.value;
		var xmlhttp = new XMLHttpRequest();
		var btnSubmit = document.getElementById("SubmitButton");
		var output = document.getElementById("OrderNumError");

		btnSubmit.disabled = true;
		if (orderNum.length == 0) { //OK to submit
			output.innerHTML = "";
			btnSubmit.disabled = false;
			return true;
		}
		if (isNaN(orderNum)) { //Error
			output.innerHTML = "Must be numeric.";
			order.style.color = "red";
			btnSubmit.disabled = true;
			return true;
		}

		//Look up order number in database
		xmlhttp.open("GET", "Lookup?orderNum=" + orderNum, true);

		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				lookupResults(xmlhttp);
			}
		}
		xmlhttp.send(null);

		function lookupResults(xmlhttp) { //Callback function
			if (xmlhttp.responseText.indexOf("success") == -1) { //Error: no match
				output.innerHTML = "No such order number.";
				order.style.color = "red";
				btnSubmit.disabled = true;
			} else {
				output.innerHTML = "<img src='check.gif'>";
				order.style.color = "green";
				btnSubmit.disabled = false;
			}
		}

		return false;
	}

	observeEvent(window, "load", function() {
		var order = document.getElementById("OrderNum");
		observeEvent(order, "blur", function() {
			lookup(order);
		});
	});
</script>
</head>
<body>
<form id="LookupForm" onsubmit="alert('Form would submit.'); return false;">
	<h1>Lookup Form</h1>
	<p>Enter an order number.</p>
	<div class="FormRow">
		<label for="OrderNum">Order Number:</label>
		<input type="text" size="10" id="OrderNum" name="OrderNum">
		<span id="OrderNumError"></span>
	</div>
	<div class="FormRow">
		<label for="Comments">Comments:</label><br >
		<textarea id="Comments" name="Comments" cols="40" rows="4"></textarea>
	</div>
	<div class="FormRow">
		<input type="submit" id="SubmitButton" value="Submit">
	</div>
</form>
</body>
</html>

The first thing that the Lookup function does is disable the submit button, so that the user does not have the opportunity to click it while the request is pending. We have also made the function return false if an Ajax request is made, to further ensure that the user cannot successfully click the submit button before the request completes.

The server-side piece of the lookup form is generated by the /Lookup GET route in AjaxApplications/Demos/server.js:

app.get('/Lookup', function(req, res) {
  var orderNum = req.param('orderNum');
  var sql = "SELECT OrderID FROM Orders WHERE OrderID = " + orderNum;
  db.serialize(function() {
  db.get(sql, function(err, row) {
      if(err !== null) {
        res.status(500).send("An error has occurred -- " + err);
      } else {
        if (row) {
          res.status(200);
          return res.send("success");
        } else {
          res.status(200);
          return res.send("failed");
        }
      }
    });
  });
});
			

The server-side script simply return "success" if the order number is found or "failed" if it is not, querying the database by order number.

Creating a Simple Lookup Form

Duration: 20 to 30 minutes.

In this exercise, you will create a lookup form that takes a year and returns the name of the person who was president that year. The page looks like this:Page

  1. From the command line, navigate to the directory AjaxApplications/Exercises/ and type npm install to install the needed Node.js modules.
  2. Also from the command line, type npm start to start the Node.js server.
  3. Open AjaxApplications/Exercises/server.js in your editor and review the "Lookup" route (the method which begins app.get('/Lookup', function(req, res) {). This route is complete. It expects a year to be passed in and returns, using the Lookup.jade template, an HTML string containing the names of the presidents who were in office that year.
  4. Open AjaxApplications/Exercises/Lookup.html in your editor and write all the JavaScript code for this page. The call to the server should look something like this: /Lookup?year=1801, where "1801" is the year entered by the user.
  5. Test your solution by opening http://localhost:8080/Lookup.html in the browser.

The server-side script is complete. The Node.js response route "Lookup" queries the database and returns all presidents for the given year, supplied as the GET parameter year:

app.get('/Lookup', function(req, res) {
    var year = req.param('year');
    presidentsdb.all("SELECT FirstName, LastName, StartYear, EndYear FROM Presidents WHERE StartYear <= " + year + " AND EndYear >= " + year, function(err, row) {
      if(err !== null) {
        res.status(500).send("An error has occurred -- " + err);
      } else {
        res.render('Lookup.jade', {presidents: row}, function(err, html) {
          res.status(200).send(html);
        });
      }
    });
});
				

The response is rendered via the template Lookup.jade template:

doctype html
html(lang="en")
  head
    title Lookup

  body
    - each pres in presidents
        p #{pres.FirstName} #{pres.LastName} (#{pres.StartYear} - #{pres.EndYear})

You'll do your work here on the client-side:

Solution:

AjaxApplications/Solutions/Lookup.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Lookup Form</title>
<link href="Lookup.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript">
	function lookUp(form) {
		var year = form.Year.value;
		var xmlhttp = new XMLHttpRequest();
		if (isNaN(year) || year.length != 4) {
			alert("Please enter a valid year.");
			return false;
		}
		if (year < 1789 || year > 1845) {
			alert("Please enter a year between 1789 and 1845.");
			return false;
		}

		xmlhttp.open("get", "Lookup?year=" + year, true);

		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				lookupResults(xmlhttp);
			}
		}
		xmlhttp.send(null);

		function lookupResults(xmlhttp) {
			var resultDiv = document.getElementById("LookupResults");
			resultDiv.innerHTML = xmlhttp.responseText;
		}
	}

	observeEvent(window, "load", function() {
		var form = document.getElementById("LookupForm");
		observeEvent(form, "submit", function() {
			lookUp(form);
		});
	});
</script>
</head>
<body>

<form id="LookupForm" onsubmit="return false;">
	<h1>Lookup Form</h1>
	<p>Enter a year between 1789 and 1845 to find out who was president that year.</p>
	<div class="FormRow">
		<label for="Year">Year:</label>
		<input type="text" size="5" id="Year" name="Year">
		<input type="submit" value="Lookup">
	</div>
	<hr>
	<div class="FormRow" id="LookupResults"></div>
</form>

</body>
</html>

We check first to test if the user-entered year is a number, is four digits in length, and falls between the date range indicated.

We then make a GET request to Lookup, passing along the year parameter.

We then set the innerHTML of div#resultDiv with the returned results.

Preloaded Data

Google Maps (http://maps.google.com) was one of the applications that brought so much attention to Ajax. One of the cool things about it is that it allows the user to drag maps around the screen seamlessly loading new sections. It does this by preloading the sections around the map that the user is likely to drag on to the screen. This same concept can be applied to other applications, such as slideshows and navigable tables.

Ajax Slideshow

Let's first take a look at the slideshow shown below:Slideshow

When the user clicks on the Previous or Next buttons, the page makes an XMLHttpRequest to the server, which returns XML as shown below:XML Returned

The callback function creates the next slide from this XML. The code is shown below. Remember to start the Node.js server, if it isn't already running:

  1. Open the command line (on a PC) or terminal (on a Mac), and navigate to the directory AjaxApplications/Demos/.
  2. Type npm start to start the Node.js server.
  3. Visit http://localhost:8080/SlideShow.html in your browser to view the page.

Code Sample:

AjaxApplications/Demos/SlideShow.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Slide Show</title>
<link href="SlideShow.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript">
function prevSlide() {
	var curSlide = document.getElementById("CurSlideNum").innerHTML;
	getSlide(curSlide - 1);
}

function nextSlide() {
	var curSlide = Number(document.getElementById("CurSlideNum").innerHTML);
	getSlide(curSlide + 1);
}

function getSlide(curSlide) {
	var xmlhttp = new XMLHttpRequest();
	var btnPrev = document.getElementById("PrevButton");
	var btnNext = document.getElementById("NextButton");

	btnPrev.disabled = true;
	btnNext.disabled = true;

	xmlhttp.open("get", "SlideShow?Slide=" + curSlide, true);

	xmlhttp.onreadystatechange = function() {
		if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				changeSlide(xmlhttp);
		}
	}
	xmlhttp.send(null);

	function changeSlide(xmlhttp) { //Callback function creates slide
		var docElem = xmlhttp.responseXML.documentElement;
		var slideText = document.getElementById("SlideText");
		var slideImage = document.getElementById("SlideImage");
		var numSlides = document.getElementById("TotalSlideNum").innerHTML;
		var name, years;
		removeWhitespace(docElem, true);
		name = docElem.firstChild.firstChild.nodeValue;
		years = docElem.childNodes[1].firstChild.nodeValue;
		slideText.innerHTML = name + "<br>" + years;
		slideImage.src = "Slides/" + docElem.childNodes[2].firstChild.nodeValue;
		slideImage.alt = name;
		document.getElementById("CurSlideNum").innerHTML = curSlide;
		if (curSlide != 1) {
				btnPrev.disabled = false;
		}
		if (curSlide != numSlides) {
				btnNext.disabled = false;
		}
	}
}

observeEvent(window, "load", function() {
	var btnPrev = document.getElementById("PrevButton");
	var btnNext = document.getElementById("NextButton");
	var totalSlides = 10;
	observeEvent(btnPrev, "click", prevSlide);
	observeEvent(btnNext, "click", nextSlide);
	document.getElementById("TotalSlideNum").innerHTML = totalSlides;
	getSlide(1);
});
</script>
</head>

<body>
<h1>First 10 Presidents</h1>
<div id="Slide">
	<img id="SlideImage">
	<div id="SlideText"></div>
	<hr>
	<button id="PrevButton">Previous</button>
	Slide <span id="CurSlideNum">1</span> of <span id="TotalSlideNum"></span>
	<button id="NextButton">Next</button>
	<hr>
	<div id="SlideMessage"></div>
</div>
</body>
</html>

Notice how the changeSlide() callback function changes the text of the slide and the image src and alt value based on the XML returned. Although this is pretty cool in and of itself, it can be made better by preloading the preceding and following images, so the user experiences no delay when navigating from slide to slide. In this case, the server-side script needs to return more data. Our script, the SlideShow response route from AjaxApplications/Demos/server.js, is shown below; the response is rendered via AjaxApplications/Demos/SlideShow.xml.jade

app.get('/SlideShow', function(req, res) {
	presidentsdb.serialize(function() {
		var Slide = req.param('Slide');
		var sql = "SELECT FirstName, LastName, StartYear, EndYear, ImagePath FROM Presidents WHERE PresidentID=" + Slide;
		res.setHeader('Content-type', 'text/xml');
		presidentsdb.all(sql, function(err, row) {
			if(err !== null) {
				res.status(500).send("An error has occurred -- " + err);
			} else {
				res.render('SlideShow.xml.jade', {presidents: row}, function(err, xml) {
					res.status(200).send(xml);
				});
			}
		});
	});
});

Notice that the SQL query will return records for the chosen president, the preceding president, and the following president. The resulting XML will look something like this: Returned XML

Now we need to change the code to handle the preloaded slides and change the HTML to have hidden locations for the preloaded data. The code below shows how this is done. As always, be sure to start the Node.js server, if it isn't already running:

  1. Open the command line (on a PC) or terminal (on a Mac), and navigate to the directory AjaxApplications/Demos/.
  2. Type npm start to start the Node.js server.

Code Sample:

AjaxApplications/Demos/SlideShow-preloaded.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Slide Show</title>
<link href="SlideShow.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript">
function prevSlide() {
	var curSlide = document.getElementById("CurSlideNum").innerHTML;
	getSlide(curSlide - 1, "prev");
}

function nextSlide() {
	var curSlide = Number(document.getElementById("CurSlideNum").innerHTML);
	getSlide(curSlide + 1, "next");
}

function getSlide(curSlide, slideSource) {
	var slideText = document.getElementById("CurSlideText");
	var slideImage = document.getElementById("CurSlideImage");
	var curSlideNum = document.getElementById("CurSlideNum");
	var btnPrev = document.getElementById("PrevButton");
	var btnNext = document.getElementById("NextButton");
	var xmlhttp, url;

	btnPrev.disabled = true;
	btnNext.disabled = true;

	if (slideSource == "prev") {
		slideText.innerHTML = document.getElementById("PrevSlideText").innerHTML;
		slideImage.src = document.getElementById("PrevSlideImage").src;
		slideImage.alt = document.getElementById("PrevSlideImage").alt;
		curSlideNum.innerHTML = curSlide;
		message("Loading from Prev");
	} else if (slideSource == "next") {
		slideText.innerHTML = document.getElementById("NextSlideText").innerHTML;
		slideImage.src = document.getElementById("NextSlideImage").src;
		slideImage.alt = document.getElementById("NextSlideImage").alt;
		curSlideNum.innerHTML = curSlide;
		message("Loading from Next");
	}

	xmlhttp = new XMLHttpRequest();
	url = "SlideShow-preloaded?Slide=" + curSlide;
	xmlhttp.open("get", url, true);

	xmlhttp.onreadystatechange = function() {
		if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				changeSlide(xmlhttp);
		}
	}
	xmlhttp.send(null);

	function changeSlide(xmlhttp) { //Callback function creates slide
		var docElem = xmlhttp.responseXML.documentElement;
		var numSlides = document.getElementById("TotalSlideNum");
		var prevSlidePos = (curSlide == 1) ? null : 0;
		var curSlidePos = (curSlide == 1) ? 0 : 1;
		var nextSlidePos = (curSlide == 1) ? 1 : (curSlide == 10) ? null : 2;

		removeWhitespace(docElem, true);

		if (prevSlidePos !== null) {
				prevSlideNode = docElem.childNodes[prevSlidePos];
				loadSlide("Prev", prevSlideNode);
				btnPrev.disabled = false;
		}

		if (nextSlidePos !== null) {
				nextSlideNode = docElem.childNodes[nextSlidePos];
				loadSlide("Next", nextSlideNode);
				btnNext.disabled = false;
		}

		if (slideSource == "ajax") {
				curSlideNode = docElem.childNodes[curSlidePos];
				loadSlide("Cur", curSlideNode);
				message("Loading from Ajax");
		}

		curSlideNum.innerHTML = curSlide;
	}
}

function loadSlide(slide, node) {
	var slideElem = document.getElementById(slide + "Slide");
	var slideText = slideElem.getElementsByTagName("div")[0];
	var slideImage = slideElem.getElementsByTagName("img")[0];
	var name = node.firstChild.firstChild.nodeValue;
	var years = node.childNodes[1].firstChild.nodeValue;
	slideText.innerHTML = name + "<br>" + years;
	slideImage.src = "Slides/" + node.childNodes[2].firstChild.nodeValue;
	slideImage.alt = name;
}

function message(msg) {
	var output = document.getElementById("SlideMessage");
	output.innerHTML = msg;
}

observeEvent(window, "load", function() {
	var btnPrev = document.getElementById("PrevButton");
	var btnNext = document.getElementById("NextButton");
	var totalSlides = 10;
	observeEvent(btnPrev, "click", prevSlide);
	observeEvent(btnNext, "click", nextSlide);
	document.getElementById("TotalSlideNum").innerHTML = totalSlides;
	getSlide(1, "ajax");
});
</script>
</head>

<body>
<h1>First 10 Presidents</h1>
<div id="PrevSlide">
	<img id="PrevSlideImage">
	<div id="PrevSlideText"></div>
</div>
<div id="CurSlide">
	<img id="CurSlideImage">
	<div id="CurSlideText"></div>
	<hr>
	<button id="PrevButton">Previous</button>
	Slide <span id="CurSlideNum">1</span> of <span id="TotalSlideNum"></span>
	<button id="NextButton">Next</button>
	<hr>
	<div id="SlideMessage"></div>
</div>
<div id="NextSlide">
	<img id="NextSlideImage">
	<div id="NextSlideText"></div>
</div>
</body>
</html>

Notice these two divs in the HTML body:

<div id="PrevSlide">
	<img id="PrevSlideImage"/>
	<div id="PrevSlideText"></div>
</div>

<div id="NextSlide">
	<img id="NextSlideImage"/>
	<div id="NextSlideText"></div>
</div>

These divs are simply there to hold the incoming data. SlideShow.css has a commented out line that sets the display property of these divs to "none", so the page appears as below: Page If you were to remove the comment marks, the previous and next slide divs would be hidden.

The images in the upper corners are preloaded (by being in the browser cache, so that new slides load seamlessly). We do it this way for demonstrative purposes only. You could just as easily store the preloaded images in javascript variables.

In the JavaScript code, we have to know the source of the current slide; in other words, from where to get the data for the current slide:

  1. When the page first loads, we'll get the data using an Ajax call.
  2. When the Next button is clicked, we'll get the data from the "NextSlide" div.
  3. When the Previous button is clicked, we'll get the data from the "PrevSlide" div.

So now, when we call getSlide(), we will pass in the slide source:

  1. "ajax" when the page first loads.
  2. "next" when the Next button is clicked.
  3. "prev" when Previous button is clicked.

If the slide source is "prev" or "next, we load the current slide from the data in either the "Next" or "Previous" div based on which button was pushed:

if (slideSource == "prev") {
	slideText.innerHTML = document.getElementById("PrevSlideText").innerHTML;
	slideImage.src = document.getElementById("PrevSlideImage").src;
	slideImage.alt = document.getElementById("PrevSlideImage").alt;
	curSlideNum.innerHTML=curSlide;
	message("Loading from Prev");
} else if (slideSource == "next") {
	slideText.innerHTML = document.getElementById("NextSlideText").innerHTML;
	slideImage.src = document.getElementById("NextSlideImage").src;
	slideImage.alt = document.getElementById("NextSlideImage").alt;
	curSlideNum.innerHTML=curSlide;
	message("Loading from Next");
}

Even if we do load the current slide from the "cache", we still need to make our Ajax call, to repopulate the "caching" divs. In the changeSlide() callback function, we do the following:

  1. Determine the position of each slide within the XML nodes returned. Usually, three nodes get returned and the first (position 0 in JavaScript) is the data for the previous slide, the second (position 1) is the data for the current slide, and the third (position 2) is the data for the next slide. But this isn't the case, when the current slide is the first slide or the last slide. In either of these cases, only two XML nodes are returned. Our code has to know which slides they match:
    var prevSlidePos = (curSlide == 1) ? null : 0;
    var curSlidePos = (curSlide == 1) ? 0 : 1;
    var nextSlidePos = (curSlide == 1) ? 1 : (curSlide == 10) ? null : 2;
  2. We then populate the "caching" divs if their position (as assigned above) is not null:
    if (prevSlidePos !== null) {
    	prevSlideNode = docElem.childNodes[prevSlidePos];
    	loadSlide("Prev",prevSlideNode);
    	btnPrev.disabled=false;
    }
    
    if (nextSlidePos !== null) {
    	nextSlideNode = docElem.childNodes[nextSlidePos];
    	loadSlide("Next",nextSlideNode);
    	btnNext.disabled=false;
    }
  3. We then only populate the current slide in the callback function if the slide source is "ajax." Otherwise, it was already populated from "cache":
    if (slideSource == "ajax") {
    	curSlideNode = docElem.childNodes[curSlidePos];
    	loadSlide("Cur",curSlideNode);
    	message("Loading from Ajax");
    }

Navigable Tables

The same techniques can be used to create navigable tables like the one shown below:Navigable Table

This screenshot shows the preloaded rows with a gray background. In practice, these rows would be hidden.

To view the page in action, first:

  1. Open the command line (on a PC) or terminal (on a Mac), and navigate to the directory AjaxApplications/Solutions/.
  2. Type npm install. (If you've already done this when viewing one of the solutions earlier in this lesson, then no need to do this again - but it isn't a problem to do it again.)
  3. Type npm start to start the Node.js server.

Then open AjaxApplications/Solutions/TableRows.html in your browser to try it out. To hide the preloaded rows, uncomment line 39 (display:none) in TablesRows.css.

You will build this page in the next exercise, but first we'll review some JavaScript functions in our lib.js.

Inserting and Removing Table Elements

There are table-specific methods for adding rows and cells. The helper function show below makes use of these:

function addRow(tableId, cells){
	var tableElem = document.getElementById(tableId);
	var newRow = tableElem.insertRow(tableElem.rows.length);
	var newCell;
	for (var i = 0; i < cells.length; i++) {
		newCell = newRow.insertCell(newRow.cells.length);
		newCell.innerHTML = cells[i];
	}
	return newRow;
}

Table elements have an insertRow() method, which takes one argument: the position at which to insert the row. The thead, tbody, and tfoot elements also have this method, so our addRow function above can take the id of any of the element types as the first argument. The method inserts an empty row which we then can fill with cells.

Table rows have an insertCell() method. It also takes one argument: the position at which to insert the cell. The inserted cell is empty. Our addRow function above populates the cells from the elements in the passed-in cells array.

We have also created a helper function for deleting rows:

function deleteRow(tableId, rowNumber){
var tableElem = document.getElementById(tableId);
	if (rowNumber >= 0 && rowNumber < tableElem.rows.length) {
		tableElem.deleteRow(rowNumber);
		return true;
	} else {
		return false; //no row to delete
	}
}

It uses the deleteRow() method of table, thead, tbody, and tfoot elements to remove the row. If the passed-in rowNumber doesn't exist, the function fails silently by returning false.

Create Navigable Table Rows

Duration: 20 to 30 minutes.

Using AjaxApplications/Demos/SlideShow-preloaded.html as a guide, you will complete the following code in the starting file AjaxApplications/Exercises/TableRows.html where you see the two "Task:" comments:

The server-side script (the TableRows response route in AjaxApplications/Exercises/server.js) expects Row (the current row number) and RowsToShow (the number of rows to show) to be passed in. It will return XML that looks like this:Returned XML The number of nodes will depend on the value of RowsToShow (currently set to 2).

Solution:

AjaxApplications/Solutions/TableRows.html
---- C O D E   O M I T T E D ----

		url = "TableRows?Row=" + curRow + "&RowsToShow=" + rowsToShow;
		xmlhttp = new XMLHttpRequest();
		xmlhttp.open("get", url, true);

		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				changeRows(xmlhttp);
			}
		}
		xmlhttp.send(null);

		function changeRows(xmlhttp) {
			var docElem = xmlhttp.responseXML.documentElement;
			var lastSetStart = totalRows - rowsToShow + 1;
			var prevRowPos = (curRow == 1) ? null : 0;
			var curRowPos = (curRow == 1) ? 0 : rowsToShow;
			var nextRowPos = (curRow == 1) ? rowsToShow :
				(curRow >= lastSetStart) ? null : 2 * rowsToShow;

			removeWhitespace(docElem, true);

			if (prevRowPos !== null) {
				loadRows("Prev", prevRowPos, docElem);
				btnPrev.disabled = false;
			}

			if (nextRowPos !== null) {
				loadRows("Next", nextRowPos, docElem);
				btnNext.disabled = false;
			}

			if (rowSource == "ajax") {
				loadRows("Cur", curRowPos, docElem);
				message("Loading from Ajax");
			}

			curRowStart.innerHTML = curRow;
			curRowEnd.innerHTML = curRow + rowsToShow - 1;

		}
	}
---- C O D E   O M I T T E D ----

We make an Ajax request to /TableRows (served up by the Node.js server), passing GET variables curRow (the current row) and rowsToShow (number of rows to show).

If the Ajax call finishes successfully, we invoke function changeRows, which sets values for the previous row position, current row position, and next row position. If previous/next rows exist, then they are loaded (via function loadRows); if not, then their respective buttons are disabled.