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.

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

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

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(\Elgg\Hook $hook) {

        /* @var $response AjaxResponse */
        $response = $hook->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_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`` 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 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.
