Ajax

The elgg/Ajax AMD module (introduced in Elgg 2.1) provides a set of methods for communicating with the server in a concise and uniform way, which allows plugins to collaborate on the request data, the server response, and the returned client-side data.

Client and server code written for the legacy API should not need modification.

Overview

All the ajax methods perform the following:

  1. Client-side, the data option (if given as an object) is filtered by the hook ajax_request_data.
  2. The request is made to the server, either rendering a view or a form, calling an action, or loading a path.
  3. The method returns a jqXHR object, which can be used as a Promise.
  4. Server-echoed content is turned into a response object (Elgg\Services\AjaxResponse) containing a string (or a JSON-parsed value).
  5. The response object is filtered by the hook ajax_response.
  6. The response object is used to create the HTTP response.
  7. Client-side, the response data is filtered by the hook ajax_response_data.
  8. The jqXHR promise is resolved and any success callbacks are called.

More notes:

  • All hooks have a type depending on the method and first argument. See below.
  • By default the elgg/spinner module is automatically used during requests.
  • User messages generated by system_message() and register_error() are collected and displayed on the client.
  • Elgg gives you a default error handler that shows a generic message if output fails.
  • PHP exceptions or denied resource return HTTP error codes, resulting in use of the client-side error handler.
  • The default HTTP method is POST for actions, otherwise GET. You can set it via options.method.
  • If a non-empty options.data is given, the default method is always POST.
  • For client caching, set options.method to "GET" and options.data.elgg_response_ttl to the max-age you want in seconds.
  • To save system messages for the next page load, set options.data.elgg_fetch_messages = 0. You may want to do this if you intent to redirect the user based on the response.
  • To stop client-side API from requiring AMD modules required server-side with elgg_require_js(), set options.data.elgg_fetch_deps = 0.
  • All methods accept a query string in the first argument. This is passed on to the fetch URL, but does not appear in the hook types.

Performing actions

Consider this action:

// in myplugin/actions/do_math.php

elgg_ajax_gatekeeper();

$arg1 = (int)get_input('arg1');
$arg2 = (int)get_input('arg2');

// will be rendered client-side
system_message('We did it!');

echo json_encode([
        'sum' => $arg1 + $arg2,
        'product' => $arg1 * $arg2,
]);

To execute it, use ajax.action('<action_name>', options):

var Ajax = require('elgg/Ajax');
var ajax = new Ajax();

ajax.action('do_math', {
        data: {
                arg1: 1,
                arg2: 2
        },
}).done(function (output, statusText, jqXHR) {
    if (jqXHR.AjaxData.status == -1) {
        return;
    }
        alert(output.sum);
        alert(output.product);
});

Notes for actions:

  • All hooks have type action:<action_name>. So in this case, three hooks will be triggered:
    • client-side "ajax_request_data", "action:do_math" to filter the request data (before it’s sent)
    • server-side "ajax_response", "action:do_math" to filter the response (after the action runs)
    • client-side "ajax_response_data", "action:do_math" to filter the response data (before the calling code receives it)
  • CSRF tokens are added to the request data.
  • The default method is POST.
  • An absolute action URL can be given in place of the action name.
  • Using forward() in an action simply sends the response. The URL given in not returned to the client.

Note

When setting data, use ajax.objectify($form) instead of $form.serialize(). Doing so allows the ajax_request_data plugin hook to fire and other plugins to alter/piggyback on the request.

Fetching data

Consider this PHP script that runs at http://example.org/myplugin_time.

// in myplugin/start.php
elgg_register_page_handler('myplugin_time', 'myplugin_get_time');

function myplugin_get_time() {
        elgg_ajax_gatekeeper();

        echo json_encode([
                'rfc2822' => date(DATE_RFC2822),
                'day' => date('l'),
        ]);

        return true;
}

To fetch its output, use ajax.path('<url_path>', options).

var Ajax = require('elgg/Ajax');
    var ajax = new Ajax();

    ajax.path('myplugin_time').done(function (output, statusText, jqXHR) {
        if (jqXHR.AjaxData.status == -1) {
            return;
        }
            alert(output.rfc2822);
            alert(output.day);
    });

Notes for paths:

  • The 3 hooks (see Actions above) will have type path:<url_path>. In this case, “path:myplugin_time”.
  • If the page handler echoes a regular web page, output will be a string containing the HTML.
  • An absolute URL can be given in place of the path name.

Fetching views

Consider this view:

// in myplugin/views/default/myplugin/get_link.php

if (empty($vars['entity']) || !$vars['entity'] instanceof ElggObject) {
        return;
}

$object = $vars['entity'];
/* @var ElggObject $object */

echo elgg_view('output/url', [
        'text' => $object->getDisplayName(),
        'href' => $object->getUrl(),
        'is_trusted' => true,
]);

Since it’s a PHP file, we must register it for Ajax first:

// in myplugin_init()
elgg_register_ajax_view('myplugin/get_link');

To fetch the view, use ajax.view('<view_name>', options):

var Ajax = require('elgg/Ajax');
var ajax = new Ajax();

ajax.view('myplugin/get_link', {
        data: {
                guid: 123 // querystring
        },
}).done(function (output, statusText, jqXHR) {
    if (jqXHR.AjaxData.status == -1) {
        return;
    }
        $('.myplugin-link').html(output);
});

Notes for views:

  • The 3 hooks (see Actions above) will have type view:<view_name>. In this case, “view:myplugin/get_link”.
  • output will be a string with the rendered view.
  • The request data are injected into $vars in the view.
  • If the request data contains guid, the system sets $vars['entity'] to the corresponding entity or false if it can’t be loaded.

Warning

In ajax views and forms, note that $vars can be populated by client input. The data is filtered like get_input(), but may not be the type you’re expecting or may have unexpected keys.

Fetching forms

Consider we have a form view. We register it for Ajax:

// in myplugin_init()
elgg_register_ajax_view('forms/myplugin/add');

To fetch this using ajax.form('<action_name>', options).

var Ajax = require('elgg/Ajax');
var ajax = new Ajax();

ajax.form('myplugin/add').done(function (output, statusText, jqXHR) {
    if (jqXHR.AjaxData.status == -1) {
        return;
    }
        $('.myplugin-form-container').html(output);
});

Notes for forms:

  • The 3 hooks (see Actions above) will have type form:<action_name>. In this case, “form:myplugin/add”.
  • output will be a string with the rendered view.
  • The request data are injected into $vars in your form view.
  • If the request data contains guid, the system sets $vars['entity'] to the corresponding entity or false if it can’t be loaded.

Note

Only the request data are passed to the requested form view (i.e. as a third parameter accepted by elgg_view_form()). If you need to pass attributes or parameters of the form element rendered by the input/form view (i.e. normally passed as a second parameter to elgg_view_form()), use the server-side hook view_vars, input/form.

Warning

In ajax views and forms, note that $vars can be populated by client input. The data is filtered like get_input(), but may not be the type you’re expecting or may have unexpected keys.

Piggybacking on an Ajax request

The client-side ajax_request_data hook can be used to append or filter data being sent by an elgg/Ajax request.

Let’s say when the view foo is fetched, we want to also send the server some data:

// in your boot module
var Ajax = require('elgg/Ajax');
var elgg = require('elgg');

    var ajax = new Ajax();

elgg.register_hook_handler(Ajax.REQUEST_DATA_HOOK, 'view:foo', function (name, type, params, data) {
    // send some data back
    data.bar = 1;
    return data;
});

This data can be read server-side via get_input('bar');.

Note

If data was given as a string (e.g. $form.serialize()), the request hooks are not triggered.

Piggybacking on an Ajax response

The server-side ajax_response hook can be used to append or filter response data (or metadata).

Let’s say when the view foo is fetched, we want to also send the client some additional data:

use Elgg\Services\AjaxResponse;

function myplugin_append_ajax($hook, $type, AjaxResponse $response, $params) {

    // alter the value being returned
    $response->getData()->value .= " hello";

    // send some metadata back. Only client-side "ajax_response" hooks can see this!
    $response->getData()->myplugin_alert = 'Listen to me!';

    return $response;
}

    // in myplugin_init()
    elgg_register_plugin_hook_handler(AjaxResponse::RESPONSE_HOOK, 'view:foo', 'myplugin_append_ajax');

To capture the metadata send back to the client, we use the client-side ajax_response hook:

// in your boot module
var Ajax = require('elgg/Ajax');
var elgg = require('elgg');

elgg.register_hook_handler(Ajax.RESPONSE_DATA_HOOK, 'view:foo', function (name, type, params, data) {

    // the return value is data.value

    // the rest is metadata

    alert(data.myplugin_alert);

    return data;
});

Note

Only data.value is returned to the success function or available via the Deferred interface.

Note

Elgg uses these same hooks to deliver system messages over elgg/Ajax responses.

Handling errors

Responses basically fall into three categories:

  1. HTTP success (200) with status 0. No register_error() calls were made on the server.
  2. HTTP success (200) with status -1. register_error() was called.
  3. HTTP error (4xx/5xx). E.g. calling an action with stale tokens, or a server exception. In this case the done and success callbacks are not called.

You may need only worry about the 2nd case. We can do this by looking at jqXHR.AjaxData.status:

ajax.action('entity/delete?guid=123').done(function (value, statusText, jqXHR) {
if (jqXHR.AjaxData.status == -1) {
    // a server error was already displayed
    return;
}

        // remove element from the page
});

Requiring AMD modules

Each response from an Ajax service will contain a list of AMD modules required server side with elgg_require_js(). When response data is unwrapped, these modules will be loaded asynchronously - plugins should not expect these modules to be loaded in their $.done() and $.then() handlers and must use require() for any modules they depend on. Additionally AMD modules should not expect the DOM to have been altered by an Ajax request when they are loaded - DOM events should be delegated and manipulations on DOM elements should be delayed until all Ajax requests have been resolved.

Legacy elgg.ajax APIs

Elgg 1.8 introduced elgg.action, elgg.get, elgg.getJSON, and other methods which behave less consistently both client-side and server-side.

Legacy elgg.action

Differences:

  • you must manually pull the output from the returned wrapper
  • the success handler will fire even if the action is prevented
  • the success handler will receive a wrapper object. You must look for wrapper.output
  • no ajax hooks
elgg.action('do_math', {
  data: {
    arg1: 1,
    arg2: 2
  },
  success: function (wrapper) {
    if (wrapper.output) {
      alert(wrapper.output.sum);
      alert(wrapper.output.product);
    } else {
      // the system prevented the action from running, but we really don't
      // know why
      elgg.ajax.handleAjaxError();
    }
  }
});

elgg.action notes

  • It’s best to echo a non-empty string, as this is easy to validate in the success function. If the action was not allowed to run for some reason, wrapper.output will be an empty string.
  • You may want to use the elgg/spinner module.
  • Elgg does not use wrapper.status for anything, but a call to register_error() causes it to be set to -1.
  • If the action echoes a non-JSON string, wrapper.output will contain that string.
  • elgg.action is based on jQuery.ajax and returns a jqXHR object (like a Promise), if you should want to use it.
  • After the PHP action completes, other plugins can alter the wrapper via the plugin hook 'output', 'ajax', which filters the wrapper as an array (not a JSON string).
  • A forward() call forces the action to be processed and output immediately, with the wrapper.forward_url value set to the normalized location given.
  • To make sure Ajax actions can only be executed via XHR, use elgg_ajax_gatekeeper().

elgg.action JSON response wrapper

{
  current_url: {String} "http://example.org/action/example/math", // not very useful
  forward_url: {String} "http://example.org/foo", ...if forward('foo') was called
  output: {String|Object} from echo in action
  status: {Number} 0 = success. -1 = an error was registered.
  system_messages: {Object}
}

Warning

It’s probably best to rely only on the output key, and validate it in case the PHP action could not run for some reason, e.g. the user was logged out or a CSRF attack did not provide tokens.

Warning

If forward() is used in response to a legacy ajax request (e.g. elgg.ajax), Elgg will always respond with this wrapper, even if not in an action.

Legacy view fetching

A plugin can use a view script to handle XHR GET requests. Here’s a simple example of a view that returns a link to an object given by its GUID:

// in myplugin_init()
elgg_register_ajax_view('myplugin/get_link');
// in myplugin/views/default/myplugin/get_link.php

if (empty($vars['entity']) || !$vars['entity'] instanceof ElggObject) {
    return;
}

$object = $vars['entity'];
/* @var ElggObject $object */

echo elgg_view('output/url', [
    'text' => $object->getDisplayName(),
    'href' => $object->getUrl(),
    'is_trusted' => true,
]);
elgg.get('ajax/view/myplugin/get_link', {
  data: {
    guid: 123 // querystring
  },
  success: function (output) {
    $('.myplugin-link').html(output);
  }
});

The Ajax view system works significantly differently than the action system.

  • There are no access controls based on session status.
  • Non-XHR requests are automatically rejected.
  • GET vars are injected into $vars in the view.
  • If the request contains $_GET['guid'], the system sets $vars['entity'] to the corresponding entity or false if it can’t be loaded.
  • There’s no “wrapper” object placed around the view output.
  • System messages/errors shouldn’t be used, as they don’t display until the user loads another page.
  • Depending on the view’s suffix (.js, .html, .css, etc.), a corresponding Content-Type header is added.

Warning

In ajax views and forms, note that $vars can be populated by client input. The data is filtered like
get_input(), but may not be the type you’re expecting or may have unexpected keys.

Returning JSON from a view

If the view outputs encoded JSON, you must use elgg.getJSON to fetch it (or use some other method to set jQuery’s ajax option dataType to json). Your success function will be passed the decoded Object.

Here’s an example of fetching a view that returns a JSON-encoded array of times:

elgg.getJSON('ajax/view/myplugin/get_times', {
  success: function (data) {
    alert('The time is ' + data.friendly_time);
  }
});

Legacy form fetching

If you register a form view (name starting with forms/), you can fetch it pre-rendered with elgg_view_form(). Simply use ajax/form/<action> (instead of ajax/view/<view_name>):

// in myplugin_init()
elgg_register_ajax_view('forms/myplugin/add');
elgg.get('ajax/form/myplugin/add', {
  success: function (output) {
    $('.myplugin-form-container').html(output);
  }
});

Only the request data are passed to the requested form view (i.e. as a third parameter accepted by elgg_view_form()). If you need to pass attributes or parameters of the form element rendered by the input/form view (i.e. normally passed as a second parameter to elgg_view_form()), use the server-side hook view_vars, input/form.

Warning

In ajax views and forms, note that $vars can be populated by client input. The data is filtered like
get_input(), but may not be the type you’re expecting or may have unexpected keys.

Legacy helper functions

These functions extend jQuery’s native Ajax features.

elgg.get() is a wrapper for jQuery’s $.ajax(), but forces GET and does URL normalization.

// normalizes the url to the current <site_url>/activity
elgg.get('/activity', {
   success: function(resultText, success, xhr) {
      console.log(resultText);
   }
});

elgg.post() is a wrapper for jQuery’s $.ajax(), but forces POST and does URL normalization.