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
data
option (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
jqXHR
object, 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
jqXHR
promise is resolved and anysuccess
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
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
POST
for actions, otherwiseGET
. You can set it viaoptions.method
.If a non-empty
options.data
is given, the default method is alwaysPOST
.For client caching, set
options.method
to"GET"
andoptions.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 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,
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) {
$('.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 orfalse
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) {
$('.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 orfalse
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
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
done
callbacks 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.