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="value" name="{template:this-attribute()}" type="text" value="{$this-value}" />
<input name="remove={template:this-element()}" type="submit" value="Remove" />
</p>
<p>
Item type:
<select template:element="type" name="{template:list-attribute('type-enum', 'value')}" multiple="multiple">
<option template:element="type-enum" template:expr="@value-is-set" template:expr-attr="selected"
template:value="@value" value="{@value}" />
</select>
</p>
<p template:element="options">
<span template:element="comment">Comment:
<textarea template:attribute="value" name="{template:this-attribute()}" cols="40" rows="3">
<span template:value="$this-value" template:effect="replace">Some comment</span>
</textarea>
</span>
</p>
<p>
Itself containing more items:
</p>
<p template:element="subitem">
Sub-item: <input template:attribute="subvalue" name="{template:this-attribute()}" type="text" value="{$this-value}" />
<input name="remove2={template:this-element()}" type="submit" value="Remove" />
</p>
<p>
<input name="add2={template:this-element()}" type="submit" value="Add subitem" />
</p>
</div>
The newly-added textarea
field will not be
presented in the application in its current state; this is due to the
lack of any options
or comment
elements
manipulated by the
application, and such template changes are actually quite safe to make.
So, we must now do some additional work to add such options
and comment
elements in our application.
One approach is to extend our transformation which adds the different type values so that these new elements are introduced as well. In the Web resource, we can make the following change:
transform_resources = {
"types" : ["structure_multivalue_types.xsl", "structure_comments.xsl"]
}
What this does is to state that when we carry out the types
transformation, two stylesheets are employed, one before the other,
such that the type values are first added using the first stylesheet
(and the additional reference document containing the type values) and
that the comments are then added using the second stylesheet.
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 type value transformation,
we may (and even must) take advantage of the results of that
transformation, 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="value" name="{template:this-attribute()}" cols="40" rows="3">
<span template:value="$this-value" template:effect="replace">Some comment</span>
</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:element="type" name="{template:list-attribute('type-enum', 'value')}" multiple="multiple"
onchange="requestUpdate(
'{$application-url}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/options')">
<option template:element="type-enum" template:expr="@value-is-set" template:expr-attr="selected"
template:value="@value" value="{@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:
application-url
which will need to be provided to the template when generating the Web
page. This will produce something like the following: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
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.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")
}
transform_resources = {
"types" : ["structure_multivalue_types.xsl", "structure_comments.xsl"]
}
document_resources = {
"types" : "structure_types.xml"
}
in_page_resources = {
"comments" : ("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")
# Transform, adding enumerations/ranges.
types_xsl_list = self.prepare_transform("types")
types_xml = self.prepare_document("types")
structure = self.get_result(types_xsl_list, structure, references={"types" : types_xml})
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))
# Define the stylesheet parameters.
stylesheet_parameters = {}
# Ensure that an output stylesheet exists.
if in_page_resource in self.in_page_resources.keys():
trans_xsl = self.prepare_fragment("structure", in_page_resource)
element_path = parameters.get("element-path", [""])[0]
stylesheet_parameters["element-path"] = element_path
else:
trans_xsl = self.prepare_output("structure")
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 a special element-path
parameter directly from the request parameters; this parameter is added
to a collection of parameters that will be used to control the
stylesheet when making the final Web page output.
Another parameter that will be used in stylesheet processing is
the application-url
parameter mentioned above. We
obtain the address and port of the Web server environment and add the
result as a simple URL to the application-url
stylesheet parameter. Finally, we send the output to the user but
employing the additional stylesheet parameters to configure the result:
# Add information essential for in-page requests.
if trans.get_server_port() == "80":
stylesheet_parameters["application-url"] = \
"http://%s%s" % (trans.get_server_name(), trans.get_path_without_query())
else:
stylesheet_parameters["application-url"] = \
"http://%s:%s%s" % (trans.get_server_name(), trans.get_server_port(), trans.get_path_without_query())
# 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).