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.

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 event 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 elgg_register_success_message() and elgg_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, 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 modules required server-side with elgg_import_esm(), 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
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)

  • 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 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) {
    $('.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 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:

  1. HTTP success (200) with status 0. No elgg_register_error_message() calls were made on the server.

  2. HTTP success (200) with status -1. elgg_register_error_message() was called.

  3. 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.