Skip to content

Commit

Permalink
feature(ajax): Adds a new elgg/ajax AMD module with unified API
Browse files Browse the repository at this point in the history
All elgg/ajax methods transparently handle server-side messages/errors, as
well as return the raw value (the JSON wrapper is not exposed to the user
as in elgg.action().)

A client plugin hook allows modifying the request data, and both client
and server-side hooks can modify the returned data.

Fixes Elgg#8323
  • Loading branch information
mrclay committed Sep 18, 2015
1 parent 367d3c3 commit 3e680ae
Show file tree
Hide file tree
Showing 12 changed files with 936 additions and 107 deletions.
11 changes: 11 additions & 0 deletions actions/ajax_example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

/**
* Remove before merge...
*/

system_message('A OK!');

echo json_encode([
'foo' => 'bar',
]);
283 changes: 233 additions & 50 deletions docs/guides/ajax.rst
Original file line number Diff line number Diff line change
@@ -1,31 +1,221 @@
Ajax
####

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

From JavaScript we can execute actions via XHR POST operations. Here's an example action and script for some basic math:
.. contents:: Contents
:local:
:depth: 2

.. code:: php
Overview
========

// in myplugin/actions/do_math.php
All perform the following actions:

if (!elgg_is_xhr()) {
register_error('Sorry, Ajax only!');
forward();
}
#. Client-side, the ``data`` option 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.
#. Echoed JSON is turned into a response object (``Elgg\Ajax\Response``).
#. 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 method returns a ``jqXHR`` object, which can be used as a Promise.

$arg1 = (int)get_input('arg1');
$arg2 = (int)get_input('arg2');
More notes:

system_message('We did it!');
* Each method has a different hook type (see the following subsections).
* The ``elgg/spinner`` module is automatically used during requests (but can be disabled).
* 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, but you can set it via ``options.method``.
* For client caching, set ``options.method`` to "GET" and ``options.data.response_ttl`` to the max-age you want in seconds.
* Do not use ``forward()`` with this API.

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

To execute an action, use ``performAction('action_name', options)``:

.. 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,
]);
.. code-block:: js
var ajax = require('elgg/ajax');
ajax.performAction('do_math', {
data: {
arg1: 1,
arg2: 2
},
}).done(function (output) {
alert(output.sum);
alert(output.product);
});
Notes for actions:

* All hooks have type ``action:<action_name>``. In this case, "action:do_math".
* CSRF tokens are added to the data.
* The default method is POST.


Fetching data
-------------

To fetch arbitrary data from registered page handlers, use ``fetchPath('url_path', options)``.

.. code-block:: php
// in myplugin/start.php
elgg_register_page_handler('myplugin_time', 'myplugin_get_time');
function myplugin_get_time() {
elgg_ajax_gatekeeper();
echo json_encode([
'rfc2822' => date(DATE_RFC2822),
'day' => date('l'),
]);
return true;
}
.. code-block:: js
var ajax = require('elgg/ajax');
ajax.fetchPath('myplugin_time').done(function (output) {
alert(output.rfc2822);
alert(output.day);
});
Notes for paths:

* All hooks 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.

Fetching views
--------------

To fetch a server-rendered view, use ``fetchView('view_name', options)``. If the view is a PHP file, you must register it for Ajax first:

.. 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;
}
.. code:: js
$object = $vars['entity'];
/* @var ElggObject $object */
echo elgg_view('output/url', [
'text' => $object->getDisplayName(),
'href' => $object->getUrl(),
'is_trusted' => true,
]);
.. code-block:: js
var ajax = require('elgg/ajax');
ajax.fetchView('myplugin/get_link', {
data: {
guid: 123 // querystring
},
}).done(function (output) {
$('.myplugin-link').html(output);
});
Notes for views:

* All hooks 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::

Ajax views must treat ``$vars`` as completely untrusted user data. You may want to use ``get_input()``
instead to make this clear to other developers. Review the use of ``$vars`` in an existing view before
registering it for Ajax fetching.


Fetching forms
--------------

Fetch server-rendered forms using ``fetchForm('action_name', options)``. You must register the view for Ajax first:

.. code-block:: php
// in myplugin_init()
elgg_register_ajax_view('forms/myplugin/add');
.. code-block:: js
var ajax = require('elgg/ajax');
ajax.fetchForm('myplugin/add').done(function (output) {
$('.myplugin-form-container').html(output);
});
Notes for forms:

* All hooks 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 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::

Request data are **not** passed into the ``input/form`` view! If you need to set ``$vars`` in the
``input/form`` view, you'll need to use the ``view_vars, input/form`` plugin hook (this can't
be done client-side).

.. warning::

Ajax views must treat ``$vars`` as completely untrusted user data. You may want to use ``get_input()``
instead to make this clear to other developers. Review the use of ``$vars`` in an existing form before
registering it for Ajax fetching.


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: {
Expand All @@ -37,23 +227,16 @@ From JavaScript we can execute actions via XHR POST operations. Here's an exampl
alert(wrapper.output.sum);
alert(wrapper.output.product);
} else {
// the system prevented the action from running
// the system prevented the action from running, but we really don't
// know why
elgg.ajax.handleAjaxError();
}
}
});
Basically what happens here:

#. CSRF tokens are added to the data.
#. The data is posted via XHR to http://localhost/elgg/action/example/add.
#. The action makes sure this is an XHR request, and returns a JSON string.
#. Once the action completes, Elgg builds a JSON response wrapper containing the echoed output.
#. Client-side Elgg extracts and displays the system message "We did it!" from the wrapper.
#. The ``success`` function receives the full wrapper object and validates the ``output`` key.
#. The browser alerts "3" then "2".
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.
Expand All @@ -66,10 +249,10 @@ elgg.action notes
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, check ``elgg_is_xhr()`` first.
* To make sure Ajax actions can only be executed via XHR, use ``elgg_ajax_gatekeeper()``.

The action JSON response wrapper
--------------------------------
elgg.action JSON response wrapper
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code::
Expand All @@ -86,18 +269,18 @@ The action JSON response wrapper
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.

Fetching Views
==============
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:: php
.. code-block:: php
// in myplugin_init()
elgg_register_ajax_view('myplugin/get_link');
.. code:: php
.. code-block:: php
// in myplugin/views/default/myplugin/get_link.php
Expand All @@ -114,7 +297,7 @@ link to an object given by its GUID:
'is_trusted' => true,
]);
.. code:: js
.. code-block:: js
elgg.get('ajax/view/myplugin/get_link', {
data: {
Expand All @@ -138,60 +321,60 @@ The Ajax view system works significantly differently than the action system.

.. warning::

Unlike views rendered server-side, Ajax views must treat ``$vars`` as completely untrusted user data.
Ajax views must treat ``$vars`` as completely untrusted user data.

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:: js
.. code-block:: js
elgg.getJSON('ajax/view/myplugin/get_times', {
success: function (data) {
alert('The time is ' + data.friendly_time);
}
});
Fetching Forms
==============
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:: php
.. code-block:: php
// in myplugin_init()
elgg_register_ajax_view('forms/myplugin/add');
.. code:: js
.. code-block:: js
elgg.get('ajax/form/myplugin/add, {
elgg.get('ajax/form/myplugin/add', {
success: function (output) {
$('.myplugin-form-container').html(output);
}
});
* The GET params will be passed as ``$vars`` to *your* view, **not** the ``input/form`` view.
* If you need to set ``$vars`` in the ``input/form`` view, you'll need to use the ``("view_vars", "input/form")``
plugin hook (this can't be done client-side).
* The GET params will be passed as ``$vars`` to *your* view, **not** the ``input/form`` view.
* If you need to set ``$vars`` in the ``input/form`` view, you'll need to use the ``("view_vars", "input/form")`` plugin hook (this can't be done client-side).

.. warning::

Unlike views rendered server-side, Ajax views must treat ``$vars`` as completely untrusted user data. Review
the use of ``$vars`` in an existing form before registering it for Ajax fetching.

Ajax helper functions
---------------------

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:: js
.. code-block:: js
// normalizes the url to the current <site_url>/activity
elgg.get('/activity', {
Expand Down
Loading

0 comments on commit 3e680ae

Please sign in to comment.