Chapter 18. Communication

18.0. Introduction

This book has explored a lot of subjects, but the functionality that revolutionized web client development is (arguably) Ajax. With Ajax, we’re no longer dependent on the slow, cumbersome round-trips and associated page load times that plagued early websites. Now we can make an Ajax call, get some data, update the page—sometimes without the user even being aware that the activity is happening.

Ajax is also a relatively uncomplicated functionality, at least compared to other JavaScript functionality I’ve covered in the book. The main steps to an Ajax application are:

  • Prepare the server-side API call.

  • Make the call.

  • Process the result.

Of course, there are interesting challenges that can occur at any time during these three steps, but for a basic application, it really is just that simple.

Ajax is now joined by a new communication kid: the postMessage. This new functionality originated with HTML5, though it has since split off to its own specification. It’s an uncomplicated functionality that allows for easy communication between a parent and child window, even if the child window is located in another domain.

Note

There are two other new communication APIs in work: Cross Origin Resource Sharing (CORS) and the Web Sockets API. Both are being developed in the W3C, and both are currently in Working Draft state: CORS at http://www.w3.org/TR/access-control/ and Web Sockets at http://dev.w3.org/html5/websockets/.

CORS is a way of doing cross-domain Ajax calls, and is currently implemented in Firefox 3.5 and up, and Safari 4.x and up. The Web Sockets API is a bidirectional communication mechanism, implemented only in Chrome at this time.

18.1. Accessing the XMLHttpRequest Object

Problem

You want to access an instance of the XMLHttpRequest object.

Solution

If you’re not concerned about support for IE6, you can use the following:

var xmlHttp = new XMLHttpRequest();

This works with all of the target browsers for this book, and is the only method I used in examples in the book. However, if you must still support IE6 and you’re not using one of the JavaScript libraries, you’ll need to use the following cross-browser code:

if (window.XMLHttpRequest) {
   xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
   xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}

Discussion

Microsoft invented the XMLHttpRequest object as an ActiveX object. However, the XMLHttpRequest object that we know and primarily use today, even in newer versions of IE, evolved independently. There’s now an effort underway to standardize the object within the W3C.

The XMLHttpRequest object isn’t very complicated. Here are the supported client application methods, which are explored in more depth in the other recipes in this chapter:

open

Initializes a request. Parameters include the method (GET or POST), the request URL, whether the request is asynchronous, and a possible username and password. By default, all requests are sent asynchronously.

setRequestHeader

Sets the MIME type of the request.

send

Sends the request.

sendAsBinary

Sends binary data.

abort

Aborts an already sent request.

getResponseHeader

Retrieves the header text, or null if the response hasn’t been returned yet or there is no header.

getAllResponseHeaders

Retrieves the header text for a multipart request.

All of our target browsers support the methods just listed. There’s also an additional, frequently used method, overrideMimeType, not included in the list. I didn’t include it because it’s not part of the XMLHttpRequest standardization process, and one browser company doesn’t support it (Microsoft).

The overrideMimeType method is normally used to override the MIME type of the server response., As an example, the following overrides whatever the server’s response is, and the returned resource is treated as XML:

xmlhttp.overrideMimeType('text/xml');

Lack of support for overrideMimeType is an inconvenience but not a showstopper. Either we’ll need to process the data according to MIME type or ensure that our server applications set the proper content header for the data. For instance, if we want to ensure that our client application receives data as XML, we can use the following in a PHP application:

header("Content-Type: text/xml; charset=utf-8");

There are also a number of properties supported by all browsers:

status

The HTTP result status of the request response.

statusText

The response text returned from the server.

readyState

The state of the request.

responseText

The text-based response to the request.

responseXML

The response to the request as an XML-based DOM object.

There are other properties, some proprietary and some not, but these are the ones we’re concerned with in this book.

A major restriction associated with the XMLHttpRequest object is the same-origin security restriction. This means that you can’t use XMLHttpRequest to make a service request to an API in another domain.

See Also

Recipe 18.7 provides a solution and a discussion related to the same-origin restriction with XMLHttpRequest. The W3C XMLHttpRequest draft specification can be found at http://www.w3.org/TR/XMLHttpRequest/.

18.2. Preparing the Data for Transmission

Problem

You want to process form data for an Ajax call rather than send the data via the usual form submit process.

Solution

Access the data from form fields or other page elements:

var state = document.getElementById("state").value;

If the data is user-supplied, such as the data from a text field, encode the result using the encodeURIComponent function, so any characters in the text that could impact on the Ajax call are escaped:

var state = encodeURIComponent(document.getElementById("state").value);

You shouldn’t need to escape data from radio buttons, checkboxes, selections, or any other form element in which your application controls the data, as you can make sure those values are in the proper format.

Discussion

Depending on the type of action, data for an Ajax call can come from user-supplied text, such as that typed into a text field. When it is, you’ll need to ensure that the data can be used in the Ajax request by escaping certain characters, such as an ampersand (&), plus sign (+), and equal sign (=).

The following string:

This is $value3 @value &and ** ++ another

Is encoded as:

This%20is%20%24value3%20%40value%20%26and%20**%20%2B%2B%20another

The spaces are escaped as %20, the dollar sign as %24, the ampersand as %26, and the plus sign as %2B, but the alphanumeric characters and the reserved asterisk characters are left alone.

Once escaped, the data can be attached as part of an Ajax request. If the Ajax request is going to be a POST rather than a GET request, further encoding is needed—the spaces should be encoded as pluses. Replace the %20 with +, following the call to encodeURIComponent. You can package this functionality for reuse:

function postEncodeURIComponent(str) {
   str=encodeURIComponent(str);
   return str.replace(/%20/g,"+");
}

The escaping ensures that the Ajax request will be successfully communicated, but it doesn’t ensure that the Ajax request is safe. All data input by unknown persons should always be scrubbed to prevent SQL injection or cross-site scripting (XSS) attacks. However, this type of security should be implemented in the server-side application, because if people can put together a GET request in JavaScript, they can put together a GET request directly in a browser’s location bar and bypass the script altogether. The only time you need to scrub the input in JavaScript is if you’re planning on embedding a user’s data directly back into the page.

18.3. Determining the Type of Query Call

Problem

You’re not sure whether to send your Ajax call as a GET or a POST.

Solution

For an update, send a POST request. Set the first parameter in the XMLHttpRequest open method to POST, call the setRequestHeader to set the content-type, and send the request parameters in the send method:

xmlhttp.open('POST',url,true);
xmlhttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xmlhttp.send(param);

For a query, send a GET request. The first parameter in the XMLHttpRequest open method is set to GET, the parameters are appended on to the URL for the request, and null is passed as parameter in the send method:

url = url + "?" + param;
xmlhttp.open('GET',url,true);
xmlhttp.send(null);

Discussion

What the server component of the application is expecting has the most impact on your request type decision. However, accepted practice is that a request for data should be made with a GET, while an update should occur through a POST.

The request type practice used in Ajax is derived from RESTful guidelines (REST translates as REpresentational State Transfer). According to the guidelines, there are four types of HTTP requests:

GET

Used for retrieving information, with parameters attached to the URL.

POST

Used for creating new data, and parameters are sent via function parameter.

DELETE

Used for deleting data records, and parameters are sent via function parameter.

PUT

Used to send an update, and parameters are sent via function parameter.

There is broad support only for the GET and POST requests at this time, so I’ll focus on these two.

A GET HTTP request is used to retrieve information, and the parameters are attached to the URL in the request. A function that processes a GET request could look like the following, which passes two parameters with the request:

function sendData(evt) {

  // cancel default form submittal
  evt = evt || window.event;
  evt.preventDefault();
  evt.returnValue = false;

  // get input data
  var one = encodeURIComponent(document.getElementById("one").value);
  var two = encodeURIComponent(document.getElementById("two").value);
  var params = "one=" + one + "&two=" + two;

  // prep request
  if (!http) {
     http = new XMLHttpRequest();
  }
  var url = "ajaxserver.php?" + params;
  http.open("GET", url, true)

  // callback function
  http.onreadystatechange=processResult;

  // make Ajax call with params
  http.send(null);
}

In the code snippet, two parameters are passed with the request. First, they’re escaped, using encodeURIComponent. Next, they’re attached to the URL using RESTful GET notation:

http://somecompany.com?param=value&param2=value2

The parameters for the XMLHttpRequest open method are:

GET

A GET request

url

The URL for the service

true

Whether the operation is performed asynchronously

The optional third asynchronous parameter should always be set to true, or the page blocks until the server request returns. This is not a cool thing to do to your page readers.

There are two other optional parameters not shown: username and password. If the application is protected on the server side, the username and password can be used for authentication.

A POST request with two parameters would look like the following:

function sendData(evt) {

  // cancel default form submittal
  evt = evt || window.event;
  evt.preventDefault();
  evt.returnValue = false;

  // get input data
  var one = encodeURIComponent(document.getElementById("one").value).
  replace(/%20/g,'+');
  var two = encodeURIComponent(document.getElementById("two").value).
  replace(/%20/g,'+');
  var params = "one=" + one + "&two=" + two;

  // prep request
  if (!http) {
     http = new XMLHttpRequest();
  }
  var url = "ajaxserver.php";
  http.open("POST", url, true)

  // set up Ajax headers
  http.setRequestHeader("Content-Type",
  "application/x-www-form-urlencoded");
  http.setRequestHeader("Content-length", params.length);
  http.setRequestHeader("Connection", "close");

  // callback function
  http.onreadystatechange=processResult;

  // make Ajax call with params
  http.send(params);
}

This code differs from the code for the GET request in several ways. The first is that after encoding, the spaces in the parameters are converted from %20 to +. The second is they’re concatenated into a parameter-formatted string, which is sent within the send method.

The first parameter of the open method is set to POST, but the other two parameters are the same as those sent with the GET request: the application URL and the asynchronous flag.

Additional calls are made to the setRequestHeader method, to set Connection and Content-length request headers. You can use it to send any HTTP request header, but you must provide the Content-Type for the POST—in this case, a multipart form request.

Both of the request approaches set the callback function for the Ajax object call’s onreadystatechange event handler.

See Also

Recipe 18.1 covers how to get the XMLHttpRequest object, and Recipe 18.2 covers how to encode parameters.

18.4. Adding a Callback Function to an Ajax Request

Problem

You want to process the result of an Ajax request, even if the result does an update rather than a query.

Solution

When processing the Ajax request, before calling the XMLHttpRequest object’s send method, assign the object’s onreadystatechange property to the callback function’s name:

xmlhttp.open("GET", url, true);
xmlhttp.onreadystatechange=callbackFunction;
xmlhttp.send(null);

Discussion

The readyState attribute for the XMLHttpRequest object is updated based on the state of the request. In order to check the state of the request, you need to assign the onreadystatechange event handler to a callback function that is called every time the ready state changes.

You should use onreadystatechange with every Ajax call, even one in which you’re doing an update rather than processing a request. Without the callback function, you have no way of knowing if the update operation was successful, and if not, what kind of error occurred.

The readyState property has the values shown in Table 18-1 during the Ajax request.

Table 18-1. Values of XMLHttpRequest readyState property

Value

STATE

Purpose

0

UNINITIALIZED

open has not been called yet

1

LOADING

send has not been called yet

2

LOADED

send has been called

3

INTERACTIVE

Downloading response is not completed

4

COMPLETED

Request is complete

You should not set onreadystatechange if your request is synchronous, because the code won’t continue until the request returns anyway—you can check the result of the operation in the very next line of the code, if you’re so inclined.

Note

I can’t stress strongly enough: please avoid synchronous Ajax calls. Locking the page up until the request finishes is just not a good thing.

See Also

Recipe 18.5 covers how to check the status and results of an Ajax request.

18.5. Checking for an Error Condition

Problem

You want to check the status of the Ajax request.

Solution

In addition to checking the readyState property in the onreadystatechange event handler, you can also check the XMLHttpRequest’s status:

function processResult() {
   if (http.readyState == 4 && http.status == 200) {
      document.getElementById("result").innerHTML=http.responseText;
   }
}

Discussion

The XMLHttpRequest’s status property is where the response to the request is returned. You hope for a value of 200, which means the request is successful. If the request is something else, such as 403 (forbidden) or 500 (server error), you can access the XMLHttpRequest’s statusText property to get more detailed information:

function processResult() {
   if (http.readyState == 4 && http.status == 200) {
      document.getElementById("result").innerHTML=http.responseText;
   } else {
      alert(http.statusText);
   }
}

18.6. Processing a Text Result

Problem

You want to process HTML returned as text.

Solution

If you trust the server application, and the text is formatted to use immediately in the page, the simplest approach is to assign the text to the innerHTML property of the element where it should be placed:

function processResult() {
   if (http.readyState == 4 && http.status == 200) {
      document.getElementById("result").innerHTML=http.responseText;
   }
}

Discussion

If you’re writing both the server and client sides of the application, why make it hard on yourself? Format the response in the server application in such a way that it can be added into the web page using the easiest possible approach, and nothing is easier than innerHTML.

However, if the response text isn’t formatted for immediate publication, you’ll have to use String functions, possibly in combination with regular expressions, to extract the data you need. If HTML formatting isn’t possible or desirable, and you have any control of the server application, try to format it either as XML or JSON, both of which can be more easily supported in the JavaScript environment.

See Also

See Recipe 19.1 for extracting information from an XML response. See Recipe 19.4 and Recipe 19.5 for converting JSON formatted text into a JavaScript object. See Recipe 12.1 for an introduction to innerHTML.

18.7. Making an Ajax Request to Another Domain (Using JSONP)

Problem

You want to query for data using a web service API, such as the Netflix API or Twitter’s API. However, the Ajax same-origin policy prevents cross-domain communication.

Solution

The most commonly used technique to solve the cross-domain problem, and the approach I recommend, is to create a server-side proxy application that is called in the Ajax application. The proxy then makes the call to the other service’s API, returning the result to the client application. This is safe, secure, and very efficient, because we can clean the data before returning it to the Ajax application.

There is another approach: use JSONP (JSON, or JavaScript Object Notation, with Padding) to workaround the security issues. I once used this approach to create a mashup between Google Maps and a Flickr query result:

function addScript( url) {
   var script = document.createElement('script');
   script.type="text/javascript";
   script.src = url;
   document.getElementsByTagName('head')[0].appendChild(script);

The URL looked like the following, including a request to return the data formatted as JSON, and providing a callback function name:

http://api.flickr.com/services/rest/?method=flickr.photos.search&user_id=xxx&api_ke
y=xxx&format=json&
jsoncallback=processPhotos

When the script tag is created, the request to Flickr is made, and since I passed in the request for a JSON formatted result and provided a callback function name, that’s how the return was provided. The callback string is similar to the following:

// assign photos globally, call first to load
function processPhotos(obj) {
  photos = obj.photos.photo;
  ...
}

The callback function would pass the data formatted as a JSON object in the function argument.

Discussion

Ajax works within a protected environment that ensures we don’t end up embedding dangerous text or code into a web page because of a call to an external application (which may or may not be secure).

The downside to this security is that we can’t directly access services to external APIs, such Flickr, Twitter, and Google. Instead, we need to create a server-side proxy application, because server applications don’t face the cross-domain restriction.

The workaround is to use something like JSONP, demonstrated in the solution. Instead of using XMLHttpRequest, we convert the request URL into one that we can attach to a script’s src attribute, because the script element does not follow the same-origin policy. It couldn’t, or we wouldn’t be able to embed applications such as Google Maps into the application.

If the service is amenable, it returns the data formatted as JSON, even wrapping it in a callback function. When the script is created, it’s no different than if the function call is made directly in our code, and we’ve passed an object as a parameter. We don’t even have to worry about converting the string to a JavaScript object.

It’s a clever trick, but I don’t recommend it. Even with secure services such as Flickr and Twitter, there is the remote possibility that someone could find a way to inject JavaScript into the data via the client-side application for the service, which can cause havoc in our own applications.

It’s better to be smart then clever. Use a proxy application, scrub the result, and then pass it on to your client application.

See Also

Chapter 19 covers JSON in more detail.

18.8. Populating a Selection List from the Server

Problem

Based on a user’s actions with another form element, you want to populate a selection list with values.

Solution

Capture the change event for the trigger form element:

document.getElementById("nicething").onchange=populateSelect;

In the event handler function, make an Ajax call with the form data:

var url = "nicething.php?nicething=" + value;
xmlhttp.open('GET', url, true);
xmlhttp.onreadystatechange = getThings;
xmlhttp.send(null);

In the Ajax result function, populate the selection list:

   if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
     var select = document.getElementById("nicestuff");
     select.length=0;
     var nicethings = xmlhttp.responseText.split(",");
     for (var i = 0; i < nicethings.length; i++) {
       select.options[select.length] =
new Option(nicethings[i],nicethings[i]);
     }
     select.style.display="block";
   } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) {
     document.getElementById('nicestuff').innerHTML =
'Error: Search Failed!';
   }

Discussion

One of the more common forms of Ajax is to populate a select or other form element based on a choice made by the user. Rather than have to populate a select element with many options, or build a set of 10 or 20 radio buttons, you can capture the user’s choice in another form element, query a server application based on the value, and then build the other form elements based on the value—all without leaving the page.

Example 18-1 demonstrates a simple page that captures the change event for radio buttons within a fieldset element, makes an Ajax query with the value of the selected radio button, and populates a selection list by parsing the returned option list. A comma separates each of the option items, and new options are created with the returned text having both an option label and option value. Before populating the select element, its length is set to 0. This is a quick and easy way to truncate the select element—removing all existing options, and starting fresh.

Example 18-1. Creating an on-demand select Ajax application
<!DOCTYPE html>
<head>
<title>On Demand Select</title>
<style>
#nicestuff
{
  display: none;
  margin: 10px 0;
}
#nicething
{
  width: 400px;
}
</style>
<script>

var xmlhttp;

function populateSelect() {

  var value;

  var inputs = this.getElementsByTagName('input');
  for (var i = 0; i < inputs.length; i++) {
    if (inputs[i].checked) {
      value = inputs[i].value;
      break;
    }
  }

  // prepare request
  if (!xmlhttp) {
    xmlhttp = new XMLHttpRequest();
  }
  var url = "nicething.php?nicething=" + value;
  xmlhttp.open('GET', url, true);
  xmlhttp.onreadystatechange = getThings;
  xmlhttp.send(null);
}
// process return
function getThings() {
   if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
     var select = document.getElementById("nicestuff");
     select.length=0;
     var nicethings = xmlhttp.responseText.split(",");
     for (var i = 0; i < nicethings.length; i++) {
       select.options[select.length] = new Option(nicethings[i],
nicethings[i]);
     }
     select.style.display="block";
   } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) {
     alert("No items returned for request");
   }
}

window.onload=function() {

   document.getElementById("submitbutton").style.display="none";
   document.getElementById("nicething").onclick=populateSelect;
}

</script>

</head>
<body>
<form action="backupprogram.php" method="get">
<p>Select one:</p>
<fieldset id="nicething">
<input type="radio" name="nicethings" value="bird" /><label
for="bird">Birds</label><br />
<input type="radio" name="nicethings" value="flower" /><label
for="flower">Flowers</label><br />
<input type="radio" name="nicethings" value="sweets" /><label
for="sweets">Sweets</label><br />
<input type="radio" name="nicethings" value="cuddles" />
<label for="cuddles">Cute Critters</label>
</fieldset>
<input type="submit" id="submitbutton" value="get nice things" />
<select id="nicestuff"></select>
</body>

The form does have an assigned action page, and a submit button that’s hidden when the script is first run. These are the backup if scripting is turned off.

18.9. Using a Timer to Automatically Update the Page with Fresh Data

Problem

You want to display entries from a file, but the file is updated frequently.

Solution

Use Ajax and a timer to periodically check the file for new values and update the display accordingly.

The Ajax we use is no different than any other Ajax request. We’ll use a GET, because we’re retrieving data. We put together the request, attach a function to the onreadystatechange event handler, and send the request:

var url = "updatedtextfile.txt";
xmlhttp.open("GET", url, true);
xmlhttp.onreadystatechange=updateList;
xmlhttp.send(null);

The fact that we’re doing a direct request on a static text file might be new, but remember that a GET request is more or less the same as the requests we put into the location bar of our browsers. If something works in the browser, it should successfully return in an Ajax GET request...within reason.

In the code that processes the response, we just place the new text into a new unordered list item and append it to an existing ul element:

// process return
function processResponse() {
   if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
     var li = document.createElement("li");
     var txt = document.createTextNode(xmlhttp.responseText);
     li.appendChild(txt);
     document.getElementById("update").appendChild(li);
   } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) {
     alert(xmlhttp.responseText);
   }
}

The new part is the timer. The timer is controlled with start and stop buttons. When the start button is clicked the first time, the code disables the start button first, initiates the first Ajax call, and then starts a timer. There’s a reason for this, as we’ll see in a second:

// timer
function startTimer() {
  populateList();
  timer=setTimeout(timerEvent,15000);
}

The reason we want to make the Ajax call first is that the timer function timerEvent checks the readyState of the XMLHttpRequest object when it’s invoked. If the value is not 4, which means the last request was completed, it doesn’t make another Ajax call. We don’t want to have multiple requests out at the same time:

function timerEvent() {
  if (xmlhttp.readyState == 4) {
    populateList();
  }
  timer=setTimeout(timerEvent, 15000);
}

Lastly, we’ll add a cancel timer event. In this case we’re using a global timer variable, but in a production application we want to use either an anonymous function to wrap everything, or create an object literal to maintain both data and methods:

function stopTimer() {
  clearTimeout(timer);
}

Discussion

The key to using timers with Ajax calls is to make sure that the last call is completed before making the next. By including a check on the XMLHttpRequest object’s readyState property, if the value isn’t 4, we know to skip this Ajax call and just reset the timer for the next go round. We can also put in a check for the request status, and cancel the timer event altogether if we’re concerned about hitting a failing service, over and over again.

When I ran the application that included the solution code, I changed the text file by using the Unix echo command:

$ echo "This is working" > text.txt

And then watched as the text showed up on the page, as shown in Figure 18-1.

Demonstration of updates from polled Ajax calls
Figure 18-1. Demonstration of updates from polled Ajax calls

If you’re planning on using this form of polling with another service, such as against the Twitter API, be aware that if you’re considered abusive of the service, you may get kicked off. Check to see if there are restrictions for how often you can access a service using the API.

Note

Depending on the browser, you may run into caching issues if you access the text.txt file locally. Providing a full URL should prevent this from occurring.

A few years ago, there was interest in a push rather than pull type of Ajax communication. Encompassed under the coined term of Comet, the concept was that the server would initiate the communication and push the data to the client, rather than the client pulling the data from the server. Eventually, the concept led to work in the W3C on a new JavaScript API called WebSockets. Currently only implemented in Chrome, WebSockets enables bidirectional communication between server and client by using the send method on the WebSocket object for communicating to the server, and then attaching a function to WebSocket’s onmessage event handler to get messages back from the server, as demonstrated in the following code from the Chromium Blog:

 if ("WebSocket" in window) {
  var ws = new WebSocket("ws://example.com/service");
  ws.onopen = function() {
    // Web Socket is connected. You can send data by send() method.
    ws.send("message to send"); ....
  };
  ws.onmessage = function (evt) { var received_msg = evt.data; ... };
  ws.onclose = function() { // websocket is closed. };
} else {
  // the browser doesn't support WebSocket.
}

Another approach is a concept known as long polling. In long polling, we initiate an Ajax request as we do now, but the server doesn’t respond right away. Instead, it holds the connection open and does not respond until it has the requested data, or until a waiting time is exceeded.

See Also

See Recipe 14.8 for a demonstration of using this same functionality with an ARIA live region to ensure the application is accessible for those using screen readers. The W3C WebSockets API specification is located at http://dev.w3.org/html5/websockets/, and the Chrome introduction of support for WebSockets is at http://blog.chromium.org/2009/12/web-sockets-now-available-in-google.html.

18.10. Communicating Across Windows with PostMessage

Problem

Your application needs to communicate with a widget that’s located in an iFrame. However, you don’t want to have to send the communication through the network.

Solution

Use the new HTML5 postMessage to enable back-and-forth communication with the iFrame widget, bypassing network communication altogether.

One or both windows can add an event listener for the new message event. To ensure the event handling works with IE as well as Opera, Firefox, Safari, and Chrome, using object detection:

function manageEvent(eventObj, event, eventHandler) {
   if (eventObj.addEventListener) {
      eventObj.addEventListener(event, eventHandler,false);
   } else if (eventObj.attachEvent) {
      event = "on" + event;
      eventObj.attachEvent(event, eventHandler);
   }
}

The sender window that has the iFrame prepares and posts the message to the widget window:

function sendMessage() {
  try {
    var farAwayWindow =
document.getElementById("widgetId").contentWindow;
    farAwayWindow.postMessage(
"dragonfly6.thumbnail.jpg,Dragonfly on flower",
                              'http://burningbird.net');
  } catch (e) {
    alert(e);
  }
};

Two parameters are required for the HTML5 implementation of postMessage: the first is the message string; the second is the target window’s origin. If the iFrame window’s source is something like http://somecompany.com/test/testwindow.html, then the target origin would be http://somecompany.com. You could also use “*” for the target origin. This is a wildcard, which means the value will match to any origin.

In the receiving window, an event listener picks up the message and responds accordingly. In this example, the string is split on the comma (,); the first part of the string is assigned the image element’s src property, and the second is assigned the image element’s alt property:

function receive(e) {

   var img = document.getElementById("image");
   img.src = e.data.split(",")[0];
   img.alt = e.data.split(",")[1];

    e.source.postMessage("Received " + e.data, "*");
}

In the code, the widget window responds with a postMessage of its own, but uses the wildcard origin. The widget window also doesn’t check to see what domain sent the message. But when it responds, the host window checks origin:

function receive(e) {
  if (e.origin == "http://burningbird.net")
    ... does something with response message
}

Discussion

The postMessage functionality is based on listener/sender functionality. Example 18-2 contains an example sender page. It contains an iFrame in which a requested image is displayed when the cross-document communication finishes. When you click on the web page, a message is posted to the listener to parse the message, find the name for a photograph, and add the photograph to an img element in the page.

Example 18-2. Sender page
<!DOCTYPE html>
<head>
<title>Sender</title>
<script>

function manageEvent(eventObj, event, eventHandler) {
   if (eventObj.addEventListener) {
      eventObj.addEventListener(event, eventHandler,false);
   } else if (eventObj.attachEvent) {
      event = "on" + event;
      eventObj.attachEvent(event, eventHandler);
   }
}

window.onload=function() {

  manageEvent(document.getElementById("button1"),"click",sendMessage);
  manageEvent(window,"message",receive);

}

// make sure to change URL to your location
function sendMessage() {
  try {
    var farAwayWindow =
document.getElementById("widgetId").contentWindow;
    farAwayWindow.postMessage(
"dragonfly6.thumbnail.jpg,Dragonfly on flower",
                              'http://jscb.burningbird.net');
  } catch (e) {
    alert(e);
  }
};

// change URL to your location
function receive(e) {
  if (e.origin == "http://jscb.burningbird.net")
    alert(e.data);
}
</script>

</head>
<body>
<div><button id="button1">Load the photo</button></div>
<iframe src="example18-3.html" id="widgetId"></iframe>
</body>

Example 18-3 contains the listener page.

Example 18-3. Listener page
<!DOCTYPE html>
<head>
<title>Listener</title>
<script>

function manageEvent(eventObj, event, eventHandler) {
   if (eventObj.addEventListener) {
      eventObj.addEventListener(event, eventHandler,false);
   } else if (eventObj.attachEvent) {
      event = "on" + event;
      eventObj.attachEvent(event, eventHandler);
   }
}

window.onload=function() {

  manageEvent(window,"message",receive);

}

function receive(e) {

   var img = document.getElementById("image");
   img.src = e.data.split(",")[0];
   img.alt = e.data.split(",")[1];

    e.source.postMessage("Received " + e.data,
"http://burningbird.net");
}
</script>

</head>
<body>
<img src="" id="image" alt="" />
</body>

Figure 18-2 shows the page after the successful communication.

Result of postMessage communication and loading an image
Figure 18-2. Result of postMessage communication and loading an image

The new postMessage will soon be joined by a message channel capability, and both are attuned to use with widgets. This capability enables a higher level of interactivity between the hosting window and the widget that wasn’t possible before, either because of network activity or because of same-origin security restrictions.

The loosening of security restrictions is also a risk associated with postMessage. We can help mitigate the risk by following some simple rules:

  • Use the targetOrigin rather than the wildcard (*) when posting a message.

  • Use the origin value in the return message object to ensure the sender is the expected agent.

  • Check and double-check message data received. Needless to say, don’t pass it to eval, and avoid plunking the data directly into a page using innerHTML.

Companies providing widgets can also add a layer of security. For instance, when a person downloads a widget, he also registers the domain where the widget is used. This is the same functionality that Google uses when you use the Google Maps API: you register the domain where the map API is used, and if the domain differs, the API doesn’t work.

The widget company can use this to check the message’s origin, and also use this information when sending any postMessage response. In addition, the widget company can use whatever precaution necessary with the sent data and respond in kind.

Get JavaScript Cookbook now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.