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
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 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
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
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
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 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.
Bemerkung
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,
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 orfalse
if it can’t be loaded.
Warnung
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 orfalse
if it can’t be loaded.
Bemerkung
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
.
Warnung
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');
.
Bemerkung
If data was given as a string (e.g. $form.serialize()
), the request hooks are not triggered.
Bemerkung
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;
});
Bemerkung
Only data.value
is returned to the success
function or available via the Deferred interface.
Bemerkung
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
done
andsuccess
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 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
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 toregister_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 onjQuery.ajax
and returns ajqXHR
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 thewrapper.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¶
Warnung
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.
Warnung
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 orfalse
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.
Warnung
- 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
.
Warnung
- 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.