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:: 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 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 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 ``system_message()`` and ``register_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, 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 AMD modules required server-side with ``elgg_require_js()``, 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
    system_message('We did it!');

    echo json_encode([
        'sum' => $arg1 + $arg2,
        'product' => $arg1 * $arg2,
    ]);

To execute it, use ``ajax.action('<action_name>', 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) {
        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)
* 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.

.. 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('<url_path>', options)``.

.. code-block:: js

    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:

.. 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('<view_name>', 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) {
        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 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('<action_name>', options)``.

.. code-block:: js

    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 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
	hook ``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 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');``.

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

.. code-block:: php

    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:

.. code-block:: js

    // 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;
    });

.. 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 ``register_error()`` calls were made on the server.
2. HTTP success (200) with status ``-1``. ``register_error()`` was called.
3. HTTP error (4xx/5xx). E.g. calling an action with stale tokens, or a server exception. In this case the ``done`` and ``success`` callbacks are not called.

You may need only worry about the 2nd case. We can do this by looking at ``jqXHR.AjaxData.status``:

.. code-block:: js

    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 for ``wrapper.output``
* no ajax hooks

.. code-block:: js

    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 :doc:`elgg/spinner</guides/javascript>` module.
 * Elgg does not use ``wrapper.status`` for anything, but a call to ``register_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 on ``jQuery.ajax`` and returns a ``jqXHR`` 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 the ``wrapper.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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: json

    {
        current_url: {String} "http://example.org/action/example/math", // not very useful
        forward_url: {String} "http://example.org/foo", ...if forward('foo') was called
        output: {String|Object} from echo in action
        status: {Number} 0 = success. -1 = an error was registered.
        system_messages: {Object}
    }

.. warning::

    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.

.. warning::

    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:

.. code-block:: php

    // in myplugin_init()
    elgg_register_ajax_view('myplugin/get_link');

.. 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,
    ]);

.. code-block:: js

    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 or
   ``false`` 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.

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

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:

.. code-block:: js

    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>``):

.. code-block:: php

    // in myplugin_init()
    elgg_register_ajax_view('forms/myplugin/add');

.. code-block:: js

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

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


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.

.. code-block:: js

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