facebook google plus twitter
Webucator's Free Advanced JavaScript Programming Tutorial

Lesson: XHR

Welcome to our free Advanced JavaScript Programming tutorial. This tutorial is based on Webucator's Advanced JavaScript Programming course.

JavaScript's XMLHttpRequest API offers client functionality for transferring data between a client and a server.

Lesson Goals

  • Use the XMLHttpRequest object
  • Learn how to handle XMLHttpRequest responses
  • Set up a local Node.js server
  • Use promises for asynchronous processes
  • Use CORS and JSONP to get around the same origin policy

XMLHttpRequest

The XMLHttpRequest API allows us to transfer data between client and server - an easy way to fetch data from a URL without a page refresh. Often referred to as Ajax, or Asynchronous JavaScript and XML, the XMLHttpRequest API lets us update part of a page in response to user actions: submit a form, request a lookup, etc., all without performing a full http request/response to reload the page.

The XMLHttpRequest Object

Let's review the underlying code to see how the actual Ajax calls and response-handling work. The mechanism for sending data to and retrieving data from the server with Ajax is the XMLHttpRequest object. Until HTML5, the XMLHttpRequest object wasn't officially part of any common specification; however, all the major browsers have supported it for some time.

The HTML5 method is straightforward. It uses a simple XMLHttpRequest() constructor to create the object.

Code Sample:

XHR/Demos/CreateXMLHttpRequest.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript">
	function start() {
		if (window.XMLHttpRequest) {
			var xmlhttp = new XMLHttpRequest();
			document.getElementById("Content").innerHTML =
				"<h1>Using XMLHttpRequest Object</h1>";
		} else {
			var xmlhttp = false;
			document.getElementById("Content").innerHTML =
				"<h1>XMLHttp cannot be created!</h1>";
		}
	}

	window.onload = function() {
		var btn = document.getElementById("btnStart");
		btn.addEventListener('click', start, false);
	}
</script>
<title>XMLHttpRequest</title>
</head>
<body>
	<button id="btnStart">Start</button>
	<div id="Content"></div>
</body>
</html>

Code Explanation

This code attempts to create an XMLHttpRequest object using the XMLHttpRequest() constructor. If it succeeds, it writes out "Using XMLHttpRequest Object" to the body of the page. If it fails, it writes out "XMLHttp cannot be created!"

So, now that we have an XMLHttpRequest object created, what do we do with it? We use it to make HTTP requests. To do so, we initialize the object with the open() method, which takes three arguments.

XMLHttpRequest open() Method Arguments
Argument Description
Request Type String. Usually POST, GET, or HEAD
URL String. The URL receiving the request.
Asynchronous Boolean. Whether the request should be made asynchronously (true) or synchronously (false).

A typical open() method call is shown below.

xmlhttp.open("GET","Demo.xml",true);

Request Types

Although the HTTP specification identifies several methods of HTTP requests, the most commonly supported (and used) methods are GET, POST and HEAD.

HEAD

The HEAD method is the least commonly used of the three; however, for simple requests, it can be all you need. It simply returns the meta-information contained in the HTTP headers. The call would look like this:

xmlhttp.open("HEAD","Demo",true);

And the response might look like this:

Date: Wed, 11 May 2011 15:46:30 GMT
X-Powered-By: ASP.NET
Content-Length: 63
Last-Modified: Tue, 10 May 2011 19:12:27 GMT
Server: Microsoft-IIS/7.5
ETag: "712b13346fcc1:0"
Content-Type: text/xml
Accept-Ranges: bytes

The XMLHttpRequest request is sent as follows:

xmlhttp.send(null);

We'll explain why null is passed in just a moment.

GET

The GET method is used to send information to the server as part of the URL. The server returns the same header information that the HEAD method returns, but it also returns the body of the message (i.e, the content of the page). Any name-value pairs to be processed by the receiving page should be passed along the querystring. The call would look like this:

xmlhttp.open("GET","Demo?FirstName=Nat&LastName=Dunn",true);

The response would be the same as the response shown for the HEAD method followed by the message body, which would typically be simple text, JSON, HTML or XML.

Again, the XMLHttpRequest request is sent as follows:

xmlhttp.send(null);

POST

The POST method is used to send information as an enclosed entity. The call would look like this:

xmlhttp.open("POST","Demo",true);

The response header is somewhat different in that it specifies that the returned content is not cacheable. Like with GET, the message body would typically be plain text, HTML or XML.

The XMLHttpRequest request is sent as follows:

xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
xmlhttp.send("FirstName=Nat&LastName=Dunn");

As you can see, with POST, we first need to set the content type to "application/x-www-form-urlencoded;charset=UTF-8". This tells the server to expect form data. In the send method, we include name-value pairs. These name-value pairs are available to the receiving page for processing.

Data cannot be sent in this manner with the HEAD and GET methods, which is why null was passed in the previous examples.

Asynchronous vs. Synchronous Requests

The asynchronous argument should almost always be set to true. After all, that's the "A" in Ajax. Synchronous calls force the browser to wait for a response from the server before continuing. This leaves the user unable to interact with the browser until the response is complete. Asynchronous requests allow the browser to continue to process code while waiting for a response.

Handling the Response

When using asynchronous calls, we cannot be sure when the response will come, so we must write code that waits for the response and handles it when it arrives. We do this with a callback function. Callback functions are functions that are triggered by some event. In our case, the event we are looking for is a change in the state of the xmlhttp response.

The xmlhttp object's readyState property holds the current state of the response. There are five possible states (0-4), which are described below. Browsers do not necessarily inform you of all states; states 0 and 3 in particular may not appear when you run the demo file.

Values of the readyState Property
State Description
0 uninitialized
1 loading
2 loaded
3 interactive
4 complete

Each change in the readyState is captured by the xmlhttp object's onreadystatechange event handler. We can assign a callback function to this property like this:

xmlhttp.onreadystatechange = function() {
//Do something here
}

In JavaScript, as we have seen, functions are first-class objects and can be assigned to variables or properties of other objects. We could also create a named function and assign that function to xmlhttp.onreadystatechange like this:

xmlhttp.onreadystatechange = handler;

The following sample file illustrates how the readystatechange event is handled.

Code Sample:

XHR/Demos/ReadyStateChange.html
---- C O D E   O M I T T E D ----

<script type="text/javascript">
	function start() {
		var xmlhttp = new XMLHttpRequest();
		var contentDiv = document.getElementById("Content");

		xmlhttp.open("HEAD", "Demo.xml", true);
		xmlhttp.onreadystatechange = function() {
			contentDiv.innerHTML +=
				"Ready State: " + xmlhttp.readyState + "<br>";
		}
		xmlhttp.send(null);
	}
---- C O D E   O M I T T E D ----

Code Explanation

The output will look something like this. The actual ready states returned will depend on your setup.Ready State Change Output

In practice, before doing anything with the xmlhttp response data, we want to make sure the readyState is complete (4), so we put a condition inside our function to check for this:

xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
	//Do something here
}
}

Of course, the real power of using the XMLHttpRequest object is to consume resources from another page, from an external feed, from a database, etc., which we aren't doing in our simple examples above. To perform more interesting tasks using the XMLHttpRequest object, we'll need to run files from a local web server. A little later we'll cover how you can set up a Node.js server on your local computer.

Node.js

Unlike most of the content in this JavaScript course, many of the XMLHttpRequest demos and exercises in this lesson won't work if you run them locally instead of via a webserver. As such, we ask you to use Node.js to serve HTML, CSS, and JavaScript from your computer. Node.js is a "platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices." Much like PHP, ColdFusion, Microsoft .NET, or JavaServer Pages, Node.js (pronounced to rhyme with "Toad Say Yes") is an application server. You might use Node.js to connect to a database (returning a result set from a query, say, or updating a record); deliver HTML, XML, or JSON content; connect to local files; or serve up static web pages like Apache or another web server.

Installing Node.js

Please refer to the setup instructions for installing and running Node.js here: https://www.webucator.com/portal/wiki/index.cfm/JSC401

Running Node.js

We will use Node.js both as a web server to serve static HTML files and their associated CSS and JavaScript components. We will also use Node.js to serve dynamic responses, which will serve as the back end of our Ajax applications. An Ajax-enabled web page might, for instance, allow the user to display details for a desired person by clicking on that person's name on a list displayed on the page; the click would generate an Ajax call to a "behind the scenes" page that queries a database. The Ajax call carries with it the ID of the desired person's record. We'll use Node.js to serve up the "page" that returns the database query. Let's look at a simple example.

A First Node.js Application & Server

In your class files, navigate to the directory XHR/Demos/nodejs/. In it you will find the following files/directories:

  1. package.json
  2. server.js
  3. HelloWorld.pug

package.json defines this particular Node.js application; most importantly, the file defines the Node.js modules needed for this app. Modules enable functionality - connecting to a database, say, or browsing a directory from the web server - and are usually downloaded via npm, the Node.js package manager. All of the Node.js applications in this course will have all the needed modules already installed.

Navigate to the same directory mentioned above from the command line (Terminal on a Mac, Command Prompt on a Windows PC). To do so, on either Windows or Mac, type cd ("change directory") and the path to the directory. On a PC, you will use backslashes ("\"); on a Mac, you will use forward slashes ("/").

On a PC, for instance, one would type cd c:\webucator\ClassFiles\XHR\Demos\nodejs if the class files were stored in a directory named webucator in the root of the c: drive. A Mac user would type cd ~/Documents/webucator/ClassFiles/XHR/Demos/nodejs if the class files were stored in a directory named webucator inside the user's Documents directory; note that, on a Mac, ~ refers to the root of the current user's directory.

Next, type npm install from the command line: this will download the needed modules, as defined in package.json. Node modules will install to the directory node_modules.

After modules have installed, type npm start to start the application.

When needed, press CTRL+c on the command line to stop the Node.js server instance.

server.js defines the application itself: this is the file that runs when you started the Node.js application, which defines how the application makes use of Node.js modules, and which starts the web server on a specific port. Note the when you open up server.js with your web editor, the last line - app.listen(8080); - starts the web server (an HTTP server) and listens on port 8080 so that, once the application is running, visitors can browse to http://localhost:8080 to view files served by and response routes defined by this application.

server.js also defines a set of response routes; in this example, these are the GET routes /HelloWorld and /HelloWorldPug. Each route is a function in server.js, defining the method (usually GET or POST), the actual route (/HelloWorld, for example), and how to respond, defined as an anonymous function. We will review each of these in detail.

app.get('/HelloWorld', function(req, res) {
			var name = req.param('name') || 'Somebody';
			var respondWith = '<?xml version="1.0" encoding="UTF-8"?>';
			respondWith += "<h1>Hello " + name + "!</h1>";
			res.status(200);
			res.setHeader('Content-type', 'text/xml');
			return res.send(respondWith);
		});

The first line of code, app.get('/HelloWorld..., defines a GET route, meaning that, once this Node.js application is running, users will be able to visit http://localhost:8080/HelloWorld in their browser to view the response from this route.

var name = req.param('name') || 'Somebody'; attempts to read a GET parameter from the request made for this response route. If the user browses to http://localhost:8080/HelloWorld?name=Jane, for example, then local variable name will have value "Jane". If the user browses to http://localhost:8080/HelloWorld then local variable name will get the default value "Somebody".

The next few lines of code define the response sent back to the browser: we send back XML, encoded as UTF-8, with a single <h1> tag containing Hello [name]. The very last line, return res.send(respondWith);, sends the response back to the browser.

We will also make use of Pug (formerly known as "Jade"), a templating engine for Node.js, which allows us to define some parts of the response in a template file (usually ending with a .pug extension). Let's look at the other response route defined in our server.js file:

app.get('/HelloWorldPug', function(req, res) {
			var name = req.param('name') || 'Somebody';
			res.render('HelloWorld.pug', {
				'name': name
			}, function(err, html) {
			if (err) {
				res.redirect('/404');
			} else {
				res.status(200).send(html);
			}
		});
	});

Similar to our first example, this response route defines what the user sees when going to http://localhost:8080/HelloWorldPug in their browser. The code which gets the name parameter, or sets its default value, if the parameter is not present, is the same as we saw previously.

Different this time is the manner in which we craft the response sent back to the server. As you might expect, for a more complicated response like a long web page with a considerable volume of content, for example, it quickly becomes unwieldy to build the response string in the response route definition in server.js itself. As such, using Pug templates - as we have done here - allows us to structure much of the response in a separate file, using a simplified syntax.

The line in server.js that starts res.render('HelloWorld.pug', {'name': name}, function(err, html) {... specifies the way in which we return content to the browser. The first parameter tells Node.js that we want to serve up the HelloWorld.pug template as the response for this route. The second parameter ({'name': name}) in our call to render passes our name variable to the template so that the template has the value of name available to it. The final parameter is a callback function by which we actually send back the data; note that we check to see if the template exists and, if not, redirect the user to a "page not found" error. (On a production site our error handling would likely be more robust - checking for more sources of error than just the template not existing.)

In the template HelloWorld.pug:

doctype html
	html(lang="en")
		head
			title Hello World

		body
	h1 Hello #{name}

Pug is a templating engine designed for use with Node.js. It offers a simplified syntax to specify the type of page (an HTML5 page, in our case) and to display content. In our example here, we use Jade to give the page a <title> tag with value Hello World and, on the last line, use interpolation to display the variable name as the value the "Hello" <h1> tag. Recall that the template gets the value of the variable name from the response route: the value defaults to Somebody or gets its value from the GET parameter passed in the request, if present.

You can find more information about the Pug template engine at pugjs.org.

Using the XMLHttpRequest Object

Now we're ready to do something with the data returned. Before looking at an example, let's take a look at the properties and methods of the xmlhttp object, so we know what's available to us.

XMLHttpRequest Object Properties
Property Description
onreadystatechange Specifies the callback function to be triggered when the ready state changes.
readyState Holds the state of the response.
responseText Holds the message body as a string.
responseXML Holds the message body as an XML object.
status Holds the status code returned from the server (e.g, 200 for success, 404 for page not found, etc.).
statusText Holds the status text returned from the server.
XMLHttpRequest Object Methods
Method Description
abort() Aborts the xmlhttp request.
getAllResponseHeaders() Retrieves the values of all the HTTP headers as a string.
getResponseHeader(header) Retrieves the value of the specified HTTP header as a string.
open(Method,URL,Async) Initializes the XMLHttpRequest object.
send(postData) Sends the HTTP request to the server.
setRequestHeader(header,value) Specifies the name and value of an HTTP header.

A common application is to check the status property to make sure that the request was successful (200) and then to output the message body to a div on the HTML page. The following sample file demonstrates this. To run the demo, first start the Node.js server:

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

With the server started, you can then browse to http://localhost:8080/UsingXMLHttpRequest-Get.html to view the page.

Code Sample:

XHR/Demos/UsingXMLHttpRequest-Get.html
---- C O D E   O M I T T E D ----

	function start() {
		var xmlhttp = new XMLHttpRequest();
		var contentDiv = document.getElementById("Content");

		xmlhttp.open("GET", "Demo?FirstName=Nat&LastName=Dunn", true);
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				contentDiv.innerHTML = xmlhttp.responseText;
			}
		}
		xmlhttp.send(null);
	}
---- C O D E   O M I T T E D ----

Code Explanation

This page simply "copies" the response text (xmlhttp.responseText) and "pastes" it into the "Content" div on the page.

Ajax Using the POST Method

Duration: 10 to 15 minutes.

In this exercise, you will modify the code from the last demonstration to send data using the POST method. You will need to use the setRequestHeader() method to alert the server to expect form data.

  1. If needed, stop (by pressing ctrl-c from the command line) any running instance of the Node.js.
  2. Navigate to the directory XHR/Exercises/ in command line.
  3. Type npm install from the command line, to install the needed Node.js modules.
  4. Type npm start to start the Node.js server.
  5. Open XHR/Exercises/UsingXMLHttpRequest-Post.html in your editor.
  6. Modify the code to make the request using the POST method shown earlier.
  7. Test your solution in a browser by visiting http://localhost:8080/UsingXMLHttpRequest-Post.html

Code Sample:

XHR/Exercises/UsingXMLHttpRequest-Post.html
---- C O D E   O M I T T E D ----

<script type="text/javascript">
	function start() {
		var xmlhttp = new XMLHttpRequest();
		var contentDiv = document.getElementById("Content");

		xmlhttp.open("GET", "Demo?FirstName=Nat&LastName=Dunn", true);
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				contentDiv.innerHTML = xmlhttp.responseText;
			}
		}
		xmlhttp.send(null);
	}

	window.onload = function() {
		var btn = document.getElementById("btnStart");
		btn.addEventListener('click', start);
	};
</script>
---- C O D E   O M I T T E D ----

Code Explanation

Challenge

Write out all the response headers to the end of the same "Content" div.

Solution:

XHR/Solutions/UsingXMLHttpRequest-Post.html
---- C O D E   O M I T T E D ----

	function start() {
		var xmlhttp = new XMLHttpRequest();
		var contentDiv = document.getElementById("Content");

		xmlhttp.open("POST", "Demo", true);
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				contentDiv.innerHTML = xmlhttp.responseText;
			}
		}
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("FirstName=Nat&LastName=Dunn");
	}
---- C O D E   O M I T T E D ----

Code Explanation

We specify "POST" as the method for our Ajax call in xmlhttp.open and, in xmlhttp.setRequestHeader specify the MIME type (application/x-www-form-urlencoded) and character encoding (charset=UTF-8) of the request we send to the /Demo response route.

We pass along two POST variables, FirstName and LastName, with values Nat and Dunn, respectively.

Challenge Solution:

XHR/Solutions/UsingXMLHttpRequest-Post-Challenge.html
---- C O D E   O M I T T E D ----

	function start() {
		var xmlhttp = new XMLHttpRequest();
		var contentDiv = document.getElementById("Content");

		xmlhttp.open("POST", "Demo", true);
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				contentDiv.innerHTML = xmlhttp.responseText;
				var headers = xmlhttp.getAllResponseHeaders();
				contentDiv.innerHTML += headers.replace(/\n/g, "<br>");
			}
		}
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("FirstName=Nat&LastName=Dunn");
	}
---- C O D E   O M I T T E D ----

Code Explanation

We use innerHTML to set the contents of #contentDiv, retrieving the response headers with xmlhttp.getAllResponseHeaders() and displaying with newlines converted to HTML <br /> tags.

The Callback Function

In the demos thus far, we have used anonymous callback functions to hanndle the Ajax response, like so:

xmlhttp.onreadystatechange=function() {
 //handle response
}

As these anonymous functions are nested within the original function, they have access to the variable set in the original function. That's why we can reuse the xmlhttp and contentDiv variables within the callback function:

function start() {
	var xmlhttp = new XMLHttpRequest();
	var contentDiv = document.getElementById("Content");

	xmlhttp.open("POST", "Demo", true);
	xmlhttp.onreadystatechange=function() {
		if (xmlhttp.readyState==4 && xmlhttp.status==200) {
			contentDiv.innerHTML=xmlhttp.responseText;
		}
	}
	xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
	xmlhttp.send("FirstName=Nat&LastName=Dunn");
}

But callback functions don't have to be anonymous or nested. Another way to write this is shown below:

               var xmlhttp = new XMLHttpRequest();
var contentDiv = document.getElementById("Content");

function start() {
	xmlhttp.open("POST", "Demo", true);
	xmlhttp.onreadystatechange=myCallBack;
	xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
	xmlhttp.send("FirstName=Nat&LastName=Dunn");
}

function myCallBack() {
	if (xmlhttp.readyState==4 && xmlhttp.status==200) {
		contentDiv.innerHTML=xmlhttp.responseText;
	}
}

The problem with this method is that it uses global variables. This can be problematic as the variables can easily be unintentionally and unexpectedly overwritten. One way around this is to use an anonymous callback function to make the call to the now-pseudo-callback function and pass the necessary variables into it, like this:

function start() {
	var xmlhttp = new XMLHttpRequest();
	var contentDiv = document.getElementById("Content");
	xmlhttp.open("POST", "Demo", true);
	xmlhttp.onreadystatechange=function() {
		myCallBack(xmlhttp, contentDiv);
	};
	xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
	xmlhttp.send("FirstName=Nat&LastName=Dunn");
}

function myCallBack(xmlhttp, contentDiv) {
	if (xmlhttp.readyState==4 && xmlhttp.status==200) {
		contentDiv.innerHTML=xmlhttp.responseText;
	}
}

Another way to handle it is to nest a named callback function within the original function. As JavaScript functions are objects, they can have their own methods, so you can do the following:

function start() {
	var xmlhttp = new XMLHttpRequest();
	var contentDiv = document.getElementById("Content");
	xmlhttp.open("POST", "Demo", true);
	xmlhttp.onreadystatechange=myCallBack;
	xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
	xmlhttp.send("FirstName=Nat&LastName=Dunn");

	function myCallBack() {
		if (xmlhttp.readyState==4 && xmlhttp.status==200) {
			contentDiv.innerHTML=xmlhttp.responseText;
		}
	}
}

Because the callback function is nested within the start() function, it has access to the xmlhttp and contentDiv variables. The only limitation of this method is that the myCallBack() function is not callable from outside the start() function. Depending on what you're trying to do, that may or may not be an issue.

Displaying and Updating Records

Duration: 30 to 40 minutes.

In this exercise, you will create a mini-application for displaying and updating employee records. The server-side files are already created and much of the JavaScript is written for you. You will have to finish the Ajax portions of the code.

  1. From the command line, navigate to the directory XHR/Exercises/.
  2. Type npm start to start the Node.js server.
  3. Note that the code already created for you makes an Ajax call to /EmployeeList. Generated by Node.js, this call returns a list of the employees, as shown below:
    <ul>
    	<li id="emp1">Nancy Davolio</li>
    	<li id="emp2">Andrew Fuller</li>
    	<li id="emp3">Janet Leverling</li>
    	<li id="emp4">Margaret Peacock</li>
    	<li id="emp5">Steven Buchanan</li>
    	<li id="emp6">Michael Suyama</li>
    	<li id="emp7">Robert King</li>
    	<li id="emp8">Laura Callahan</li>
    	<li id="emp9">Anne Dodsworth</li>
    </ul>
    Notice the ids of the list items are the employee id from the database prefixed with "emp".
  4. Open XHR/Exercises/EmployeeAdmin.html for editing. This is where you'll do your work.
    1. In the getEmployeeList() function where the comment is:
      • open an asynchronous XMLHttpRequest using the GET method and the URL passed into the function.
      • When the readystate of the request changes, call the display() function with the appropriate parameters.
      • Send the request.
    2. In the display() function:
      • Set the innerHTML property of output div to the responseText of the returned xmlhttp object.
      • Test your solution to this piece by opening XHR/Exercises/EmployeeAdmin.html in your browser. You should see a list of the employees. If you do not, check your getEmployeeList() and display() functions.
    3. Finish the getEmployeeForm() function by setting the appropriate content type for submitting form data and sending the request.
    4. In the updateEmployee() function:
      • Write the code leading up to the call to send() method call.
      • When the readystate of the request changes, call the employeeUpdated() function.
  5. Test your solution in a browser by visiting http://localhost:8080/EmployeeAdmin.html

Challenge

In the employeeUpdated() callback function, we currently call getEmployeeList() to update the list of employees. This updates the entire list after each change. It would be better to just update the record that was changed. There is no need to make a call to the database to find out which record it was or how it was changed. The client already has that information. See if you can figure out how to remove this unnecessary Ajax call and use pure client-side JavaScript to update the list. Be careful not to update the list until you are sure that the record has been successfully updated in the database. The only file you will need to change is EmployeeAdmin.html.

Solution:

XHR/Solutions/EmployeeAdmin.html
---- C O D E   O M I T T E D ----

	function getEmployeeList(url) {
		var xmlhttp = new XMLHttpRequest();
		var output = document.getElementById("EmployeeList");
		output.innerHTML = "<h2>Loading...</h2>";
		xmlhttp.open("GET", url, true);
		xmlhttp.onreadystatechange = function() {
			display(output, xmlhttp);
		}
		xmlhttp.send(null);
	}

	function display(output, xmlhttp) {
		var employees, eid, target, i;
		if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
			output.style.display = "block";
			output.innerHTML = xmlhttp.responseText;

			if (xmlhttp.responseText.indexOf("Failed") == 0) {
				output.className = "Warning";
			} else {
				employees = output.getElementsByTagName("li");
				for (i = 0; i < employees.length; ++i) {
					employees[i].addEventListener('click', function(e) {
						target = e.target;
						eid = target.id.substring(3);
						getEmployeeForm("EmployeeForm", eid);
					});
				}
			}
		}
	}

	function getEmployeeForm(url, eid) {
		var xmlhttp = new XMLHttpRequest();
		var output = document.getElementById("FormDiv");
		var fields, field, value, i;
		output.innerHTML = "Loading...";
		xmlhttp.open("POST", url, true);
		xmlhttp.onreadystatechange = function() {
			display(output, xmlhttp);
			fields = output.getElementsByTagName("input");
			for (i = 0; i < fields.length; ++i) {
				fields[i].addEventListener('change', function(e) {
					target = e.target;
					field = target.name;
					value = target.value;
					updateEmployee("EditEmployee", field, value, eid);
				});
			}
		}
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("eid=" + eid);
	}

	function updateEmployee(url, field, value, eid) {
		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open("POST", url, true);
		xmlhttp.onreadystatechange = employeeUpdated;
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("eid=" + eid + "&field=" + field + "&value=" + value);

		function employeeUpdated() {
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				var msg = document.getElementById("MessageDiv");
				if (xmlhttp.responseText.indexOf("Failed") == 0) {
					msg.className = "Warning";
					msg.innerHTML = xmlhttp.responseText;
				} else {
					msg.innerHTML = "Updated!";
					getEmployeeList('EmployeeList');
				}
				fadeElem(msg, 255, 255, 0, 255, 255, 255);
			}
		}
	}
---- C O D E   O M I T T E D ----

Code Explanation

Open http://localhost:8080/EmployeeAdmin.html in your browser to view the solution; be sure to start the Node.js server (by typing npm start from the command line) in the XHR/Solutions/ directory first.

Function getEmployeeList, invoked when the page loads, makes an Ajax call to /EmployeeList and invokes function display. If the Ajax calls is successful, then function display displays the returned list of employees in div#EmployeeList.

Also in function display we iterate over each of the bulleted names, listening for clicks. A click on any of the names results in a call to function getEmployeeForm, passing along the id of the clicked-upon employee.

Function getEmployeeForm, in turn, makes an Ajax request, via the POST method, to /EmployeeForm, displays the returned form, and adds change handlers on each of the fields of the form.

As a result of the handlers added in function getEmployeeForm, a change to any of the employee-form fields invokes function updateEmployee which, in turn, makes a POST Ajax request to /EditEmployee, sending along the id of the employee, field name, and field value. The function displays a success or failure message based on the status of the returned results.

Challenge Solution:

XHR/Solutions/EmployeeAdmin-challenge.html
---- C O D E   O M I T T E D ----

	function getEmployeeForm(url, eid) {
		var xmlhttp = new XMLHttpRequest();
		var output = document.getElementById("FormDiv");
		var fields, field, value, i, fname, lname;
		output.innerHTML = "Loading...";
		xmlhttp.open("POST", url, true);
		xmlhttp.onreadystatechange = function() {
			display(output, xmlhttp);
			fields = output.getElementsByTagName("input");
			for (i = 0; i < fields.length; ++i) {
				fields[i].addEventListener('change', function(e) {
					target = e.target;
					field = target.name;
					value = target.value;
					fname = target.form.FirstName.value;
					lname = target.form.LastName.value;
					updateEmployee("EditEmployee", field, value, eid, fname, lname);
				});
			}
		}
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("eid=" + eid);
	}

	function updateEmployee(url, field, value, eid, fname, lname) {
		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open("POST", url, true);
		xmlhttp.onreadystatechange = function() {
			employeeUpdated(eid, fname, lname);
		};
		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
		xmlhttp.send("eid=" + eid + "&field=" + field + "&value=" + value);

		function employeeUpdated(eid, fname, lname) {
			var employee, msg;
			if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
				msg = document.getElementById("MessageDiv");
				if (xmlhttp.responseText.indexOf("Failed") == 0) {
					msg.className = "Warning";
					msg.innerHTML = xmlhttp.responseText;
				} else {
					msg.innerHTML = "Updated!";
					employee = document.getElementById("emp" + eid);
					employee.innerHTML = fname + ' ' + lname;
				}
				fadeElem(msg, 255, 255, 0, 255, 255, 255);
			}
		}
	}
---- C O D E   O M I T T E D ----

Code Explanation

Open http://localhost:8080/EmployeeAdmin-challenge.html in your browser to view the solution.

In function getEmployeeForm, we now keep track of the first and last name (from the fields with ids FirstName and LastName, respectively) of each employee. We pass these values as parameters in the call to updateEmployee when any of the fields is changed.

Function updateEmployee works largely as before but, upon successful update of the employee (i.e. a successful Ajax response code), the function now sets the displayed name of the relevant employee in the bulleted list.

Promises

We use promises in JavaScript for asynchonous processes: a promise represents a proxy - a stand-in - for a value not known at the time, but that is expected in the future. Promises have the following syntax:

new Promise(function(resolve, reject) {
	// body of function
});

The function supplied to the Promise is the executor, a function that will be passed to other functions via the resolve and reject arguments. The resolve and reject functions are bound to the promise; calling them fulfills (the resolve function) or rejects (the reject function) the promise. The executor function initiates some asynchronous process - once that completes, the function calls either the resolve or reject function to resolve the promise's final value, or rejects it if there is an error.

A Promise always exists in one of these states:

  • Pending: the initial state (not fulfilled nor rejected).
  • Fulfilled: the operation completed successfully.
  • Rejected: the operation failed.

A pending promise can become fulfilled with a value or rejected with a reason (an error). When either of these occurs, the handlers associated by the promise's then method are called.

At its simplest, we might use a promise for a case like the following:

var prom = new Promise(function(resolve, reject) {
	// perform some asynchronous task...
	if(/* successful */) {
		resolve('success');
	} else {
		reject('failure');
	}
});
	prom.then(function() {
	/* handle result */
}).catch(function() {
	/* error */
})

The following tables list Promise properties and methods:

Promise Properties
Property Description
Promise.length Length property whose value is 1 (number of constructor arguments)
Promise.prototype The prototype for the Promise constructor.
Promise Methods
Method Description
Promise.all(iterable) Returns a promise that either resolves when all of the promises in the iterable argument have resolved or rejects as soon as one of the promises in the iterable argument rejects. If the returned promise resolves, it is resolved with an array of the values from the resolved promises in the iterable. If the returned promise rejects, it is rejected with the reason from the promise in the iterable that rejected.
Promise.race(iterable) Returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.
Promise.reject(reason) Returns a Promise object that is rejected with the given reason.
Promise.resolve(value) Returns a Promise object that is resolved with the given value. If the value is a thenable (i.e. has a then method), then the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value.

Let's look at how we can use promises to work with the XMLHttpRequest object:

Code Sample:

XHR/Demos/promise.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Promise</title>
<script type="text/javascript">
	function get(url) {
		return new Promise(function(resolve, reject) {
			var req = new XMLHttpRequest();
			req.open('GET', url);
			req.onload = function() {
				if (req.status == 200) {
					resolve(req.response);
				} else {
					reject(Error(req.statusText));
				}
			};
			req.onerror = function() {
				reject(Error("Network Error"));
			};
			req.send();
		});
	}

	get('/promise').then(function(response) {
		var msgDiv = document.getElementById("msg");
		msgDiv.innerHTML = response;
	}).catch(function(error) {
		var msgDiv = document.getElementById("msg");
		msgDiv.innerHTML = error;
	});
</script>

</head>
<body>

<div id="msg"></div>
</body>
</html>

Code Explanation

Our function get instantiates and returns a new Promise, invoking resolve or reject based on the status returned by the XMLHttpRequest() for the url parameter passed to get. We call get('/promise').then to handle the response - in our case, writing either (success or failure) to the same div, but we could have, of course, processed these different cases in different manners.

CORS/JSONP: Accessing Remote Sites

Most of the time, accessing scripts from one domain to another - for instance, from example1.com to example2.com - isn't permitted because the same-origin policy allows scripts to run only if they match in protocol (http vs. https, for example), hostname (example1.com vs. example2.com), and post (port 80 or 443 for http or https traffic, by default). For obvious reasons, browsers enforce these rules to block potentially-malicious exploits: it would be a poor sort of Internet on which clicking any link opened up threats from other sites.

Specifically, it is the response from the foreign-site script that our browser prevents us from consuming: Ajax requests to an external site are sent along, but - in the absence of some other mechanism - the response isn't accepted by our browsers.

Of course, there are times when we want to explicitly allow remote access, either sharing our own resources with external sites or purposefully leveraging resources available on foreign sites for our own purposes. In this lesson, we look at two strategies for accessing foreign-site resources: Cross-Origin Resource Sharing (CORS) and JSON with Padding (JSONP).

CORS

Cross-Origin Resource Sharing (CORS) is a mechanism for requesting fonts, scripts, and other resources from an origin (defined, as above, as the combination of domain, protocol, and port) other than the requesting origin. For instance, if sending a request from http://www.example.com, any of the following would be "cross origin":

  • http://mail.example.com (domain differs),
  • https://www.example.com (protocol differs),
  • http://www.example.com:8080 (port differs),

and, thus, scripts (or font, or other similar resources) would be blocked from these "foreign" sites. CORS offers a way for two sites to allow safe sharing of resources.

How CORS Works

CORS defines the communication between browser and server: specific headers in the HTTP request and HTTP response tell the browser that its OK to accept the resource. At its most basic, a server issuing an HTTP response which includes the header

Access-Control-Allow-Origin: *

is allowing access from all requesting domains. A more-restrictive response, for example

Access-Control-Allow-Origin: http://www.example.com

would allow access only from a particular domain.

The great thing for us, as web developers, is that CORS-enabled responses work just like responses from our own (same-origin) site: our code can process the JSON, XML, or other response we receive just as if we were making a request of a page or resource on our own server.

The CORS request/response cycle can get significantly more complex, with "preflight" requests sent by the browser and responded to from the server, before another set of request/response; the passing of cookies or other authentication mechanisms; and other sharing of data. Check out https://www.html5rocks.com/en/tutorials/cors/, an excellent tutorial on HTML5 Rocks, to delve deeper into the topic.

Check out http://enable-cors.org/resources.html#apis for a list of sites that offer CORS-enabled resources.

Let's look at an example, accessing a CORS-enabled site and a not-CORS-enabled site. Open XHR/Demos/cors-html5-rocks.html in your browser and in a code editor to review the code. No need to start the Node.js server for any of the following demos and exercises.

Code Sample:

XHR/Demos/cors-html5-rocks.html
<!DOCTYPE HTML>
<html>
<head>
---- C O D E   O M I T T E D ----

<script>

	window.onload = function() {
		var btnhtml5rocks = document.getElementById('btnhtml5rocks');
		var responseContent = document.getElementById('responseContent');
		btnhtml5rocks.addEventListener('click', function(e) {
			responseContent.innerHTML = '<h2>Response Content</h2>';
			
			var xmlhttp = new XMLHttpRequest();

			xmlhttp.open("GET", "https://www.html5rocks.com/en/tutorials/file/xhr2/", true);
			xmlhttp.onreadystatechange = function() {
				if (xmlhttp.readyState == 4) {
					if (xmlhttp.status == 200) {
						responseContent.innerHTML += xmlhttp.responseText;
					} else {
						alert("failed!");
					}
				}
			}
			xmlhttp.send(null);
		});

		btnnytimes.addEventListener('click', function(e) {
			responseContent.innerHTML = '<h2>Response Content</h2>';;
			
			var xmlhttp = new XMLHttpRequest();

			xmlhttp.open("GET", "http://www.nytimes.com/", true);
			xmlhttp.onreadystatechange = function() {
				if (xmlhttp.readyState == 4) {
					if (xmlhttp.status == 200) {
						responseContent.innerHTML += xmlhttp.responseText;
					} else {
						alert("failed!");
					}
				}
			}
			xmlhttp.send(null);
		});
	};
</script>
</head>
<body>
	<button id="btnhtml5rocks">Fetch HTML5 Rocks</button>
	<button id="btnnytimes">Fetch NY Times</button>
	<br>
	<div id="responseContent"></div>
</body>
</html>

Code Explanation

The page presents two buttons, with ids btnhtml5rocks and btnnytimes, respectively; we add a click handler to each button

When either button is clicked, we make an Ajax call to a remote site, setting the contents of #responseContent div, if successful in our Ajax call, to the contents received as the response.

Clicking the "Fetch HTML5 Rocks" button generates an Ajax call to https://www.html5rocks.com/en/tutorials/file/xhr2/. We display the contents of the response on our page.

Despite our making a call to a cross-origin (i.e. non-local) site, our code works. Specifically, it is the presence of the Access-Control-Allow-Origin: * response header that tells our browser it is OK to allow this Ajax call:

HTML5 Rocks

Clicking the "Fetch NY Times" button, conversely, doesn't work: we get no response and, thus, generate a popup alert. If we inspect the response headers, we would find no Access-Control-Allow-Origin: * among them. If we check the console, we find that our browser complains of our attempt to violate the same origin policy:

NY Times

In large part, CORS depends on the server responding with the appropriate headers; if that is the case then, conveniently, our work as client-side developers becomes relatively easy, pretty much the same as if we were working with resources on our own server.

Let's have you try out a call to a remote data source that sends back CORS-enabled headers.

JSONP

JSON with Padding, or JSONP, exploits a loophole in the same-origin policy which browsers employ to prevent access to resources passed via scripts from foreign sites. Instead of passing JSON-formatted data back in an Ajax call, as we do when using Ajax from within our own site, the JSONP response instead returns the JSON-formatted data as the argument of a callback function - "padded" (the "P" in JSONP) by the callback function. Most of the time, our JSONP call to the external resource specifies the name we want for the callback function; on our end, as we receive the "padded" JSON-formatted data, we then invoke the function to process the data. Without this padding - without wrapping the JSON data in a callback function - the security policies in place in our browsers would not allow us to access the foreign resources.

We we've seen previously in this course, a non-JSONP Ajax call might result in the following data returned from another page on our own domain:

{ name: 'Nat' }

With JSONP, the return content might look as such:

callbackfunction({ name: 'Nat' });

On our end, we treat the returned data as a call to callbackfunction() and, if we define what callbackfunction() should do, then we can make use of the JSON-formatted data returned by the foreign server. For example:

callbackfunction = function(data){
  alert(data.name);
};

would popup an alert displaying "Nat", since the result of our JSONP call invokes callbackfunction with a JavaScript object with name name and value Nat.

Let's look at an example using a JSONP call to Yahoo! using their Query Language service.

Code Sample:

XHR/Demos/jsonp-yahooquery.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP - Yahoo! Query Language</title>
<script>
	function displayInfo(data) {
		var content = document.getElementById('content');
		content.innerHTML = '';
		var results = data.query.results.Result;
		for(i=0; i<results.length;i++) {
			result = results[i];
			content.innerHTML += '<p><a href="' + result.MapUrl + '" target="_blank">' + result.Title + '</a></p>';
		}
	}

	function requestJSONP(url) {
		var script = document.createElement('script');
		script.src = url;
		script.onload = function () {
			this.remove();
		};
		document.head.appendChild(script);
	}

	window.onload = function() {

		var btn = document.getElementById('btn');

		btn.addEventListener('click', function(e) {
			var zip = document.getElementById('zip').value;
			var query = document.getElementById('query').value;
			var script = document.createElement('script');
			var url = "http://query.yahooapis.com/v1/public/yql?format=json&q=select+*+from+local.search+where+zip='" + zip + "'+and+query='" + query + "'&callback=displayInfo"
			requestJSONP(url);
		});
	}
</script>
</head>
<body>
	<input type="text" id="zip" placeholder="zip"> <input type="text" id="query" placeholder="query (e.g. pizza)">
	<button id="btn">Go</button>
	<div id="content"></div>
</body>
</html>

Code Explanation

Enter a zip code and a type of establishment for which to search (e.g. "pizza", "movies", "supermarkets").

We add a click handler on the button: when the button is clicked, three things happen:

  1. Local variable zip is assigned the value entered in the #zip text field.
  2. Local variable query is assigned the value entered in the #query text field.
  3. We create a URL string, appending to it a search query in the format dictated by Yahoo!'s Query Language service, embedding the user-entered zip code and item (like "pizza") into the string.
  4. We initiate a JSONP call:
    • We call function requestJSONP, passing to it our URL string.
    • Function requestJSONP appends a script tag to the head of our page.
    • The callback parameter we added to the URL invokes our callback function displayInfo, to which is passed the JSON response as the data parameter.
    • Function displayInfo processes the returned JSON, iterating over the relevant results and appending each item (each pizza place, for example) to the #content div.

The Yahoo! Query Language service offers a variety of available resources: we choose here to ask for "local" results (usually businesses). As such, as we pass - via the data parameter, a query that sends along the user-supplied zip code and query. We might, for instance, ask for all "pizza" places near a zip code like "13214" using the query select * from local.search where zip='13214' and query='pizza'. Here's a screenshot of https://developer.yahoo.com/yql/ with that query:

Pizza Results

The next exercise asks you to try out using JSONP.

CORS Vs. JSONP Differences

  • CORS is the more modern of the two approaches to cross-origin resource sharing.
  • CORS supports a variety of HTTP requests; JSONP supports only GET requests.
  • Additionally, CORS allows us to use a regular XMLHttpRequest, which offers better error handling than does JSONP.
  • Conversely, more (older) web browsers support JSONP than do CORS.

Retrieving Country Info from GeoNames via CORS

Duration: 15 to 25 minutes.

In this exercise, you will make an Ajax call to a cross-origin site which includes appropriate response headers to enable CORS.

  1. Geonames.org offers a wide range of geographical data, returned in both XML and JSON format. Visit http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=US&username=webucator&style=full to view the response from the resource we will access.
  2. Note that the response includes the Access-Control-Allow-Origin: * header: Country Info
  3. Note, too, the schema of the response: a field geonames contains an array (n this case with a single element) which contains fields with information about the given country: countryName, population, etc.
  4. Open XHR/Exercises/cors-geonames.html in your editor; you'll write the code here:
    • Get the value of the #countryabbr input field: this is the two-character country abbreviation - "US" for the United States, "CN" for China, "DE" for Germany, etc.
    • Use XMLHttpRequest to retrieve date from Geonames:
      • The type of this Ajax call should be get.
      • The call should go to http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=XX&username=webucator&style=full, where "XX" is the two-character country abbreviation supplied by the user.
      • Parse the response: check if the country code was valid by checking the length of the returned response; if there was a response with valid data, then display on the page the country's name, population, and any other fields you wish. If not - that is, if JSON.parse(xmlhttp.responseText).geonames.length == 0, then display an error message.
  5. Test your solution in a browser by visiting XHR/Exercises/cors-geonames.html

Solution:

XHR/Solutions/cors-geonames.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>CORS - Geonames</title>
<style>
	#responseContent {
		width:66%;
		padding:1% 2%;
		background-color:#ccc;
		margin-top:20px;
	}
	h2 {
		margin:0;
		padding:0;
	}
</style>
<script>
	window.onload = function() {
		var btn = document.getElementById('btn');
		var responseContent = document.getElementById('responseContent');

		btn.addEventListener('click', function(e) {
			responseContent.innerHTML = '<h2>Response Content</h2>';
			var country = document.getElementById('countryabbr').value;

			var xmlhttp = new XMLHttpRequest();

			xmlhttp.open("GET", "http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=" + country + "&username=webucator&style=full", true);
			xmlhttp.onreadystatechange = function() {
				if (xmlhttp.readyState == 4) {
					if (xmlhttp.status == 200) {
						var jsonResponse = JSON.parse(xmlhttp.responseText);
						if (jsonResponse.geonames.length > 0) {
							responseContent.innerHTML += '<p>';
							responseContent.innerHTML += 'Name: ' + jsonResponse.geonames[0].countryName + '<br>';
							responseContent.innerHTML += 'Currency Code: ' + jsonResponse.geonames[0].currencyCode + '<br>';
							responseContent.innerHTML += 'Capital City: ' + jsonResponse.geonames[0].capital + '<br>';
							responseContent.innerHTML += 'Population: ' + jsonResponse.geonames[0].population + '<br>';
							responseContent.innerHTML += '</p>';
						} else {
							responseContent.innerHTML += '<p><em>Country not found</em></p>';
						}
					} else {
						alert("failed!");
					}
				}
			}
			xmlhttp.send(null);
		});
	};
</script>
</head>
<body>
	<input type="text" id="countryabbr" placeholder="US">
	<button id="btn">Fetch Country Info</button>
	<br>
	<div id="responseContent"></div>
</body>
</html>

Code Explanation

Our Ajax calls goes out to

http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country='+country+'&username=webucator&style=full'

where country is the user-entered value from the text field.

We check that the country code we send along in our Ajax call results in a valid response; if so, then we append to the #responseContent div some information about the country. If the response is empty, then we display "Country not found".

Retrieving Place Information from Yahoo! with JSONP

Duration: 15 to 25 minutes.

In this exercise, you will put to use the JSONP concepts we reviewed previously, using Yahoo! to retrieve information about a user-entered place.

  1. Visit https://developer.yahoo.com/yql/
    • Enter the following as the YQL Query:
      select * from geo.places where text='sfo'
    • Choose response type "JSON" and click the "Test" button.
    • Yahoo! returns information about "sfo" - the San Francisco airport in California, US.
    • Check the schema for the returned data: the top-level field is query, which contains an object with field results, which in turn contains a field place.
    • For this query ("sfo") the returned field place contains a number of fields like placeTypeName, whose field content tells us that this is an "Airport", and name, which tells us the full name of "sfo".
    • If we change the query to select * from geo.places where text='13066' - a United States zip code - we see that the query.results.place is now an array: we get results for US zip code "13066" but also for Mexican postal code "130", Brazilian postal code "13066", etc. We'll need to handle this fact - that Yahoo! will sometimes return an array of places and sometimes just a single place.
  2. Open XHR/Exercises/jsonp-yahooquery.html; you'll write the code here:
    • The event-handling code is done for you, getting the user-entered value from the #text input field.
    • Where indicated, create an appropriate URL to send to YQL to get information about the user-entered place.
    • Complete the body of the function displayPlace: test whether the returned result is an array or not (using Array.isArray()) and either iterate over the results or handle the single returned result to set the innerHTML of the #content div.
    • Test your work in a browser.
  3. Test your solution in a browser by visiting XHR/Exercises/jsonp-yahooquery.html

Solution:

XHR/Solutions/jsonp-yahooquery.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP - Yahoo! Query Language</title>
<script type="text/javascript">
	function displayPlace(data) {
		var content = document.getElementById('content');
		content.innerHTML = '';
		var results = data.query.results.place;
		if (Array.isArray(results)) {
			for(i=0; i<results.length;i++) {
				place = results[i];
				content.innerHTML += '<p>' + place.placeTypeName.content + ': ' + place.name + ' (country: ' + place.country.content + ')</p>';
			}
		} else {
			content.innerHTML += '<p>' + results.placeTypeName.content + ': ' + results.name + ' (country: ' + results.country.content + ')</p>';
		}
	}

	function requestJSONP(url) {
		var script = document.createElement('script');
		script.src = url;
		script.onload = function () {
			this.remove();
		};
		document.head.appendChild(script);
	}

	window.onload = function() {

		var btn = document.getElementById('btn');

		btn.addEventListener('click', function(e) {
			var text = document.getElementById('text').value;
			var script = document.createElement('script');
			var url = "http://query.yahooapis.com/v1/public/yql?format=json&q=select+*+from+geo.places+where+text='" + text + "'&callback=displayPlace";
			requestJSONP(url);
		});
	}
</script>
</head>
<body>
	<input type="text" id="text" placeholder="place">
	<button id="btn">Go</button>
	<div id="content"></div>
</body>
</html>

Code Explanation

We create the URL var url = "http://query.yahooapis.com/v1/public/yql?format=json&q=select+*+from+geo.places+where+text='" + text + "'&callback=displayPlace";, setting the query to send to YQL from the user-entered place name and referencing our callback function displayPlace.

In the displayPlace callback, we first set the contents of div#Content to be empty, so that repeated clicks of the button "wipe out" any previous displayed data.

We then check to see if we get an array returned; if so, we iterate (with a for loop) over the array of returned results, each time appending a new paragraph to div#Content with the type (place.placeTypeName.content) and name (place.name) and country (place.country.content) of the place. If there is no array, we append a single paragraph with the results.