Web & Mobile Development JLW288

06.Ajax

Ajax?

  • Acronym: Asynchronous JavaScript and XML
    • Term coined in 2005 by Jesse James Garret
    • Technology pre-dated 2005 though
  • In Short
    • Ajax allows you to do requests to a web server without ever leaving the webpage
  • Example
    • Click a link in the navigation of a webpage and see the content get updated dynamically

XMLHttpRequest

  • At the core of Ajax sits a JavaScript API named XMLHttpRequest (XHR)
    • It allows you to do a request to a web server
    • Calls can be made synchronously and asynchronously
  • Other parts of Ajax are DOM Manipulations and XML
    • The data (text) returned by the server is XML-formatted (*)
(* by now other formats are more popular)

Example

  • Synchronous example
    xhr = new XMLHttpRequest();
    xhr.open('GET', 'assets/06/examples/xml.php', false);
    xhr.send(null);
    alert('I am outputted once the call has finished loading (5s delay)');
    alert(xhr.responseText);
  • Asynchronous example
    xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
    	if (xhr.readyState == 4) {
    		alert(xhr.responseText);
    	}
    };
    xhr.open('GET', 'assets/06/examples/xml.php', true);
    xhr.send(null);
    alert('I am outputted immediately (no delay)');
The asynchronous way is preferred

Advantages & Disadvantages

  • Advantages
    • No reloads
    • Less bandwidth used
    • Perceived speed by user
  • Disadvantages
    • URLs “broken”
      • No browser history
      • No back/forward
      • No proper bookmarking
    • Search Engines don't index content injected afterwards
    • Requires a change in programming attitude

XHR History

  • XHR's predecessor IXMLHTTPRequest invented by Microsoft
    • Shipped with IE5 (1999!), as part of the MSXML Library
    • Available in JS via ActiveX
  • By 2002 Mozilla had a fully working clone named XMLHttpRequest in Gecko
  • Other browsers also followed
    • Safari (2004)
    • Opera (2005)
    • IE7 (2006)
  • W3C started working on a standard in 2008

XHR in IE5/IE6

  • Define your own XMLHttpRequest Object and return the ActiveX Object
    if (typeof XMLHttpRequest == "undefined")
    	XMLHttpRequest = function() {
    		try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); }
    			catch (e) {}
    		try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }
    			catch (e) {}
    		try { return new ActiveXObject("Microsoft.XMLHTTP"); }
    			catch (e) {}
    		throw new Error("This browser does not support XMLHttpRequest");
    	};
    
    xhr = new XMLHttpRequest();
    
    // rest of your code ....

Same Origin Policy

  • For security reasons, it's only possible to make requests to the same url that serves the page making the request
    • It's not possible to call http://api.ikdoeict.be/ from http://www.ikdoeict.be/
  • There are ways to circumvent this though (see further)

Ajax Return Types

I've sent you each 300 buckeroos. In the form of a tricky Dick fun bill. Knock yourselves out!

Ajax Return Types

  • Everything returned by Ajax is textual data
  • Doesn't mean it's plaintext though
    • XML
    • HTML
    • JavaScript
    • JSON

XML

  • Initial usage mostly used XML
    • Server must send Content-Type: text/xml header, XHR will automagically parse the returned data as XML
  • Once data is parsed by XHR, walk through the XML nodes tree to get fetch the values you need
    • Most of the time a cumbersome work though
  • Once XML is parsed, build an HTML string with the data and inject the HTML blog into your document.

XML Example (1)

  • Data returned
    <?xml version="1.0" encoding="UTF-8"?>
    <sample>
    	<course>WMD</course>
    	<lecturers>
    		<lecturer>Bramus Van Damme</lecturer>
    		<lecturer>Davy De Winne</lecturer>
    	</lecturers>
    </sample>

XML Example (2)

  • JS Implementation
    xhr = new XMLHttpRequest();
    
    xhr.onreadystatechange = function() {
    	if (xhr.readyState == 4) {
    		var xml = xhr.responseXML,
    		    courseName = xml.getElementsByTagName('course')[0].firstChild.nodeValue,
    		    lecturersXml = xml.getElementsByTagName('lecturer'),
    		    lecturers = [];
    		console.log(xml);
    		for (var i = 0, len = lecturersXml.length; i < len; i++) {
    			lecturers.push(lecturersXml[i].firstChild.nodeValue);
    		}
    		alert('<p>' + courseName + ' is taught by ' + lecturers.join(' & ') + '</p>');
    	}
    };
    
    xhr.open('GET', 'assets/06/examples/xml-nodelay.php', true);
    xhr.send(null);

HTML

  • Why not let the server prepare a blob of HTML instead of building it after having parse the XML?
    • Data returned
      <p>WMD is taught by Davy De Winne &amp; Bramus Van Damme</p>
    • JS Implementation
      xhr = new XMLHttpRequest();
      
      xhr.onreadystatechange = function() {
      	if (xhr.readyState == 4) {
      		var html = xhr.responseText;
      		alert(html);
      	}
      };
      
      xhr.open('GET', 'assets/06/examples/html.php', true);
      xhr.send(null);

JavaScript

  • Load in a tad of JavaScript, and then eval() it.
    • Data returned
      alert('<p>WMD is taught by Davy De Winne &amp; Bramus Van Damme</p>');
    • JS Implementation
      xhr = new XMLHttpRequest();
      
      xhr.onreadystatechange = function() {
      	if (xhr.readyState == 4) {
      		var js = xhr.responseText;
      		eval(js);
      	}
      };
      
      xhr.open('GET', 'assets/06/examples/js.php', true);
      xhr.send(null);
  • Beware when using eval() though: eval() is evil.
    • But not always: In this case, it is allowed

JavaScript: External Files

  • No need for Ajax if you need to include an external file
    var head = document.getElementsByTagName("head")[0],
        script = document.createElement('script');
    
    script.type = 'text/javascript';
    script.src = "/path/to/your/file.js";
    head.appendChild(script);
  • Does give problems though, as you cannot know when the script has finished loading
    • In that case you will need XHR, or — even better — a specific script loader such as RequireJS

JSON

  • JSON = JavaScript Object Notation
    • In Short: JSON a JavaScript Object Literal, represented as a string
    • Described by Douglas Crockford in 2006 (RFC 4627)
  • Format Example
    {
    	"course": "WMD",
    	"lecturers": [
    		{
    			"id": 1,
    			"name": "Davy De Winne"
    		},
    		{
    			"id": 2,
    			"name": "Bramus Van Damme"
    		}
    	]
    }

JSON Format

  • JSON = One big Object with keys and values
    Figure by json.org
    • Multiple key-value pairs: comma-delimited
    • Keys are of the type string (double quotes required!)
    • Values are string, number, Object, Array, boolean or null

JSON Example

  • Example
    xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
    	if (xhr.readyState == 4) {
    		var json = JSON.parse(xhr.responseText), lecturers  =[];
    		for (var i = 0, len = json.lecturers.length; i < len; i++) {
    			lecturers.push(json.lecturers[i].name);
    		}
    		alert('<p>' + json.course + ' is taught by ' + lecturers.join(' & ') + '</p>');
    	}
    };
    xhr.open('GET', 'assets/06/examples/json.php', true);
    xhr.send(null);
    • XHR doesn't parse the string to JSON automagically, you will need to manually parse it using JSON.parse() (reverse: JSON.stringify())
      • Older browsers that don't support JSON.parse() require a library to do the parsing.

Sidenote: JSON in PHP

  • Since PHP 5.2, JSON in PHP is really easy
    • Encode an Array to JSON with json_encode
    • Decode JSON String into an Object or Associative Array with json_decode
  • Send a JSON header back with this header
    • Content-Type: application/json

What to choose?

  • JSON FTW!
    • Easier to work with (both JS & PHP)
    • Smaller than XML
    • Very readable
    • All modern webapp API's output JSON
  • Ajax: X is for JSON

Ajax in jQuery

Shut up and take my money!

$.ajax()

  • The core function jQuery to make Ajax calls is $.ajax()
    • Pass in an object to tell jQuery what to call and how
  • Example
    jQuery(document).ready(function($) {
    	$.ajax({
    		url: 'json.php',
    		type: 'get',
    		dataType: 'json',
    		success: function(data, textStatus, jqXHR) {
    			var lecturers = [];
    			for (var i = 0, len = data.lecturers.length; i < len; i++) {
    				lecturers.push(data.lecturers[i].name);
    			}
    			$('#output').html('<p>' + data.course + ' is taught by ' + lecturers.join(' & ') + '</p>');
    		},
    		error : function(jqXHR, textStatus, errorThrown) {
    			console.log(errorThrown);
    		}
    	});
    });

$.ajax() Parameters (1)

  • Five basic parameters
    • url: A string containing the URL to which the request is sent.
    • success: A function to be called if the request succeeds.
    • error: A function to be called if the request fails.
    • type: Type of request to make (POST or GET). Defaults to GET
    • dataType: Type of data you're expecting back from the server.
      • xml
      • html
      • script — Will be interpreted automatically. Only via GET.
      • json — Will be parsed automatically.
      • jsonp
      • text
      • (empty → jQuery will guess the type)

$.ajax() Parameters (1)

  • Other interesting parameters
    • async: Set to false if you need synchronous requests
    • data: Extra data to be sent to the server (eg. form data)
    • headers: Extra headers to be sent along with the request
    • username & password: HTTP Authentication
  • Full list of parameters to be found in the jQuery API

Sending data

  • Possible via data parameter
    • Format is a querystring: key=value&key=value&key=value
  • Manually building a querystring
    var values = [
    	'firstname=' + encodeURIComponent($('#firstname').val()),
    	'lastname=' + encodeURIComponent($('#lastname').val())
    ];
    
    var queryString = values.join('&');
  • Automatic version: $.fn.serialize()
    var queryString = $('#signupform').serialize();
  • Also possible: an Object
    {key : "value"}

$.ajax() alternative syntax

  • jQuery1.5 introduced deferreds which allow this:
    jQuery(document).ready(function($) {
    	$.ajax({
    		url: 'json.php',
    		type: 'get',
    		dataType: 'json'
    	}).success(function(data, textStatus, jqXHR) {
    		var lecturers = [];
    		for (var i = 0, len = data.lecturers.length; i < len; i++) {
    			lecturers.push(data.lecturers[i].name);
    		}
    		$('#output').html('<p>' + data.course + ' is taught by ' + lecturers.join(' & ') + '</p>');
    	}).error(function(jqXHR, textStatus, errorThrown) {
    		console.log(errorThrown);
    	});
    });
    • Instead of passsing succes and error functions in the call, you hook them onto the request

Sidenote: Deferreds

  • Deferreds are awesome!
    • When creating the ajax call, you get back a promise
    • The success and error functions are bound to the promise
    • When the ajax call is completed, the promise is resolved and the attached functions are executed
  • You can create your own deferreds in jQuery
  • jQuery has an easy $.when(deferred).then(function) shorthand

Basic Shorthands

  • jQuery offers shorthands to make GET and POST requests
  • Internally these calls are routed to $.ajax()
  • Code is more concise, yet allows less configuration

Basic Shorthands: $.get()

  • Example
    jQuery(document).ready(function($) {
    	$.get('json.php', function(data, textStatus, jqXHR) {
    		var lecturers = [];
    		for (var i = 0, len = data.lecturers.length; i < len; i++) {
    			lecturers.push(data.lecturers[i].name);
    		}
    		$('#output').html('<p>' + data.course + ' is taught by ' + lecturers.join(' & ') + '</p>');
    	}, 'json');
    });
  • Possible to pass in extra data
    jQuery(document).ready(function($) {
    	$.get('json.php', { key: "value" }, function(data, textStatus, jqXHR) {
    		// ...
    	}, 'json');
    });

Basic Shorthands: $.post()

  • Same syntax as $.get()
    jQuery(document).ready(function($) {
    	$.post('json.php', { key: "value" }, function(data, textStatus, jqXHR) {
    		// ...
    	}, 'json');
    });

Specific Shorthands

  • jQuery also sports other shorthands for commonly used actions
  • These functions only work using GET

Specific Shorthands: $.fn.load()

  • Example
    jQuery(document).ready(function($) {
    	$('#output').load('html.php');
    });
  • Also possible to send extra data and hook a success function (see jQuery API)

Specific Shorthands: $.getScript()

  • Example
    jQuery(document).ready(function($) {
    	$.getScript('js.php');
    });
  • Also possible to send extra data and hook a success function (see jQuery API)

Specific Shorthands: $.getJSON()

  • Example
    jQuery(document).ready(function($) {
    	$.getJSON('json.php', function(data, textStatus, jqXHR) {
    		var lecturers = [];
    		for (var i = 0, len = data.lecturers.length; i < len; i++) {
    			lecturers.push(data.lecturers[i].name);
    		}
    		$('#output').html('<p>' + data.course + ' is taught by ' + lecturers.join(' & ') + '</p>');
    	});
    });
  • Also possible to send extra data (see jQuery API)

Sidenote: Multiple Calls

  • In some cases, you'll need to do 2 requests before you have all needed data
    • Oldskool solution: chain calls
      • Invoke call #1
      • success function of call #1 will invoke call #2
      • success function of call #2 will populate the page
    • Better solution: use the force deferreds
      function doAjax() { return $.get('foo.htm'); }
      function doMoreAjax() { return $.get('bar.htm'); }
      
      $.when(doAjax(), doMoreAjax())
      .then(function(){
      	console.log('I am fired once BOTH ajax requests have completed!');
      }).fail(function(){
      	console.log('I am fired if one or more requests failed');
      });

What to use?

  • Shorthands allowed, yet if you want to have full control use $.ajax()
  • Use deferreds when requiring multiple resources

Ajax Challenges

What's this? Hermes Conrad is closing the gap. He's limbo-ed out of retirement and straight into my heart!

Ajax Challenges

  • When using Ajax, some usability challenges are faced
    • How does the visitor know the site isn't broken but data is loading?
    • The back and previous button (and thus bookmarking of specific content) is broken.
  • As a developer, you're also facing some technical challenges
    • Cross-Domain Communication
    • Working with authentication

Ajax Challenges

I. User feedback

User Feedback

  • Always give the user feedback that something is loading.
    • Textual feedback — viz. “loading”
    • Spinners (gifs)

  • Generate spinners at ajaxload.info
    • Also possible with CSS3 nowadays #lmgtfy

User Feedback: Example

  • Show spinner when request is being made, hide spinner when it's done.
    <img src="spinner.gif" style="display: none;" id="spinner">
    
    <div id="output"></div>
    
    <script src="js/jquery-1.7.1.min.js"></script>
    <script>
    	jQuery(document).ready(function($) {
    		$('#spinner').show();
    		$('#output').load('html-delay.php', function() {
    			$('#spinner').hide();
    		});
    	});
    </script>
  • Additionally, when loading data after pressing a button
    1. Disable the button to prevent the user from clicking it twice
    2. Show spinner

Ajax Challenges

II. Unbreaking History

History: Hashes

  • When building a single page interface (think GMail, or even this presentation), you must enable a way to directly link to a certain part/state of the page.
  • Oldskool solution: hashbang URLs
    • Change the hash of the window.location
    • Listen for the hashchange event to detect wheter a user has clicked the back/forward button and re-apply the state
    • When loading the page, check for a hash and re-apply the state
  • But really, don't use hashbang URLs

History: pushState

  • HTML5 introduced history.pushState() and history.replaceState() which allow you to manipulate the browser history object
    • history.pushState() = add new entry onto history object
    • history.replaceState() = replace the current entry in history object
  • Whenever user presses back/forward, browser will trigger a popstate event
Note: the fabricated URLs must also be available when doing a normal request

History: pushState Syntax

  • history.pushState()
    var stateObj = { foo: "bar" };
    history.pushState(stateObj, "page 2", "bar.html");
    • stateObj — An object you want to associate with the history entry. Will be returned when popstate is triggered
    • title — Title you wish to link to the history entry (ingored!)
    • url — URL to show in the address bar (optional)
  • popstate event
    window.addEventListener('popstate', function(stateObj) {
    	console.log(stateObj); // { foo: "bar" }
    });

History: pushState Demo (1)

  • Without history.pushState()
    <ul>
    	<li><a href="#" id="link_1">link 1</a></li>
    	<li><a href="#" id="link_2">link 2</a></li>
    	<li><a href="#" id="link_3">link 3</a></li>
    	<li><a href="#" id="link_4">link 4</a></li>
    </ul>
    
    <script src="js/jquery-1.7.1.min.js"></script>
    <script>
    	jQuery(document).ready(function($) {
    		$('ul').on('click', 'a', function(e) {
    			e.preventDefault();
    			$('ul a').css('background', 'inherit');
    			$(this).css('background', '#FF9');
    		});
    	});
    </script>
    Show demo →

History: pushState Demo (2)

  • With history.pushState()
    jQuery(document).ready(function($) {
    	$('ul').on('click', 'a', function(e) {
    		e.preventDefault();
    		$('ul a').css('background', 'inherit');
    		$(this).css('background', '#FF9');
    		history.pushState({'id':this.id}, 'clicked ' + this.id, '');
    	});
    	$(window).on('popstate', function(e) {
    		$('ul a').css('background', 'inherit');
    		if (e.originalEvent.state && e.originalEvent.state.id) {
    			$('#' + e.originalEvent.state.id).css('background', '#FF9');
    		}
    	});
    });
    Show demo →

Ajax Challenges

III. Cross-Domain

Cross-Domain

  • Cross-Domain XHR Requests are not allowed
    Figure by Yahoo!
    Figure by Yahoo!
  • Three possible solutions to bypasssing this problem
    • Proxy Script
    • JSONP
    • CORS (XHR2)

Cross-Domain: Proxy Script (1)

  • Put a little (PHP) Script on your own webserver which fetches the data from the remote server
    Figure by Yahoo!

Cross-Domain: Proxy Script (2)

  • Dataflow
    • Client calls http://site.com/proxy.php?route=ajax%2fuser%2flogin%3fparam_a%3d1%26param_b%3d2
    • proxy.php fetches data from http://othersite.com/ajax/user/login?param_a=1&param_b=2
    • proxy.php outputs fetched data
  • proxy.php Implementation

Cross-Domain: JSONP (1)

  • Short for JSON with Padding
  • Exploits the fact that we can make requests to JavaScript code on other servers
    • Return JSON wrapped inside a callback function instead of just JSON
      {"firstname" : "forrest", "lastname" : "gump"}
      callbackFunction({"firstname" : "forrest", "lastname" : "gump"})
    • The returned data will be eval()'d, thus the callback function — which will parse the JSONString to JSON — will be executed
  • The name of the callback function is passed as an URL parameter, by convention callback or jsonp
    http://othersite.com/ajax/user/login?param_a=1&callback=callbackFunction

Cross-Domain: JSONP (2)

  • In jQuery, working with JSONP is very easy
    • Specify the dataType to being jsonp
      • jQuery will automagically define callback function
      • jQuery will automagically add callback parameter to the request url
      $.ajax({
      	url: 'jsonp.php',
      	type: 'get',
      	dataType: 'jsonp',
      	success: function(data, textStatus, jqXHR) {
      		// ...
      	}
      });
  • JSONP downsides:
    • Only works with GET
    • Can be harmful because external code is eval()'d

Cross-Domain: CORS

  • Short for Cross-Origin Resource Sharing
  • New feature in XMLHttpRequest2
  • Server can specify which domains may call it via a header
    • In PHP:
      header('Access-Control-Allow-Origin: othersite.com');
    • Set to * to allow calls from any domain
  • Once CORS is enabled, you can use the remote resource as you'd use a local resource.
    • Not limited to GET
  • oldIE + IE9 Note

Sidenote: XHR2

  • XHR on steroids
    • Specify response format natively
    • Send binary data (file uploads!)
    • Cross Origin Resource Sharing
  • More info on html5rocks.com and MDN
  • Can we use it already? It looks like it :-)

Sidenote: App Authentication

  • When working with public API's, you'll sometimes need to authenticate your app with an API key
    • Some apps accept it as an URL parameter
      http://othersite.com/ajax/user/login?param_a=1&apikey=yourapikey
    • Some apps require it sent in via a custom header
      $.ajax({
      	url: 'json.php',
      	dataType: 'json',
      	header : { 'X-Api-Key' : 'yourapikey' },
      	success: function(data, textStatus, jqXHR) {
      		// ...
      	}
      });
  • API keys are used for logging, rate limiting and usage analysis (and eventual blocking)

Questions?

Sources

ikdoeict.be