An Aynchronous Javascript (AJAX) application uses the XMLHttpRequest object from Javascript to load data within the user's Web browser without navigating to a new Web page. The following code example demonstrates calling the Elance Developer API from an AJAX application to display a table of jobs that contain the keywords "php" and "mysql." The displayed records are sorted by the number of bids placed on each job, in ascending order (i.e., the jobs with the lowest bids are listed first).

A major complication arises when using the Elance Developer API within AJAX that you don't encounter when using it from a Web server-based development platform such as PHP: the XMLHttpRequest object forbids cross-domain data access. If you're programming your AJAX application on a host named somerandomhost.com, most Web browsers will forbid you from using XMLHttpRequest to call api.elance.com directly.

There are a couple of workarounds to this problem. In the example below, instead of calling api.elance.com directly, we wrap calls to the Elance Developer API in a PHP script that returns the API's JSON response directly to XMLHttpRequest. This example assumes that the PHP script and the HTML file containing our AJAX application reside on the same Web server. Note that the server component does not have to be written in PHP; a Web server application written in ASP.NET, Ruby on Rails, or another application framework would also work, so long as the file we call returns raw JSON output.

I. Writing a PHP Wrapper

We have already discussed in Authenticating a User with OAuth 2.0 and PHP how to access the Elance Developer API from PHP. Our wrapper script will follow the same steps, except at the end it will pass back the JSON-formatted response returned from Elance directly to the client.

NOTE: In this example, the Elance Developer API access token is hard-coded

<?php
 
$current_page = 1;
$results_per_page = 25;
if (isset($_GET["currentPage"])) { 
    is_numeric($_GET["currentPage"]) or die ("ERROR: currentPage parameter MUST be an integer!");
    intval($_GET["currentPage"]) > 0     or die ("ERROR: currentPage must be greater than 0!");
    $current_page = $_GET["currentPage"];
}
 
if (isset($_GET["resultsPerPage"])) { 
    is_numeric($_GET["resultsPerPage"]) or die ("ERROR: resultsPerPage parameter MUST be an integer!");
    intval($_GET["resultsPerPage"]) > 0     or die ("ERROR: resultsPerPage must be greater than 0!");
    $results_per_page = $_GET["resultsPerPage"];	
}
 
$access_token = "509d3290e4b036807231ad90|2614718|w38I1OiDsQ6w264IoZtbOQ";
 
// Open eLance URL. 
$url = "http://api.elance.com/api2/jobs?access_token=" . $access_token . "&keywords=php%20mysql&" . 
    "sortCol=numProposals&sortOrder=asc&page=" . $current_page . "&rpp=" . $results_per_page;
 
if (($r = @curl_init($url)) == false) {
    header("HTTP/1.1 500", true, 500);
    die("Cannot initialize cUrl session. Is cUrl enabled for your PHP installation?");
}
 
// Set cUrl to return text as a variable, instead of directly to the browser.
$curl_options = array (
    CURLOPT_FRESH_CONNECT => 1,
    CURLOPT_RETURNTRANSFER => 1
    );
curl_setopt_array($r, $curl_options);
 
// Access API, and check results.
$json_txt = curl_exec($r);
if (curl_errno($r) > 0) {
    header("HTTP/1.1 500", true, 500);
    die();
} else {
    $http_response = intval(curl_getinfo($r, CURLINFO_HTTP_CODE));
    if ($http_response != 200) {
        // Pass on any descriptive error information from the Elance server to 
        // the client.
        header("HTTP/1.1 " . $http_response, true, $http_response);
        header("Content-Type: application/json", true);
        echo($json_txt);
        flush();
        die();
    }
}
 
if ($json_txt == false) {
    header("HTTP/1.1 500", true, 500);
    die("Cannot retrieve Elance API URL using cUrl. URL: " . $url);
}
curl_close($r); 
 
header("Content-Type: application/json", true);
echo $json_txt;
 
?>

II. Accessing the PHP Wrapper from AJAX

Now that we have a localized wrapper for our Elance Developer API calls, we can call it from JavaScript. The following example calls the code defined above using XMLHttpRequest, and adds it to a pre-defined table in the Web page using Javascript commands. The results of the query are displayed 25 records at a time. Previous and Next links are provided to enable navigating between pages of response data.

The file defines five Javascript functions. initLoadJobsTable() initializes XMLHttpRequest by calling loadXMLHttpRequest(), and displays the first page of results from the query. callElanceAPIProxy() initiates an asynchronous call to our PHP script, which in turn calls the Elance Developer API. When the call to our script has returned, XMLHttpRequest calls onElanceResponseReady(), our callback method, to convert the response from JSON text to Javascript objects. Finally, populateJobsTable() loads the result set into an HTML TABLE object.

The JSON object returned by the server is converted to a Javascript object using the native Javascript JSON parser, if available, or the Javascript eval() function. The native JSON object is available in Internet Explorer 8, Firefox 3.5+, Google Chrome, and Apple Safari. Parentheses are placed around the JSON string to prevent the eval function from throwing an error.

<html>
 
<head>
 
<title>Elance AJAX/JSON Example</title>
 
<script type="text/javascript">
 
var jsonRequest = null;
var currentPage;
var resultsPerPage = 25;
 
function initLoadJobsTable() {
    currentPage = 1;
 
    // Retrieve JSON data from Elance, using our own Web server as a proxy.
    try {
        loadXMLHttpRequest();            
    } catch (e) {
        window.alert("Your browser does not support AJAX. You can't use this page. Get a better browser!");
    }
 
    callElanceAPIProxy(currentPage);
}
 
// Instantiate XMLHttpRequest object in a browser-independent way. Throw error if we can't load it.
function loadXMLHttpRequest() {
    if (window.XMLHttpRequest) {
        jsonRequest = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        try {
            jsonRequest = new ActiveXObject("Msxml2.XMLHTTP");
        } catch(e) {
            try {
                jsonRequest = new ActiveXObject("Microsoft.XMLHTTP");
            } catch(e) {
                throw(e);
            }
        }
    } else {
        throw("Cannot instantiate XMLHttpRequest");
    }
}
 
// Make an asynchronous call to the Elance Developer API.
function callElanceAPIProxy(currentPage) {
    // Inform user we're loading new records, and disable UI controls.
    displayRecordsBox.innerHTML = "<strong>Loading...</strong>";
    prevNextBox.innerHTML = "";
 
    jsonRequest.open("GET", "/elance/get-jobs-json.php?currentPage=" + currentPage + "&resultsPerPage=" + resultsPerPage, true);
    jsonRequest.onreadystatechange = onElanceResponseReady;
    jsonRequest.send(null);
}
 
// The aynschronous callback for our call to the Elance Developer API.
function onElanceResponseReady() {
    if (jsonRequest.readyState != 4) {
        return;
    }
 
    if (jsonRequest.status != 200) {
        // Report error to user.
	window.alert("Bad response from server. Status code: " + jsonRequest.status);
	return;
    } 	
 
    var serverResponse = jsonRequest.responseText;
 
    // Convert the response to a Javascript object. 
    // Use the native parser if available, as it is faster and more secure. The 
    // native parser is available in IE8, Firefox 3.5+, and Chrome. 
    var jsonResponse;
    if (JSON) {
	jsonResponse = JSON.parse(serverResponse);
    } else {
	jsonResponse = eval('(' + serverResponse + ')');
    }
 
    // Populate the HTML table.
    populateJobsTable(jsonResponse);
}
 
// Populate the HTML table with the list of results for this query.
function populateJobsTable(jsonObject) {
    var resTab = document.getElementById("resultsTable");
 
    // Clear out old entries first.
    var rowsColl = resTab.tBodies[0].rows;
    if (rowsColl.length > 0) {
	var rowCount = rowsColl.length;
	for (i = 0; i < rowCount; i++) {
	    // Always delete row 0, as this is a live collection in most browsers. I.e., if a table
	    // has 25 rows, and you delete row 0, then row 24 becomes row 23.
	    resTab.tBodies[0].deleteRow(0);
	}
    }
 
    // Tell the user which records they are seeing out of the total result set.
    var pageInt = parseInt(jsonObject.data.page);
    var maxResultNum = pageInt * resultsPerPage;
    var firstRecNum = maxResultNum - (resultsPerPage - 1);
    var topOfCurrentRange = (pageInt - 1) * resultsPerPage + parseInt(jsonObject.data.numResults);
 
    var spanText = "Displaying " + firstRecNum + " to " + topOfCurrentRange + " out of " + jsonObject.data.totalResults + " total results";
    displayRecordsBox.innerHTML = spanText;
 
    // Display Previous and Next links.
    var navText = "";
    var sepText = "";
    if (pageInt > 1) {
        navText += "<a href='javascript:callElanceAPIProxy(" + (pageInt - 1) + ");'>< Previous</a>&nbsp;";
        sepText = "|&nbsp;";
    }
    if (pageInt != parseInt(jsonObject.data.totalPages)) {
        navText += sepText;
        navText += "<a href='javascript:callElanceAPIProxy(" + (pageInt + 1) + ");'>Next ></a>&nbsp;";
    } 
 
    prevNextBox.innerHTML = navText;
 
    if (resTab != null) {
        var pageResults = jsonObject.data.pageResults;
        for (x in pageResults) {    
            var newRow = resTab.tBodies[0].insertRow(parseInt(x));
            var newCell = newRow.insertCell(0);
            newCell.innerHTML = pageResults[x].jobId;
 
            newCell = newRow.insertCell(1);
            newCell.innerHTML = "<a href='" + pageResults[x].jobURL + "'>" + pageResults[x].name + "</a>";
 
            newCell = newRow.insertCell(2);
            newCell.innerHTML = pageResults[x].budget;
 
            newCell = newRow.insertCell(3);
	    newCell.innerHTML = trimWords(pageResults[x].description, 50);
            newCell.style.wordWrap = "break-word";
 
            newCell = newRow.insertCell(4);
            newCell.innerHTML = pageResults[x].numProposals;
            newCell.style.textAlign = "center";
        }
    }
}
 
function trimWords(sentence, numWords) {
    var result = sentence;
    var resultArray = result.split(" ");
 
    if(resultArray.length > numWords){
        resultArray = resultArray.slice(0, numWords);
        result = resultArray.join(" ") + "...";
    }
 
    return result;
}
 
</script>
 
</head>
 
<body onload="initLoadJobsTable();">
 
<div style="width:100%; text-align:center;">
<h1>Elance API Search Results for Low-Bid PHP/mySql Jobs</h1>
 
<div style="width:75%;position:relative;"> 
    <span id="displayRecordsBox" style="width:37%;position:absolute;left:0;text-align:left;"></span> 
    <span id="prevNextBox" style="width:37%;position:absolute; right:0; text-align:right;"></span> 
</div>
<div style="padding-top:20px;">
<table id="resultsTable" style="width:75%;table-layout:fixed;">
    <thead>
        <tr style="background-color:#cccccc;font-weight:bold;">
                <th style="width:10%;">Job ID</th>
                <th style="width:30%;">Job Title</th>
                <th style="width:20%;">Budget</th>
                <th style="width:30%;">Description</th>
                <th style="width:10%;"># of Bids</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>    
</div>
 
</body>
 
</html>