Ajax
The elgg/Ajax 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.
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 event
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
elgg_register_success_message()andelgg_register_error_message()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 modules required server-side with
elgg_import_esm(), 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
elgg_register_success_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) {
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)
- 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.
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) {
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) {
$('.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) {
$('.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
event 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 hooks = require('elgg/hooks');
var ajax = new Ajax();
hooks.register(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.
Piggybacking on an Ajax response
The server-side ajax_response event 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(\Elgg\Event $event) {
/* @var $response AjaxResponse */
$response = $event->getValue();
// 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_event_handler(AjaxResponse::RESPONSE_EVENT, 'view:foo', 'myplugin_append_ajax');
To capture the metadata send back to the client, we use the client-side ajax_response_data hook:
// in your boot module
var Ajax = require('elgg/Ajax');
var hooks = require('elgg/hooks');
hooks.register(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. Noelgg_register_error_message()calls were made on the server.HTTP success (200) with status
-1.elgg_register_error_message()was called.HTTP error (4xx/5xx). E.g. calling an action with stale tokens, or a server exception. In this case the
donecallbacks are not called.
The first and third case are the most common cases in the system. Use the done and fail callbacks to differentiate behaviour on success and error.
ajax.action('entity/delete?guid=123').done(function (value, statusText, jqXHR) {
// remove element from the page
}).fail(function() {
// handle error condition if needed
});
Requiring ES modules
Each response from an Ajax service will contain a list of ES modules required server side with elgg_import_esm(). 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 import for any modules they depend on. Additionally 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.