One fashionable avenue in Web application design has been that of updating Web pages in applications without having to refresh the entire page every time an action is performed. Together with some JavaScript support in the browser, XSLForms also provides some functionality for such "in-page" or "live" updates.
Consider the addition of a comment field to our application. Here is how the HTML code might look:
<div template:element="item">
<p>
Some item: <input template:attribute-field="value" name="..." type="text" value="..." />
<input name="..." template:selector-field="remove" type="submit" value="Remove" />
</p>
<p>
Item type:
<select template:multiple-choice-list-field="type,type-enum,value" name="..." multiple="multiple">
<option template:multiple-choice-list-value="type-enum,value,selected" value="..." />
</select>
</p>
<p template:element="options">
<span template:element="comment">Comment:
<textarea template:attribute-area="value,insert" name="..." cols="40" rows="3">
Some comment
</textarea>
</span>
</p>
<p>
Itself containing more items:
</p>
<p template:element="subitem">
Sub-item: <input template:attribute-field="subvalue" name="..." type="text" value="..." />
<input name="..." template:selector-field="remove2" type="submit" value="Remove" />
</p>
<p>
<input name="..." template:selector-field="add2,subitem" type="submit" value="Add subitem" />
</p>
</div>
Here, a textarea
element has been added with a special template:attribute-area
annotation being used to state that the contents of the element are to be mapped to the value
attribute, and that the attribute contents are to be inserted inside the textarea
element (replacing the Some Comment
text).
The newly-added textarea
field might actually be
presented in the application in its current state, despite the lack of any options
or comment
elements
manipulated by the
application, due to the document initialisation mechanism employed by
the application. However, what would be more interesting is the
possibility of only showing the comment field if something else in the
document had a certain value or state.
Let us imagine that if the type of an item was set to "Personal",
the comment field would appear and permit the recording of some text
for that item. One approach that would make this possible is to
add a transformation which checks the type values set for each of the
items and removes the options
and comment
elements for items which do not qualify. In the Web resource, we make the following
changes:
transform_resources = {
"comments" : ["structure_comments.xsl"]
}
What this does is to state that when we carry out the comments
transformation, the specified stylesheet is employed, filtering out the
comments for non-qualifying items and preserving them for qualifying
items.
Further down in the code, we add a transformation:
# After the document initialisation...
# Add the comments.
comments_xsl_list = self.prepare_transform("comments")
structure = self.get_result(comments_xsl_list, structure)
This new stylesheet works according to the following principles:
item
element (which the
stylesheet is programmed to recognise), do the following:value
attribute is retained.options
element and process it.options
element, do the
following:options
element, investigate
the values associated with the type
element.comment
element, then add any attributes that may be found on
existing comment
elements within the current type
element.Since this stylesheet is used after the document initialisation,
we may (and even must) take advantage of the results of that activity, including noting that selected values on type-enum
elements are marked with the value-is-set
attribute.
The stylesheet source code can be found in examples/Common/VerySimple/Resources/structure_comments.xsl
.
Whilst the above modifications adds a comment field for each item
with a type of "Personal", and whilst the comment field will appear and
disappear for items as their type changes, such updates only take place
when items and subitems are added and removed. We could add an update
button to the page which performs an explicit refresh of the page
without adding or removing anything, and for the sake of usability, we
probably should add such a button (just below the Add item
button):
<p>
<input name="update" type="submit" value="Update" />
</p>
However, we could also add an in-page update to make each comments field appear and disappear as soon as we have changed the type of an item.
We must first define a region of the template where a comment fields can be added and removed, regardless of whether such a field existed there before. The above template code needs modifying slightly to permit this:
<p template:element="options" template:id="comment-node" id="{template:this-element()}">
<span template:element="comment">Comment:
<textarea template:attribute-area="value,insert" name="..." cols="40" rows="3">
Some comment
</textarea>
</span>
</p>
Here, we have added this region definition to the paragraph surrounding the comment field, annotating the paragraph with the following attributes:
template:id
attribute is used to define a
template fragment used only to prepare the updated part of the Web
page. Here we define the fragment or region as being just this
paragraph.id
attribute is used to
define which part of the active Web page will be replaced when
performing an in-page update. This attribute needs to have a unique
value, but the easiest basis for such a value is a selector-style
reference to the options
element within which the comment
element resides.Another change has been to put the template:element
annotation inside the above fragment or region annotations. Had we not
done this, the lack of a comment
element in the form
data could have prevented the id
attribute from
appearing in the Web page, this preventing any hope of an in-page
update since there would be no way of knowing where such an update
should be applied.
Since we rely on JavaScript support in the browser, the following references to scripts must also be added to the template, as shown in the following excerpt:
<head>
<title>Example</title>
<script type="text/javascript" src="scripts/sarissa.js"> </script>
<script type="text/javascript" src="scripts/XSLForms.js"> </script>
</head>
These special script files can be found in examples/Common/VerySimple/Resources/scripts
.
Now we can concentrate on adding the event which triggers an in-page update. Since it is the type values that cause each comment field to be added or removed, we add an event attribute on the form field responsible for displaying the type values:
<p>
Item type:
<select template:multiple-choice-list-field="type,type-enum,value" name="..." multiple="multiple"
onchange="requestUpdate(
'comments',
'{template:list-attribute('type-enum', 'value')}',
'{template:other-elements(../options)}',
'{template:child-attribute('value', template:child-element('comment', 1, template:other-elements(../options)))}',
'/structure/item')">
<option template:multiple-choice-list-value="type-enum,value,selected" value="..." />
</select>
</p>
This complicated string calls a special update request JavaScript function which triggers the in-page update, and it specifies the following things:
http://localhost:8080/commentsSo the request for an in-page update will be sent to this generated URL.
type
element and its type-enum
elements' value
attributes, we specify the names of
the fields which yield these values.options
element holding comment
element. Thus, we use a
special value which also refers to that element from the context of
the type
element.options
element from the type
element and stating that we want
the value
attribute on any comment
element that may exist. Note that we cannot reference the comment
element directly since it may not exist at first, but then come into
being after an update, but not be referenced here in this parameter;
therefore, we need to make up the final part of the reference using the
special template:child-attribute
and template:child-element
functions.Of course, all this is pretty complicated and at some point in the future, a simplified way of triggering in-page updates will be introduced.
To support both normal requests for Web pages and the special in-page requests, we must make some modifications to the Web application. First, we must introduce some infrastructure to handle the requests for the JavaScript files separately from the requests for pages from our application. Some standard WebStack resources can be used to help with this, and we add some imports at the top of our source file:
#!/usr/bin/env python
"A very simple example application."
import WebStack.Generic
import XSLForms.Resources.WebResources
import XSLForms.Utils
import os
# Site map imports.
from WebStack.Resources.ResourceMap import MapResource
from WebStack.Resources.Static import DirectoryResource
Then, we define the resource class as before, but with an additional attribute:
# Resource classes.
class VerySimpleResource(XSLForms.Resources.WebResources.XSLFormsResource):
"A very simple resource providing a hierarchy of editable fields."
resource_dir = os.path.join(os.path.split(__file__)[0], "Resources")
encoding = "utf-8"
template_resources = {
"structure" : ("structure_multivalue_template.xhtml", "structure_output.xsl")
}
init_resources = {
"structure" : ("structure_multivalue_template.xhtml", "structure_input.xsl")
}
transform_resources = {
"comments" : ["structure_comments.xsl"]
}
document_resources = {
"types" : "structure_types.xml"
}
in_page_resources = {
"comments" : ("structure", "structure_output_comments.xsl", "comment-node")
}
This new attribute provides information about the in-page request to retrieve comment regions of the Web form, and it consists of the stylesheet filename that will be generated to produce the page fragments for such comment regions, along with the region marker that we defined above.
The respond_to_form
method now also includes some
additional code:
def respond_to_form(self, trans, form):
"""
Respond to a request having the given transaction 'trans' and the given
'form' information.
"""
in_page_resource = self.get_in_page_resource(trans)
parameters = form.get_parameters()
documents = form.get_documents()
Here, we find out whether an in-page update is requested, along with the raw parameters of the request, some of which will be used later on in the method.
The discovery of the form data structure and the addition and removal of elements happens as before, as does the merging of type values and the comment field, if applicable:
# Ensure the presence of a document.
if documents.has_key("structure"):
structure = documents["structure"]
else:
structure = form.new_instance("structure")
# Add and remove elements according to the selectors found.
selectors = form.get_selectors()
XSLForms.Utils.remove_elements(selectors.get("remove2"))
XSLForms.Utils.add_elements(selectors.get("add2"), "subitem")
XSLForms.Utils.remove_elements(selectors.get("remove"))
XSLForms.Utils.add_elements(selectors.get("add"), "item")
# Initialise the document, adding enumerations/ranges.
structure_xsl = self.prepare_initialiser("structure")
types_xml = self.prepare_document("types")
structure = self.get_result([structure_xsl], structure, references={"type" : types_xml})
# Add the comments.
comments_xsl_list = self.prepare_transform("comments")
structure = self.get_result(comments_xsl_list, structure)
The significant changes begin when presenting the result of the request processing:
# Start the response.
trans.set_content_type(WebStack.Generic.ContentType("application/xhtml+xml", self.encoding))
# Ensure that an output stylesheet exists.
if in_page_resource in self.in_page_resources.keys():
trans_xsl = self.prepare_fragment(in_page_resource)
stylesheet_parameters = self.prepare_parameters(parameters)
else:
trans_xsl = self.prepare_output("structure")
stylesheet_parameters = {}
Instead of just obtaining a stylesheet for the structure
document, we instead check to see if an in-page update is being
requested and, if so, prepare the stylesheet representing the fragment
of the Web form to be presented. Additionally, we obtain special
stylesheet parameters using the raw request parameters; this introduces
information that will be used to control the
stylesheet when making the final Web page output.
Finally, we send the output to the user but employing the additional stylesheet parameters to configure the result:
# Complete the response.
self.send_output(trans, [trans_xsl], structure, stylesheet_parameters)
In order to introduce the infrastructure mentioned above which
separates requests for Web pages from requests for JavaScript files, we
need to provide a more sophisticated implementation of the get_site
function:
# Site map initialisation.
def get_site():
"Return a simple Web site resource."
# Get the main resource and the directory used by the application.
very_simple_resource = VerySimpleResource()
directory = very_simple_resource.resource_dir
# Make a simple Web site.
resource = MapResource({
# Static resources:
"scripts" : DirectoryResource(os.path.join(directory, "scripts"), {"js" : "text/javascript"}),
# Main page and in-page resources:
None : very_simple_resource
})
return resource
What this does is to create a resource for the application, as before, but then to place the resource into a special WebStack resource which examines the path or URL on the incoming requests and directs such requests according to the following scheme:
scripts
in its URL, we employ the WebStack DirectoryResource
to
send the file from the scripts
subdirectory of the
application's Resources
directory.Thus, when the user's browser asks for a script file, it gets a script file; otherwise it gets a Web page showing either all of the form (if a normal request is received), or a part of the form (if an in-page request is received).