doc/book/en/devweb/js.rst
author Aurelien Campeas <aurelien.campeas@logilab.fr>
Wed, 24 Oct 2012 12:08:21 +0200
branchstable
changeset 8582 b0e086f451b7
parent 8128 0a927fe4541b
child 8987 d9195dce3a5b
permissions -rw-r--r--
prepare 3.15.5

.. -*- coding: utf-8 -*-

Javascript
----------

*CubicWeb* uses quite a bit of javascript in its user interface and
ships with jquery (1.3.x) and parts of the jquery UI library, plus a
number of homegrown files and also other third party libraries.

All javascript files are stored in cubicweb/web/data/. There are
around thirty js files there. In a cube it goes to data/.

Obviously one does not want javascript pieces to be loaded all at
once, hence the framework provides a number of mechanisms and
conventions to deal with javascript resources.

Conventions
~~~~~~~~~~~

It is good practice to name cube specific js files after the name of
the cube, like this : 'cube.mycube.js', so as to avoid name clashes.

.. XXX external_resources variable (which needs love)

Server-side Javascript API
~~~~~~~~~~~~~~~~~~~~~~~~~~

Javascript resources are typically loaded on demand, from views. The
request object (available as self._cw from most application objects,
for instance views and entities objects) has a few methods to do that:

* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
  javascript files and writes proper entries into the HTML header
  section. The localfile parameter allows to declare resources which
  are not from web/data (for instance, residing on a content delivery
  network).

* `add_onload(self, jscode)` which adds one raw javascript code
  snippet inline in the html headers. This is quite useful for setting
  up early jQuery(document).ready(...) initialisations.

Javascript events
~~~~~~~~~~~~~~~~~

* ``server-response``: this event is triggered on HTTP responses (both
  standard and ajax). The two following extra parameters are passed
  to callbacks :

  - ``ajax``: a boolean that says if the reponse was issued by an
    ajax request

  - ``node``: the DOM node returned by the server in case of an
    ajax request, otherwise the document itself for standard HTTP
    requests.

Important javascript AJAX APIS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* `asyncRemoteExec` and `remoteExec` are the base building blocks for
  doing arbitrary async (resp. sync) communications with the server

* `reloadComponent` is a convenience function to replace a DOM node
  with server supplied content coming from a specific registry (this
  is quite handy to refresh the content of some boxes for instances)

* `jQuery.fn.loadxhtml` is an important extension to jQuery which
  allows proper loading and in-place DOM update of xhtml views. It is
  suitably augmented to trigger necessary events, and process CubicWeb
  specific elements such as the facet system, fckeditor, etc.


A simple example with asyncRemoteExec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

On the python side, we have to define an
:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
simplest way to do that is to use the
:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
details on this, refer to :ref:`ajax`).

.. sourcecode: python

    from cubicweb.web.views.ajaxcontroller import ajaxfunc

    # serialize output to json to get it back easily on the javascript side
    @ajaxfunc(output_type='json')
    def js_say_hello(self, name):
        return u'hello %s' % name

On the javascript side, we do the asynchronous call. Notice how it
creates a `deferred` object. Proper treatment of the return value or
error handling has to be done through the addCallback and addErrback
methods.

.. sourcecode: javascript

    function asyncHello(name) {
        var deferred = asyncRemoteExec('say_hello', name);
        deferred.addCallback(function (response) {
            alert(response);
        });
        deferred.addErrback(function (error) {
            alert('something fishy happened');
        });
     }

     function syncHello(name) {
         alert( remoteExec('say_hello', name) );
     }

Anatomy of a reloadComponent call
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`reloadComponent` allows to dynamically replace some DOM node with new
elements. It has the following signature:

* `compid` (mandatory) is the name of the component to be reloaded

* `rql` (optional) will be used to generate a result set given as
  argument to the selected component

* `registry` (optional) defaults to 'components' but can be any other
  valid registry name

* `nodeid` (optional) defaults to compid + 'Component' but can be any
  explicitly specified DOM node id

* `extraargs` (optional) should be a dictionary of values that will be
  given to the cell_call method of the component

A simple reloadComponent example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The server side implementation of `reloadComponent` is the
:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.

The following function implements a two-steps method to delete a
standard bookmark and refresh the UI, while keeping the UI responsive.

.. sourcecode:: javascript

    function removeBookmark(beid) {
        d = asyncRemoteExec('delete_bookmark', beid);
        d.addCallback(function(boxcontent) {
	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
            document.location.hash = '#header';
            updateMessage(_("bookmark has been removed"));
         });
    }

`reloadComponent` is called with the id of the bookmark box as
argument, no rql expression (because the bookmarks display is actually
independant of any dataset context), a reference to the 'boxes'
registry (which hosts all left, right and contextual boxes) and
finally an explicit 'bookmarks_box' nodeid argument that stipulates
the target DOM node.

Anatomy of a loadxhtml call
~~~~~~~~~~~~~~~~~~~~~~~~~~~

`jQuery.fn.loadxhtml` is an important extension to jQuery which allows
proper loading and in-place DOM update of xhtml views. The existing
`jQuery.load`_ function does not handle xhtml, hence the addition. The
API of loadxhtml is roughly similar to that of `jQuery.load`_.

.. _`jQuery.load`: http://api.jquery.com/load/


* `url` (mandatory) should be a complete url (typically referencing
  the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
  but this is not strictly mandatory)

* `data` (optional) is a dictionary of values given to the
  controller specified through an `url` argument; some keys may have a
  special meaning depending on the choosen controller (such as `fname`
  for the JSonController); the `callback` key, if present, must refer
  to a function to be called at the end of loadxhtml (more on this
  below)

* `reqtype` (optional) specifies the request method to be used (get or
  post); if the argument is 'post', then the post method is used,
  otherwise the get method is used

* `mode` (optional) is one of `replace` (the default) which means the
  loaded node will replace the current node content, `swap` to replace
  the current node with the loaded node, and `append` which will
  append the loaded node to the current node content

About the `callback` option:

* it is called with two parameters: the current node, and a list
  containing the loaded (and post-processed node)

* whenever is returns another function, this function is called in
  turn with the same parameters as above

This mechanism allows callback chaining.


A simple example with loadxhtml
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Here we are concerned with the retrieval of a specific view to be
injected in the live DOM. The view will be of course selected
server-side using an entity eid provided by the client side.

.. sourcecode:: python

    from cubicweb.web.views.ajaxcontroller import ajaxfunc

    @ajaxfunc(output_type='xhtml')
    def js_frob_status(self, eid, frobname):
        entity = self._cw.entity_from_eid(eid)
        return entity.view('frob', name=frobname)

.. sourcecode:: javascript

    function updateSomeDiv(divid, eid, frobname) {
        var params = {fname:'frob_status', eid: eid, frobname:frobname};
        jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
     }

In this example, the url argument is the base json url of a cube
instance (it should contain something like
`http://myinstance/ajax?`). The actual AjaxController method name is
encoded in the `params` dictionary using the `fname` key.

A more real-life example
~~~~~~~~~~~~~~~~~~~~~~~~

A frequent need of Web 2 applications is the delayed (or demand
driven) loading of pieces of the DOM. This is typically achieved using
some preparation of the initial DOM nodes, jQuery event handling and
proper use of loadxhtml.

We present here a skeletal version of the mecanism used in CubicWeb
and available in web/views/tabs.py, in the `LazyViewMixin` class.

.. sourcecode:: python

    def lazyview(self, vid, rql=None):
        """ a lazy version of wview """
        w = self.w
        self._cw.add_js('cubicweb.lazy.js')
        urlparams = {'vid' : vid, 'fname' : 'view'}
        if rql is not None:
            urlparams['rql'] = rql
        w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
            vid, xml_escape(self._cw.build_url('json', **urlparams))))
        w(u'</div>')
        self._cw.add_onload(u"""
            jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
                   loadNow('#lazy-%(vid)s');});"""
            % {'event': 'load_%s' % vid, 'vid': vid})

This creates a `div` with a specific event associated to it.

The full version deals with:

* optional parameters such as an entity eid, an rset

* the ability to further reload the fragment

* the ability to display a spinning wheel while the fragment is still
  not loaded

* handling of browsers that do not support ajax (search engines,
  text-based browsers such as lynx, etc.)

The javascript side is quite simple, due to loadxhtml awesomeness.

.. sourcecode:: javascript

    function loadNow(eltsel) {
        var lazydiv = jQuery(eltsel);
        lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
    }

This is all significantly different of the previous `simple example`
(albeit this example actually comes from real-life code).

Notice how the `cubicweb:loadurl` is used to convey the url
information. The base of this url is similar to the global javascript
JSON_BASE_URL. According to the pattern described earlier,
the `fname` parameter refers to the standard `js_view` method of the
JSonController. This method renders an arbitrary view provided a view
id (or `vid`) is provided, and most likely an rql expression yielding
a result set against which a proper view instance will be selected.

The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
in a specific cubicweb namespace. It is a means to pass information
without breaking HTML nor XHTML compliance and without resorting to
ungodly hacks.

Given all this, it is easy to add a small nevertheless useful feature
to force the loading of a lazy view (for instance, a very
computation-intensive web page could be scinded into one fast-loading
part and a delayed part).

On the server side, a simple call to a javascript function is
sufficient.

.. sourcecode:: python

    def forceview(self, vid):
        """trigger an event that will force immediate loading of the view
        on dom readyness
        """
        self._cw.add_onload("triggerLoad('%s');" % vid)

The browser-side definition follows.

.. sourcecode:: javascript

    function triggerLoad(divid) {
        jQuery('#lazy-' + divd).trigger('load_' + divid);
    }


python/ajax dynamic callbacks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

CubicWeb provides a way to dynamically register a function and make it
callable from the javascript side. The typical use case for this is a
situation where you have everything at hand to implement an action
(whether it be performing a RQL query or executing a few python
statements) that you'd like to defer to a user click in the web
interface.  In other words, generate an HTML ``<a href=...`` link that
would execute your few lines of code.

The trick is to create a python function and store this function in
the user's session data. You will then be able to access it later.
While this might sound hard to implement, it's actually quite easy
thanks to the ``_cw.user_callback()``. This method takes a function,
registers it and returns a javascript instruction suitable for
``href`` or ``onclick`` usage. The call is then performed
asynchronously.

Here's a simplified example taken from the vcreview_ cube that will
generate a link to change an entity state directly without the
standard intermediate *comment / validate* step:

.. sourcecode:: python

    def entity_call(self, entity):
        # [...]
        def change_state(req, eid):
            entity = req.entity_from_eid(eid)
            entity.cw_adapt_to('IWorkflowable').fire_transition('done')
        url = self._cw.user_callback(change_state, (entity.eid,))
        self.w(tags.input(type='button', onclick=url, value=self._cw._('mark as done')))


The ``change_state`` callback function is registered with
``self._cw.user_callback()`` which returns the ``url`` value directly
used for the ``onclick`` attribute of the button. On the javascript
side, the ``userCallback()`` function is used but you most probably
won't have to bother with it.

Of course, when dealing with session data, the question of session
cleaning pops up immediately. If you use ``user_callback()``, the
registered function will be deleted automatically at some point
as any other session data. If you want your function to be deleted once
the web page is unloaded or when the user has clicked once on your link, then
``_cw.register_onetime_callback()`` is what you need. It behaves as
``_cw.user_callback()`` but stores the function in page data instead
of global session data.


.. Warning::

  Be careful when registering functions with closures, keep in mind that
  enclosed data will be kept in memory until the session gets cleared. Also,
  if you keep entities or any object referecing the current ``req`` object, you
  might have problems reusing them later because the underlying session
  might have been closed at the time the callback gets executed.


.. _vcreview: http://www.cubicweb.org/project/cubicweb-vcreview

Javascript library: overview
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* jquery.* : jquery and jquery UI library

* cubicweb.ajax.js : concentrates all ajax related facilities (it
  extends jQuery with the loahxhtml function, provides a handfull of
  high-level ajaxy operations like asyncRemoteExec, reloadComponent,
  replacePageChunk, getDomFromResponse)

* cubicweb.python.js : adds a number of practical extension to stdanrd
  javascript objects (on Date, Array, String, some list and dictionary
  operations), and a pythonesque way to build classes. Defines a
  CubicWeb namespace.

* cubicweb.htmlhelpers.js : a small bag of convenience functions used
  in various other cubicweb javascript resources (baseuri, progress
  cursor handling, popup login box, html2dom function, etc.)

* cubicweb.widgets.js : provides a widget namespace and constructors
  and helpers for various widgets (mainly facets and timeline)

* cubicweb.edition.js : used by edition forms

* cubicweb.preferences.js : used by the preference form

* cubicweb.facets.js : used by the facets mechanism

There is also javascript support for massmailing, gmap (google maps),
fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
AppEngine), flot (charts drawing), tabs and bookmarks.

API
~~~

.. toctree::
    :maxdepth: 1

    js_api/index


Testing javascript
~~~~~~~~~~~~~~~~~~

You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
inside the python unittest run . You simply have to define a new class that
inherit from ``QUnitTestCase`` and register your javascript test file in the
``all_js_tests`` lclass attribut. This  ``all_js_tests`` is a sequence a
3-tuple (<test_file, [<dependencies> ,] [<data_files>]):

The <test_file> should contains the qunit test. <dependencies> defines the list
of javascript file that must be imported before the test script.  Dependencies
are included their definition order. <data_files> are additional files copied in the
test directory. both <dependencies> and <data_files> are optionnal.
``jquery.js`` is preincluded in for all test.

.. sourcecode:: python

    from cubicweb.qunit import QUnitTestCase

    class MyQUnitTest(QUnitTestCase):

        all_js_tests = (
            ("relative/path/to/my_simple_testcase.js",)
            ("relative/path/to/my_qunit_testcase.js",(
                "rel/path/to/dependency_1.js",
                "rel/path/to/dependency_2.js",)),
            ("relative/path/to/my_complexe_qunit_testcase.js",(
                 "rel/path/to/dependency_1.js",
                 "rel/path/to/dependency_2.js",
               ),(
                 "rel/path/file_dependency.html",
                 "path/file_dependency.json")
                ),
            )