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.
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.
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:
On the python side, we have to define an cubicweb.web.views.ajaxcontroller.AjaxFunction object. The simplest way to do that is to use the cubicweb.web.views.ajaxcontroller.ajaxfunc() decorator (for more details on this, refer to Ajax).
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.
reloadComponent allows to dynamically replace some DOM node with new elements. It has the following signature:
The server side implementation of reloadComponent is the 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.
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.
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.
About the callback option:
This mechanism allows callback chaining.
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.
from cubicweb.web.views.ajaxcontroller import ajaxfunc
@ajaxfunc(output_type='xhtml')
def frob_status(self, eid, frobname):
entity = self._cw.entity_from_eid(eid)
return entity.view('frob', name=frobname)
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 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.
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:
The javascript side is quite simple, due to loadxhtml awesomeness.
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.
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.
function triggerLoad(divid) {
jQuery('#lazy-' + divd).trigger('load_' + divid);
}
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:
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.
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.
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.
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")
),
)