Actions

J1.5

Difference between revisions of "Ajax using MooTools"

From Joomla! Documentation

(New page: The MooTools [http://mootools.net/] JavaScript framework includes support for handling Ajax calls and makes coding for Ajax a lot easier, although there are still some wrinkles. Perhaps t...)
 
m (Added version warning.)
Line 1: Line 1:
 +
{{notice|This article applies to Joomla! 1.5 only as Joomla! 1.6 ships with a later version of MooTools which has a different way
 +
of handling Ajax requests.  Also, in Joomla! 1.6, it is no longer necessary to wrap JavaScript in CDATA tags before calling the JDocument::addScriptDeclaration method.}}
 +
 
The MooTools [http://mootools.net/] JavaScript framework includes support for handling Ajax calls and makes coding for Ajax a lot easier, although there are still some wrinkles.  Perhaps the most important benefit of using the MooTools framework for Ajax is that it is cross-browser and so concerns about ensuring that your code will work on all browsers can be mostly left to one side.
 
The MooTools [http://mootools.net/] JavaScript framework includes support for handling Ajax calls and makes coding for Ajax a lot easier, although there are still some wrinkles.  Perhaps the most important benefit of using the MooTools framework for Ajax is that it is cross-browser and so concerns about ensuring that your code will work on all browsers can be mostly left to one side.
  

Revision as of 19:33, 28 February 2011

Replacement filing cabinet.png
This Namespace has been archived - Please Do Not Edit or Create Pages in this namespace. Pages contain information for a Joomla! version which is no longer supported. It exists only as a historical reference, will not be improved and its content may be incomplete.
Info non-talk.png
General Information

This article applies to Joomla! 1.5 only as Joomla! 1.6 ships with a later version of MooTools which has a different way of handling Ajax requests. Also, in Joomla! 1.6, it is no longer necessary to wrap JavaScript in CDATA tags before calling the JDocument::addScriptDeclaration method.

The MooTools [1] JavaScript framework includes support for handling Ajax calls and makes coding for Ajax a lot easier, although there are still some wrinkles. Perhaps the most important benefit of using the MooTools framework for Ajax is that it is cross-browser and so concerns about ensuring that your code will work on all browsers can be mostly left to one side.

In the following text you will learn how to develop a simple Ajax request-response pair using MooTools in Joomla! 1.5. This simple example is then extended to cope with out of sequence server responses. Finally, if you are using Ajax to access a web service in another domain then you will come up against the "same origin policy" in web browsers and a secure method of working around this issue is discussed.

Contents

Ajax client code using MooTools

In a typical Ajax application you will want to pull some data from a server, which could be your own Joomla site or a remote web service, and update some element on the web page with the data returned by the server. There are three elements to a typical Ajax implementation:

  • An HTML element whose change of state will trigger the Ajax request.
  • Another HTML element where the response data will be placed. Often this will show an "Ajax loading" icon or message while the response from the server is being awaited.
  • The Ajax JavaScript code itself.

Starting with the first of these, you need to identify the element on the page that will trigger the Ajax request. The element needs to be identified by a unique id attribute. For example, suppose you have a drop-down select box on your page and you want to do an Ajax request whenever the user changes the item selected. Then you must ensure that the select element has a unique id attribute like this:

<select name="drop-down" id="drop-down">
        <option value="1">Item 1</option>
        <option value="2">Item 2</option>
        <option value="3">Item 3</option>
</select>

You can generate this select list programmatically using the JHTML class like this:

<?php
$options = array();
$options[] = JHTML::_( 'select.option', '1', 'Item 1' );
$options[] = JHTML::_( 'select.option', '2', 'Item 2' );
$options[] = JHTML::_( 'select.option', '3', 'Item 3' );
echo JHTML::_( 'select.genericlist', $options, 'drop-down' );

Secondly, you need to add an HTML element that will hold the output from the Ajax call. This could be a suitably-placed DIV, which must also have a unique id attribute, like this:

<div id="ajax-container"></div>

You can, of course, use the id in a selector to style the output using CSS.

Thirdly, you need to add the JavaScript code that will make the Ajax request and place the response into the screen output. You generally don't need to be concerned about loading MooTools itself as this is done automatically for you by Joomla, but sometimes you need to do this manually by adding the following code:

<?php
JHTML::_( 'behavior.mootools' );

There are many ways to add JavaScript code to the output from Joomla. One way, which avoids complex quoting, is to use the PHP "heredoc" syntax (see [2] for more details) like this:

<?php
$ajax = <<<EOD
Your JavaScript code goes here.
EOD;
 
$doc = & JFactory::getDocument();
$doc->addScriptDeclaration( $ajax );

You can embed PHP variables in the heredoc text by surrounding them with braces, like this:

<?php
$ajax = <<<EOD
This is some JavaScript code with {$this->embedded} PHP variable in it.
EOD;
 
$doc = & JFactory::getDocument();
$doc->addScriptDeclaration( $ajax );

The JavaScript code must add an event handler to the element that will trigger the Ajax call. This is done in MooTools using the following call:

window.addEvent( 'domready', function() {
 
        $('drop-down').addEvent( 'change', <function-declaration> );
 
});

where <function-declaration> is the JavaScript code that is to be called when the state of the element identified as drop-down is changed. Notice that you should always delay the call to addEvent until the DOM is ready following a page load. This is done by telling window.addEvent to hang the Ajax addEvent function onto the onDomReady event.

You don't have to hang the Ajax call on the onChange event; for example, you could use onClick as the trigger.

The <function-declaration> that you will add will be an instantiation of the MooTools Ajax class, looking something like this:

var a = new Ajax( {$url}, {
        method: 'get',
        update: $('ajax-container')
}).request();

where {$url} is a PHP variable containing the URL for the Ajax request. In this example the update argument has been used to copy the entire response from the server into the ajax-container element. This is quick and convenient, but very often you will want to process the response in some way before showing it to the user. Commonly, the response is JSON-encoded and you must decode the response and format it appropriately before updating ajax-container. To do this, use the onComplete argument to the Ajax object rather than the update argument.

var a = new Ajax( {$url}, {
        method: 'get',
        onComplete: <completion-function>
}).request();

where <completion-function> is a JavaScript function that will be called when a response from the remote server is received. Typically, this function will process the raw data from the server before pushing it into the ajax-container element.

The following is a more complete example of an Ajax function which receives data from the server in JSON format, decodes it, then pushes data from the response into ajax-container.

window.addEvent('domready',function() {
 
     $('drop-down').addEvent('change',function(){
 
          $('ajax-container').empty().addClass('ajax-loading');
 
          var a = new Ajax({$url},{
               method:'get',
               onComplete:function(response){
                    var resp=Json.evaluate(response);
 
                    // Other code to execute when the request completes.
                    $('ajax-container').removeClass('ajax-loading').setHTML(output);
               }
          }).request();
     });
});

Notice that in this example there is also some code to add, and subsequently remove, an ajax-loading CSS class from the ajax-container element. Typically, the presence of this class will cause a "spinner" graphic element to be loaded as a background image to make the user aware that the system is still alive.

Handling the server side of Ajax requests

The server side of your Ajax implementation may be a public web service, in which case it is already written for you. But if you need to write the server code too then it makes sense to base the code on the Joomla Framework. Although it might be tempting to implement the server code outside Joomla, you will miss out on a number of important security features that make writing secure Ajax server code very simple.

Typically you will want the server to send its response in XML or JSON format. The Joomla 1.5 support for the MVC design pattern makes this particularly easy to arrange. Simply add a new view class into the views directory in a file called view.xml.php or view.json.php depending on the format required. Sometimes the response required to a particular request is so simple that setting up a new view would be overkill. In that case there is no problem in generating the output directly in the controller; no view required.

Generating JSON output

PHP has native functions to encode and decode JSON data. You can encode data using the json_encode function, like this:

<?php
// Set up the data to be sent in the response.
$data = array( 'some data' );
 
// Output the JSON data.
echo json_encode( $data );

The json_encode function can encode almost all data types, such as strings, arrays and objects, although you may need to be aware that the corresponding json_decode function will only return an object (or optionally, an associative array).

It is good practice to set the MIME-type for the output correctly. In some applications you might also want to change the suggested filename to something other than the "index.php" that you will probably get by default. In the following example the suggested filename is changed to the name of the view, with a ".json" extension added.

<?php
// Set up the data to be sent in the response.
$data = array('some data');
 
// Get the document object.
$document =& JFactory::getDocument();
 
// Set the MIME type for JSON output.
$document->setMimeEncoding('application/json');
 
// Change the suggested filename.
JResponse::setHeader('Content-Disposition','attachment;filename="'.$view->getName().'.json"');
 
// Output the JSON data.
echo json_encode($data);

Generating XML output

Joomla supports a simple and quite efficient class, JSimpleXML, which can be used to generate XML output in, for example, an Ajax implementation. However, using an object-based XML generator suffers from the drawback that it tends to be slow and memory-intensive, even though JSimpleXML is a lightweight implementation. Unless your requirements are particularly complex, directly outputting XML in string form will result in faster response times and a lower server footprint.

For example, the following code will output an XML document consisting of a root element, called <root>, containing an <items> element which itself contains one or more <item> elements with the actual data. You will need to adjust the code to cope with your particular data requirements.

<?php
$document =& JFactory::getDocument();
$document->setMimeEncoding( 'text/xml' );
 
// Output XML header.
echo '<?xml version="1.0" encoding="UTF-8" ?>' . "\n";
 
// Output root element.
echo '<root>'."\n";
 
// Output the data.
echo "\t".'<items>'."\n";
if(!empty($data)){
     foreach($data as $datum){
          echo "\t\t".'<item>'."\n";
          foreach ($datum as $key => $value) {
               echo "\t\t\t".'<'.$key.'>'.htmlspecialchars($value).'</'.$key.'>'."\n";
          }
          echo "\t\t".'</item>'."\n";
     }
}
echo "\t".'</items>'."\n";
 
// Terminate root element.
echo '</root>'."\n";

Note that the data should be passed through htmlspecialchars to ensure that HTML characters are properly escaped.

Note also that in this example some attempt has been made to "pretty print" the XML output with tabs and carriage returns. This is not strictly necessary and these extra characters can be removed if you prefer.


Handling out of sequence responses in MooTools Ajax

It is easy to forget that the initial "A" in "Ajax" stands for "asynchronous", which means that once the user has initiated an Ajax request, she is free to do other things in the user interface, including making other Ajax requests. As a web application designer you need to take into consideration that responses to these requests may not arrive back in the same order they were made. If not handled correctly this can have unfortunate consequences for the user. For example, in a typical Ajax implementation where the user selects a country from a drop-down list of countries and is then presented with a drop-down list of regions within that country, an Ajax call is made whenever the country is changed. The response to this request will be a list of regions within the selected country. But suppose the user makes a series of fairly rapid selection changes, something that is easier to do using the keyboard that with the mouse. Say the user hits the "U" key and the first country that comes up is "Uganda". This isn't what the user wanted, so she hits the "U" key repeatedly until she gets "United Kingdom", then once more to get the one she really wanted: "United States". This causes a whole series of Ajax calls to be made in sequence, with the last being the call for "United States". But suppose the response to her "United Kingdom" request is delayed for some reason. This might be due to server load, or it could be down to network delay. But for whatever reason, it arrives after the "United States" response. Then the user will be presented with a list of "United Kingdom" regions even though her selection was "United States".

This sort of behaviour is clearly undesirable. Various remedies for this have been suggested, including some that involve the server executing requests in strict order (which ignores the possibility that a network delay on the return leg could still cause the responses to arrive back at the client out of sequence).

However, the solution suggested below has the virtue of being quite simple, it does not require any special coding on the server and it copes with all classes of delayed response, regardless of their cause. The idea is to maintain a simple queue containing all the requests that have been made and to only output the results in the correct sequence, regardless of the order in which they arrive.

There needs to be some way to determine if a particular request has been completed or not. To do this you can extend the MooTools Ajax class like this:

// Extend the Mootools Ajax class.
Ajax = Ajax.extend({
 
    initialize: function( url, options ) {
        this.parent( url, options );
        this.ready = false;
        this.output = '';
    },
 
    onComplete: function() {
        this.ready = true;
        this.parent();
    }
 
});

This adds two additional properties to the Ajax class:

  • ready is a Boolean that flags whether the response is available or not; and
  • output which is used to hold the response.

Having extended the Ajax class, you now need to set up a simple queue mechanism to hold the request information. Each new request will be pushed into the queue and completed requests will be popped from the queue. The trick is to make sure that if a request completes before older requests have been completed it remains in the queue and does not get removed out of sequence.

The queue takes the form of a simple array, declared in global scope:

var AJAX_QUEUE = [];

The window.addEvent code is modified so that the Ajax object is pushed onto the queue before the request is made:

window.addEvent( 'domready', function() {
 
        $('drop-down').addEvent( 'change', function() {
 
                $( 'ajax-container' ).empty().addClass( 'ajax-loading' );
                var url = 'index.php?option=com_component&args=whatever';
                var a = new Ajax( url, {
                        method: 'get',
                        onComplete: function( response ){
 
// Ajax onComplete function code goes here.
 
                        }
                });
                AJAX_QUEUE.push( a );
                a.request();
        });
});

Then you need to add some extra code to the onComplete function that will pop completed requests from the queue, but only in the correct order:

// Save the response in the Ajax object.
this.output = response;
 
// Process the request queue.
while ( AJAX_QUEUE.length ) {
 
        // If the oldest request in the queue is not ready, do nothing.
        if ( !AJAX_QUEUE[0].ready ) break;
 
        // Pop the request from the queue.
        r = AJAX_QUEUE.shift();
 
        // Only output to document when queue is empty.
        if ( !AJAX_QUEUE.length ) {
                $('ajax-container').removeClass('ajax-loading').setHTML(r.output);
        }
 
}

Note that the output is only placed in the document when the queue is finally empty, so the user never sees results from intermediate requests.

If you have multiple Ajax fields on a single page, then you will need to give each one its own queue, but the coding remains essentially the same.

Avoiding web browser same origin policy in Ajax applications

If you need to access a web service, using Ajax, that is running on a server that is not in your domain, then you will run up against the "same origin policy" [3] that will prevent you from retrieving data from outside your own domain for security reasons. There needs to be some way to circumvent this restriction on those occasions where the web service is trusted.

The most common method of circumventing the same origin policy in Ajax implementations is to access the remote web service via a proxy running in your domain. The proxy uses an HTTP client library (such as [4]) to access the remote server. This technique is usually simple enough, but can get tougher if the remote server requires authentication or uses cookies to track state between requests. On the plus side, using a proxy generally makes it easier to cache Ajax responses; something that is normally quite difficult.

To add a simple proxy to your Joomla component you simply need to add an extra task to the controller. The following code shows roughly what it might look like, given a static HTTP client class called httpClient with a method that will request a page from a remote server.

<?php
/**
 * Support cross-domain Ajax request by using the component as a proxy.
 * To use this facility replace "option" with "type" and "task" with "request"
 * in the query that would otherwise be sent to the remote server.
 */
function proxy()
{
        $uri = & JFactory::getURI();
        $query = $uri->getQuery( true );
        $query['option'] = $query['type'];
        unset( $query['type']);
        unset( $query['task']);
        if (isset( $query['request'] )) {
                $query['task'] = $query['request'];
                unset( $query['request']);
        }
 
        // Make the API call.
        $response = httpClient::call( $query );
        if ($response->status != '200') {
                JError::raiseError( 500, JText::_( 'Remote server error' ) );
                return false;
        }
 
        // And return the response.
        echo $response->data;
        jexit();
}

This example also shows how to work around the situation where the remote server is also running Joomla, in which case it will require a URL containing option and task arguments that may not match those required by the proxy. For example, suppose you want to make an Ajax request to a remote server using this URL:

http://www.remoteserver.com/index.php?option=com_remotecomponent&task=remotetask&arg=something

To avoid the same origin policy you send the request to the proxy instead using this URL:

http://www.localserver.com/index.php
     ?option=com_localcomponent
     &task=proxy&type=com_remotecomponent
     &request=remotetask
     &arg=something

You will need to adapt this code to meet your particular requirements, such as if you need to specify the controller in order to reach the proxy.

Adding cookie and cache support is left as an exercise for the reader.