Creating Applications: In-Page Updates

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:

  1. Descend into the form data structure, copying all elements, attributes and text that the stylesheet is not programmed to recognise.
  2. When encountering an item element (which the stylesheet is programmed to recognise), do the following:
    1. Copy the element "skeleton" and its attributes so that the value attribute is retained.
    2. Produce a new options element and process it.
  3. When processing a new options element, do the following:
    1. Inside this new options element, investigate the values associated with the type element.
    2. If any of the selected type values represents the "Personal" category, make a new 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.

Limitations and Enhancements

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.

Template Changes

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:

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.

Adding JavaScript

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/options')">
<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:

'comments'
The URL which will serve the in-page update requested by this field. Since the value stated is a relative reference to a resource, it will produce something like the following:
http://localhost:8080/comments
So the request for an in-page update will be sent to this generated URL.
'{template:list-attribute('type-enum', 'value')}'
The fields which are going to be used in the processing of the update. Since the presence of the comment field depends on a specific type element and its type-enum elements' value attributes, we specify the names of the fields which yield these values.
'{template:other-elements(../options)}'
The region which is to be updated. Here, we recall that we defined the region using a special reference to the options element holding comment element. Thus, we use a special value which also refers to that element from the context of the type element.
'{template:child-attribute('value', template:child-element('comment', 1, template:other-elements(../options)))}'
Even when the types are changed, it may be the case that an exposed comment field does not disappear (for example, if we already have "Personal" selected but select "Important" in addition), and so we need to provide the details of the field which holds the value of the comment text. We find such details by referencing the 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.
'/structure/item/options'
Finally, we need to provide some context to the application to tell it something about where in the complete form data structure the updated information resides.

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.

Updating the Web Application

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_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("structure", 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:

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).