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:: Contents :local: :depth: 2 Overview ======== All the ajax methods perform the following: #. Client-side, the ``data`` option (if given as an object) is filtered by the hook ``ajax_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 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: .. code-block:: php // 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('', options)``: .. code-block:: js 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:``. 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``. .. code-block:: php // 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('', options)``. .. code-block:: js 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:``. 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: .. code-block:: php // 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: .. code-block:: php // in myplugin_init() elgg_register_ajax_view('myplugin/get_link'); To fetch the view, use ``ajax.view('', options)``: .. code-block:: js 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:``. 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: .. code-block:: php // in myplugin_init() elgg_register_ajax_view('forms/myplugin/add'); To fetch this using ``ajax.form('', options)``. .. code-block:: js 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:``. 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: .. code-block:: php echo elgg_view_form('login', ['ajax' => true]); Redirects --------- Use ``ajax.forward()`` to start a spinner and redirect the user to a new destination. .. code-block:: js 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: .. code-block:: js // 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: .. code-block:: php 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: .. code-block:: js // 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. .. code-block:: js 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.