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.
Contents
Overview¶
All the ajax methods perform the following:
- Client-side, the
dataoption (if given as an object) is filtered by the hookajax_request_data. - The request is made to the server, either rendering a view or a form, calling an action, or loading a path.
- The method returns a
jqXHRobject, which can be used as a Promise. - Server-echoed content is turned into a response object (
Elgg\Services\AjaxResponse) containing a string (or a JSON-parsed value). - The response object is filtered by the hook
ajax_response. - The response object is used to create the HTTP response.
- Client-side, the response data is filtered by the hook
ajax_response_data. - The
jqXHRpromise is resolved and anysuccesscallbacks are called.
More notes:
- All hooks have a type depending on the method and first argument. See below.
- By default the
elgg/spinnermodule is automatically used during requests. - User messages generated by
system_message()andregister_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
POSTfor actions, otherwiseGET. You can set it viaoptions.method. - If a non-empty
options.datais given, the default method is alwaysPOST. - For client caching, set
options.methodto"GET"andoptions.data.elgg_response_ttlto 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(), setoptions.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)
- client-side
- All hooks have type
- 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/elgg-plugin.php
return [
'routes' => [
'default:myplugin:time' => [
'path' => '/myplugin_time',
'resource' => 'myplugin/time',
],
],
];
// in myplugin/views/default/resources/myplugin/time.php
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,
outputwill 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”. outputwill be a string with the rendered view.- The request data are injected into
$varsin the view. - If the request data contains
guid, the system sets$vars['entity']to the corresponding entity orfalseif 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”. outputwill be a string with the rendered view.- The request data are injected into
$varsin your form view. - If the request data contains
guid, the system sets$vars['entity']to the corresponding entity orfalseif 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.
Submitting forms¶
To submit a form using Ajax, simply pass ajax parameter with form variables:
echo elgg_view_form('login', ['ajax' => true]);
Redirects¶
Use ajax.forward() to start a spinner and redirect the user to a new destination.
var Ajax = require('elgg/Ajax');
var ajax = new Ajax();
ajax.forward('/activity');
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.
Note
The form will be objectified as FormData, and the request content type will be determined accordingly. Effectively this allows plugins to submit multipart form data without using jquery.form plugin and other iframe hacks.
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:
- HTTP success (200) with status
0. Noregister_error()calls were made on the server. - HTTP success (200) with status
-1.register_error()was called. - HTTP error (4xx/5xx). E.g. calling an action with stale tokens, or a server exception. In this case the
doneandsuccesscallbacks 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
outputfrom the returned wrapper - the
successhandler will fire even if the action is prevented - the
successhandler will receive a wrapper object. You must look forwrapper.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
successfunction. If the action was not allowed to run for some reason,wrapper.outputwill be an empty string.- You may want to use the elgg/spinner module.
- Elgg does not use
wrapper.statusfor anything, but a call toregister_error()causes it to be set to-1.- If the action echoes a non-JSON string,
wrapper.outputwill contain that string.elgg.actionis based onjQuery.ajaxand returns ajqXHRobject (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 thewrapper.forward_urlvalue 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¶
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
$varsin the view.- If the request contains
$_GET['guid'], the system sets$vars['entity']to the corresponding entity orfalseif 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
$varscan 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
$varscan 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.