1 .. -*- coding: utf-8 -*- |
|
2 |
|
3 Javascript |
|
4 ---------- |
|
5 |
|
6 *CubicWeb* uses quite a bit of javascript in its user interface and |
|
7 ships with jquery (1.3.x) and parts of the jquery UI library, plus a |
|
8 number of homegrown files and also other third party libraries. |
|
9 |
|
10 All javascript files are stored in cubicweb/web/data/. There are |
|
11 around thirty js files there. In a cube it goes to data/. |
|
12 |
|
13 Obviously one does not want javascript pieces to be loaded all at |
|
14 once, hence the framework provides a number of mechanisms and |
|
15 conventions to deal with javascript resources. |
|
16 |
|
17 Conventions |
|
18 ~~~~~~~~~~~ |
|
19 |
|
20 It is good practice to name cube specific js files after the name of |
|
21 the cube, like this : 'cube.mycube.js', so as to avoid name clashes. |
|
22 |
|
23 .. XXX external_resources variable (which needs love) |
|
24 |
|
25 Server-side Javascript API |
|
26 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
27 |
|
28 Javascript resources are typically loaded on demand, from views. The |
|
29 request object (available as self._cw from most application objects, |
|
30 for instance views and entities objects) has a few methods to do that: |
|
31 |
|
32 * `add_js(self, jsfiles, localfile=True)` which takes a sequence of |
|
33 javascript files and writes proper entries into the HTML header |
|
34 section. The localfile parameter allows to declare resources which |
|
35 are not from web/data (for instance, residing on a content delivery |
|
36 network). |
|
37 |
|
38 * `add_onload(self, jscode)` which adds one raw javascript code |
|
39 snippet inline in the html headers. This is quite useful for setting |
|
40 up early jQuery(document).ready(...) initialisations. |
|
41 |
|
42 Javascript events |
|
43 ~~~~~~~~~~~~~~~~~ |
|
44 |
|
45 * ``server-response``: this event is triggered on HTTP responses (both |
|
46 standard and ajax). The two following extra parameters are passed |
|
47 to callbacks : |
|
48 |
|
49 - ``ajax``: a boolean that says if the reponse was issued by an |
|
50 ajax request |
|
51 |
|
52 - ``node``: the DOM node returned by the server in case of an |
|
53 ajax request, otherwise the document itself for standard HTTP |
|
54 requests. |
|
55 |
|
56 Important javascript AJAX APIS |
|
57 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
58 |
|
59 * `asyncRemoteExec` and `remoteExec` are the base building blocks for |
|
60 doing arbitrary async (resp. sync) communications with the server |
|
61 |
|
62 * `reloadComponent` is a convenience function to replace a DOM node |
|
63 with server supplied content coming from a specific registry (this |
|
64 is quite handy to refresh the content of some boxes for instances) |
|
65 |
|
66 * `jQuery.fn.loadxhtml` is an important extension to jQuery which |
|
67 allows proper loading and in-place DOM update of xhtml views. It is |
|
68 suitably augmented to trigger necessary events, and process CubicWeb |
|
69 specific elements such as the facet system, fckeditor, etc. |
|
70 |
|
71 |
|
72 A simple example with asyncRemoteExec |
|
73 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
74 |
|
75 On the python side, we have to define an |
|
76 :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The |
|
77 simplest way to do that is to use the |
|
78 :func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more |
|
79 details on this, refer to :ref:`ajax`). |
|
80 |
|
81 .. sourcecode: python |
|
82 |
|
83 from cubicweb.web.views.ajaxcontroller import ajaxfunc |
|
84 |
|
85 # serialize output to json to get it back easily on the javascript side |
|
86 @ajaxfunc(output_type='json') |
|
87 def js_say_hello(self, name): |
|
88 return u'hello %s' % name |
|
89 |
|
90 On the javascript side, we do the asynchronous call. Notice how it |
|
91 creates a `deferred` object. Proper treatment of the return value or |
|
92 error handling has to be done through the addCallback and addErrback |
|
93 methods. |
|
94 |
|
95 .. sourcecode: javascript |
|
96 |
|
97 function asyncHello(name) { |
|
98 var deferred = asyncRemoteExec('say_hello', name); |
|
99 deferred.addCallback(function (response) { |
|
100 alert(response); |
|
101 }); |
|
102 deferred.addErrback(function (error) { |
|
103 alert('something fishy happened'); |
|
104 }); |
|
105 } |
|
106 |
|
107 function syncHello(name) { |
|
108 alert( remoteExec('say_hello', name) ); |
|
109 } |
|
110 |
|
111 Anatomy of a reloadComponent call |
|
112 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
113 |
|
114 `reloadComponent` allows to dynamically replace some DOM node with new |
|
115 elements. It has the following signature: |
|
116 |
|
117 * `compid` (mandatory) is the name of the component to be reloaded |
|
118 |
|
119 * `rql` (optional) will be used to generate a result set given as |
|
120 argument to the selected component |
|
121 |
|
122 * `registry` (optional) defaults to 'components' but can be any other |
|
123 valid registry name |
|
124 |
|
125 * `nodeid` (optional) defaults to compid + 'Component' but can be any |
|
126 explicitly specified DOM node id |
|
127 |
|
128 * `extraargs` (optional) should be a dictionary of values that will be |
|
129 given to the cell_call method of the component |
|
130 |
|
131 A simple reloadComponent example |
|
132 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
133 |
|
134 The server side implementation of `reloadComponent` is the |
|
135 :func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject. |
|
136 |
|
137 The following function implements a two-steps method to delete a |
|
138 standard bookmark and refresh the UI, while keeping the UI responsive. |
|
139 |
|
140 .. sourcecode:: javascript |
|
141 |
|
142 function removeBookmark(beid) { |
|
143 d = asyncRemoteExec('delete_bookmark', beid); |
|
144 d.addCallback(function(boxcontent) { |
|
145 reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box'); |
|
146 document.location.hash = '#header'; |
|
147 updateMessage(_("bookmark has been removed")); |
|
148 }); |
|
149 } |
|
150 |
|
151 `reloadComponent` is called with the id of the bookmark box as |
|
152 argument, no rql expression (because the bookmarks display is actually |
|
153 independant of any dataset context), a reference to the 'boxes' |
|
154 registry (which hosts all left, right and contextual boxes) and |
|
155 finally an explicit 'bookmarks_box' nodeid argument that stipulates |
|
156 the target DOM node. |
|
157 |
|
158 Anatomy of a loadxhtml call |
|
159 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
160 |
|
161 `jQuery.fn.loadxhtml` is an important extension to jQuery which allows |
|
162 proper loading and in-place DOM update of xhtml views. The existing |
|
163 `jQuery.load`_ function does not handle xhtml, hence the addition. The |
|
164 API of loadxhtml is roughly similar to that of `jQuery.load`_. |
|
165 |
|
166 .. _`jQuery.load`: http://api.jquery.com/load/ |
|
167 |
|
168 |
|
169 * `url` (mandatory) should be a complete url (typically referencing |
|
170 the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`, |
|
171 but this is not strictly mandatory) |
|
172 |
|
173 * `data` (optional) is a dictionary of values given to the |
|
174 controller specified through an `url` argument; some keys may have a |
|
175 special meaning depending on the choosen controller (such as `fname` |
|
176 for the JSonController); the `callback` key, if present, must refer |
|
177 to a function to be called at the end of loadxhtml (more on this |
|
178 below) |
|
179 |
|
180 * `reqtype` (optional) specifies the request method to be used (get or |
|
181 post); if the argument is 'post', then the post method is used, |
|
182 otherwise the get method is used |
|
183 |
|
184 * `mode` (optional) is one of `replace` (the default) which means the |
|
185 loaded node will replace the current node content, `swap` to replace |
|
186 the current node with the loaded node, and `append` which will |
|
187 append the loaded node to the current node content |
|
188 |
|
189 About the `callback` option: |
|
190 |
|
191 * it is called with two parameters: the current node, and a list |
|
192 containing the loaded (and post-processed node) |
|
193 |
|
194 * whenever it returns another function, this function is called in |
|
195 turn with the same parameters as above |
|
196 |
|
197 This mechanism allows callback chaining. |
|
198 |
|
199 |
|
200 A simple example with loadxhtml |
|
201 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
202 |
|
203 Here we are concerned with the retrieval of a specific view to be |
|
204 injected in the live DOM. The view will be of course selected |
|
205 server-side using an entity eid provided by the client side. |
|
206 |
|
207 .. sourcecode:: python |
|
208 |
|
209 from cubicweb.web.views.ajaxcontroller import ajaxfunc |
|
210 |
|
211 @ajaxfunc(output_type='xhtml') |
|
212 def frob_status(self, eid, frobname): |
|
213 entity = self._cw.entity_from_eid(eid) |
|
214 return entity.view('frob', name=frobname) |
|
215 |
|
216 .. sourcecode:: javascript |
|
217 |
|
218 function updateSomeDiv(divid, eid, frobname) { |
|
219 var params = {fname:'frob_status', eid: eid, frobname:frobname}; |
|
220 jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post'); |
|
221 } |
|
222 |
|
223 In this example, the url argument is the base json url of a cube |
|
224 instance (it should contain something like |
|
225 `http://myinstance/ajax?`). The actual AjaxController method name is |
|
226 encoded in the `params` dictionary using the `fname` key. |
|
227 |
|
228 A more real-life example |
|
229 ~~~~~~~~~~~~~~~~~~~~~~~~ |
|
230 |
|
231 A frequent need of Web 2 applications is the delayed (or demand |
|
232 driven) loading of pieces of the DOM. This is typically achieved using |
|
233 some preparation of the initial DOM nodes, jQuery event handling and |
|
234 proper use of loadxhtml. |
|
235 |
|
236 We present here a skeletal version of the mecanism used in CubicWeb |
|
237 and available in web/views/tabs.py, in the `LazyViewMixin` class. |
|
238 |
|
239 .. sourcecode:: python |
|
240 |
|
241 def lazyview(self, vid, rql=None): |
|
242 """ a lazy version of wview """ |
|
243 w = self.w |
|
244 self._cw.add_js('cubicweb.lazy.js') |
|
245 urlparams = {'vid' : vid, 'fname' : 'view'} |
|
246 if rql is not None: |
|
247 urlparams['rql'] = rql |
|
248 w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % ( |
|
249 vid, xml_escape(self._cw.build_url('json', **urlparams)))) |
|
250 w(u'</div>') |
|
251 self._cw.add_onload(u""" |
|
252 jQuery('#lazy-%(vid)s').bind('%(event)s', function() { |
|
253 loadNow('#lazy-%(vid)s');});""" |
|
254 % {'event': 'load_%s' % vid, 'vid': vid}) |
|
255 |
|
256 This creates a `div` with a specific event associated to it. |
|
257 |
|
258 The full version deals with: |
|
259 |
|
260 * optional parameters such as an entity eid, an rset |
|
261 |
|
262 * the ability to further reload the fragment |
|
263 |
|
264 * the ability to display a spinning wheel while the fragment is still |
|
265 not loaded |
|
266 |
|
267 * handling of browsers that do not support ajax (search engines, |
|
268 text-based browsers such as lynx, etc.) |
|
269 |
|
270 The javascript side is quite simple, due to loadxhtml awesomeness. |
|
271 |
|
272 .. sourcecode:: javascript |
|
273 |
|
274 function loadNow(eltsel) { |
|
275 var lazydiv = jQuery(eltsel); |
|
276 lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl')); |
|
277 } |
|
278 |
|
279 This is all significantly different of the previous `simple example` |
|
280 (albeit this example actually comes from real-life code). |
|
281 |
|
282 Notice how the `cubicweb:loadurl` is used to convey the url |
|
283 information. The base of this url is similar to the global javascript |
|
284 JSON_BASE_URL. According to the pattern described earlier, |
|
285 the `fname` parameter refers to the standard `js_view` method of the |
|
286 JSonController. This method renders an arbitrary view provided a view |
|
287 id (or `vid`) is provided, and most likely an rql expression yielding |
|
288 a result set against which a proper view instance will be selected. |
|
289 |
|
290 The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML |
|
291 in a specific cubicweb namespace. It is a means to pass information |
|
292 without breaking HTML nor XHTML compliance and without resorting to |
|
293 ungodly hacks. |
|
294 |
|
295 Given all this, it is easy to add a small nevertheless useful feature |
|
296 to force the loading of a lazy view (for instance, a very |
|
297 computation-intensive web page could be scinded into one fast-loading |
|
298 part and a delayed part). |
|
299 |
|
300 On the server side, a simple call to a javascript function is |
|
301 sufficient. |
|
302 |
|
303 .. sourcecode:: python |
|
304 |
|
305 def forceview(self, vid): |
|
306 """trigger an event that will force immediate loading of the view |
|
307 on dom readyness |
|
308 """ |
|
309 self._cw.add_onload("triggerLoad('%s');" % vid) |
|
310 |
|
311 The browser-side definition follows. |
|
312 |
|
313 .. sourcecode:: javascript |
|
314 |
|
315 function triggerLoad(divid) { |
|
316 jQuery('#lazy-' + divd).trigger('load_' + divid); |
|
317 } |
|
318 |
|
319 |
|
320 Javascript library: overview |
|
321 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
322 |
|
323 * jquery.* : jquery and jquery UI library |
|
324 |
|
325 * cubicweb.ajax.js : concentrates all ajax related facilities (it |
|
326 extends jQuery with the loahxhtml function, provides a handfull of |
|
327 high-level ajaxy operations like asyncRemoteExec, reloadComponent, |
|
328 replacePageChunk, getDomFromResponse) |
|
329 |
|
330 * cubicweb.python.js : adds a number of practical extension to stdanrd |
|
331 javascript objects (on Date, Array, String, some list and dictionary |
|
332 operations), and a pythonesque way to build classes. Defines a |
|
333 CubicWeb namespace. |
|
334 |
|
335 * cubicweb.htmlhelpers.js : a small bag of convenience functions used |
|
336 in various other cubicweb javascript resources (baseuri, progress |
|
337 cursor handling, popup login box, html2dom function, etc.) |
|
338 |
|
339 * cubicweb.widgets.js : provides a widget namespace and constructors |
|
340 and helpers for various widgets (mainly facets and timeline) |
|
341 |
|
342 * cubicweb.edition.js : used by edition forms |
|
343 |
|
344 * cubicweb.preferences.js : used by the preference form |
|
345 |
|
346 * cubicweb.facets.js : used by the facets mechanism |
|
347 |
|
348 There is also javascript support for massmailing, gmap (google maps), |
|
349 fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over |
|
350 AppEngine), flot (charts drawing), tabs and bookmarks. |
|
351 |
|
352 API |
|
353 ~~~ |
|
354 |
|
355 .. toctree:: |
|
356 :maxdepth: 1 |
|
357 |
|
358 js_api/index |
|
359 |
|
360 |
|
361 Testing javascript |
|
362 ~~~~~~~~~~~~~~~~~~ |
|
363 |
|
364 You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests |
|
365 inside the python unittest run . You simply have to define a new class that |
|
366 inherit from ``QUnitTestCase`` and register your javascript test file in the |
|
367 ``all_js_tests`` lclass attribut. This ``all_js_tests`` is a sequence a |
|
368 3-tuple (<test_file, [<dependencies> ,] [<data_files>]): |
|
369 |
|
370 The <test_file> should contains the qunit test. <dependencies> defines the list |
|
371 of javascript file that must be imported before the test script. Dependencies |
|
372 are included their definition order. <data_files> are additional files copied in the |
|
373 test directory. both <dependencies> and <data_files> are optionnal. |
|
374 ``jquery.js`` is preincluded in for all test. |
|
375 |
|
376 .. sourcecode:: python |
|
377 |
|
378 from cubicweb.qunit import QUnitTestCase |
|
379 |
|
380 class MyQUnitTest(QUnitTestCase): |
|
381 |
|
382 all_js_tests = ( |
|
383 ("relative/path/to/my_simple_testcase.js",) |
|
384 ("relative/path/to/my_qunit_testcase.js",( |
|
385 "rel/path/to/dependency_1.js", |
|
386 "rel/path/to/dependency_2.js",)), |
|
387 ("relative/path/to/my_complexe_qunit_testcase.js",( |
|
388 "rel/path/to/dependency_1.js", |
|
389 "rel/path/to/dependency_2.js", |
|
390 ),( |
|
391 "rel/path/file_dependency.html", |
|
392 "path/file_dependency.json") |
|
393 ), |
|
394 ) |
|