Warning
Starting from CubicWeb version 4.0 all code related to generating html views has been moved to the Cube cubicweb_web.
If you want to migrate a project from 3.38 to 4.* while still using all the
html views you need to both install the cubicweb_web cube AND add it to
your dependencies and run add_cube('web')
.
cubicweb_web can be installed from pypi this way:
pip install cubicweb_web
We donât plan to maintain the features in cubicweb_web in the long run; we are moving to a full javascript frontend using both cubicweb_api (which exposes a HTTP API) and @cubicweb/client as a frontend javascript toolkit.
In the long run cubicweb_api will be merged inside of CubicWeb.
Dissection of an entity form#
This is done (again) with a vanilla instance of the tracker cube. We will populate the database with a bunch of entities and see what kind of job the automatic entity form does.
Populating the database#
We should start by setting up a bit of context: a project with two unpublished versions, and a ticket linked to the project and the first version.
>>> p = rql('INSERT Project P: P name "cubicweb"')
>>> for num in ('0.1.0', '0.2.0'):
... rql('INSERT Version V: V num "%s", V version_of P WHERE P eid %%(p)s' % num, {'p': p[0][0]})
...
<resultset 'INSERT Version V: V num "0.1.0", V version_of P WHERE P eid %(p)s' (1 rows): [765L] (('Version',))>
<resultset 'INSERT Version V: V num "0.2.0", V version_of P WHERE P eid %(p)s' (1 rows): [766L] (('Version',))>
>>> t = rql('INSERT Ticket T: T title "let us write more doc", T done_in V, '
'T concerns P WHERE V num "0.1.0"', P eid %(p)s', {'p': p[0][0]})
>>> commit()
Now letâs see what the edition form builds for us.
>>> cnx.use_web_compatible_requests('http://fakeurl.com')
>>> req = cnx.request()
>>> form = req.vreg['forms'].select('edition', req, rset=rql('Ticket T'))
>>> html = form.render()
Note
In order to play interactively with web side application objects, we have to
cheat a bit to have request object that will looks like HTTP request object, by
calling use_web_compatible_requests()
on the connection.
This creates an automatic entity form. The .render()
call yields
an html (unicode) string. The html output is shown below (with
internal fieldset omitted).
Looking at the html output#
The form enveloppe#
<div class="iformTitle"><span>main informations</span></div>
<div class="formBody">
<form action="http://crater:9999/validateform" method="post" enctype="application/x-www-form-urlencoded"
id="entityForm" onsubmit="return freezeFormButtons('entityForm');"
class="entityForm" target="eformframe">
<div id="progress">validating...</div>
<fieldset>
<input name="__form_id" type="hidden" value="edition" />
<input name="__errorurl" type="hidden" value="http://perdu.com#entityForm" />
<input name="__domid" type="hidden" value="entityForm" />
<input name="__type:763" type="hidden" value="Ticket" />
<input name="eid" type="hidden" value="763" />
<input name="__maineid" type="hidden" value="763" />
<input name="_cw_edited_fields:763" type="hidden"
value="concerns-subject,done_in-subject,priority-subject,type-subject,title-subject,description-subject,__type,_cw_generic_field" />
...
</fieldset>
<iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);"></iframe>
</form>
</div>
The main fieldset encloses a set of hidden fields containing various metadata, that will be used by the edit controller to process it back correctly.
The freezeFormButtons(âŠ) javascript callback defined on the
onlick
event of the form element prevents accidental multiple
clicks in a row.
The action
of the form is mapped to the validateform
controller
(situated in cubicweb_web.views.basecontrollers
).
A full explanation of the validation loop is given in The form validation process.
The attributes section#
We can have a look at some of the inner nodes of the form. Some fields are omitted as they are redundant for our purposes.
<fieldset class="default">
<table class="attributeForm">
<tr class="title_subject_row">
<th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
<td>
<input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
type="text" value="let us write more doc" />
</td>
</tr>
... (description field omitted) ...
<tr class="priority_subject_row">
<th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
<td>
<select id="priority-subject:763" name="priority-subject:763" size="1">
<option value="important">important</option>
<option selected="selected" value="normal">normal</option>
<option value="minor">minor</option>
</select>
<div class="helper">importance</div>
</td>
</tr>
... (type field omitted) ...
<tr class="concerns_subject_row">
<th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
<td>
<select id="concerns-subject:763" name="concerns-subject:763" size="1">
<option selected="selected" value="760">Foo</option>
</select>
</td>
</tr>
<tr class="done_in_subject_row">
<th class="labelCol"><label for="done_in-subject:763">done in</label></th>
<td>
<select id="done_in-subject:763" name="done_in-subject:763" size="1">
<option value="__cubicweb_internal_field__"></option>
<option selected="selected" value="761">Foo 0.1.0</option>
<option value="762">Foo 0.2.0</option>
</select>
<div class="helper">version in which this ticket will be / has been done</div>
</td>
</tr>
</table>
</fieldset>
Note that the whole form layout has been computed by the form renderer. It is the renderer which produces the table structure. Otherwise, the fields html structure is emitted by their associated widget.
While it is called the attributes section of the form, it actually contains attributes and mandatory relations. For each field, we observe:
a dedicated row with a specific class, such as
title_subject_row
(responsability of the form renderer)an html widget (input, select, âŠ) with:
an id built from the
rtype-role:eid
patterna name built from the same pattern
possible values or preselected options
The relations section#
<fieldset class="This ticket :">
<legend>This ticket :</legend>
<table class="attributeForm">
<tr class="_cw_generic_field_None_row">
<td colspan="2">
<table id="relatedEntities">
<tr><th> </th><td> </td></tr>
<tr id="relationSelectorRow_763" class="separator">
<th class="labelCol">
<select id="relationSelector_763"
onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
<option value="">select a relation</option>
<option value="appeared_in_subject">appeared in</option>
<option value="custom_workflow_subject">custom workflow</option>
<option value="depends_on_object">dependency of</option>
<option value="depends_on_subject">depends on</option>
<option value="identical_to_subject">identical to</option>
<option value="see_also_subject">see also</option>
</select>
</th>
<td id="unrelatedDivs_763"></td>
</tr>
</table>
</td>
</tr>
</table>
</fieldset>
The optional relations are grouped into a drop-down combo box. Selection of an item triggers a javascript function which will:
show already related entities in the div of id relatedentities using a two-colown layout, with an action to allow deletion of individual relations (there are none in this example)
provide a relation selector in the div of id relationSelector_EID to allow the user to set up relations and trigger dynamic action on the last div
fill the div of id unrelatedDivs_EID with a dynamically computed selection widget allowing direct selection of an unrelated (but relatable) entity or a switch towards the search mode of CubicWeb which allows full browsing and selection of an entity using a dedicated action situated in the left column boxes.
The form validation process#
Validation loop#
On form submission, the form.action is invoked. Basically, the
validateform
controller is called and its output lands in the
specified target
, an invisible <iframe>
at the end of the
form.
Hence, the main page is not replaced, only the iframe contents. The
validateform
controller only outputs a tiny javascript fragment
which is then immediately executed.
<iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);">
<script type="text/javascript">
window.parent.handleFormValidationResponse('entityForm', null, null,
[false, [2164, {"name-subject": "required field"}], null],
null);
</script>
</iframe>
The window.parent
part ensures the javascript function is called
on the right context (that is: the form element). We will describe its
parameters:
first comes the form id (entityForm)
then two optional callbacks for the success and failure case
an array containing:
a boolean which indicates status (success or failure), and then, on error:
an array structured as
[eid, {'rtype-role': 'error msg'}, ...]
on success:
a url (string) representing the next thing to jump to
Given the array structure described above, it is quite simple to manipulate the DOM to show the errors at appropriate places.
Explanation#
This mecanism may seem a bit overcomplicated but we have to deal with two realities:
in the (strict) XHTML world, there are no iframes (hence the dynamic inclusion, tolerated by Firefox)
no (or not all) browser(s) support file input field handling through ajax.