Preloaded Data

Contact Us or call 1-877-932-8228
Preloaded Data

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.

Next