XSLTools

Annotated docs/in-page-updates.html

687:ae7bdbf61e32
2009-06-22 Paul Boddie Added some error handling to the questionnaire application.
paulb@615 1
<?xml version="1.0" encoding="iso-8859-1"?>
paulb@176 2
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
paulb@270 3
<html xmlns="http://www.w3.org/1999/xhtml"><head>
paulb@176 4
  <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type" />
paulb@615 5
  <title>Creating Applications: In-Page Updates</title>
paulb@270 6
  <link href="styles.css" rel="stylesheet" type="text/css" /></head>
paulb@176 7
<body>
paulb@176 8
<h1>Creating Applications: In-Page Updates</h1>
paulb@176 9
<p>One fashionable avenue in Web application design has been that of
paulb@176 10
updating Web pages in applications without having to refresh the entire
paulb@176 11
page every time an action is performed. Together with some JavaScript
paulb@176 12
support in the browser, XSLForms also provides some functionality for
paulb@176 13
such "in-page" or "live" updates.</p>
paulb@176 14
<p>Consider the addition of a comment field to our application. Here is
paulb@176 15
how the HTML code might look:</p>
paulb@270 16
<pre>&lt;div template:element="item"&gt;<br />  &lt;p&gt;<br />    Some item: &lt;input template:attribute-field="value" name="..." type="text" value="..." /&gt;<br />    &lt;input name="..." template:selector-field="remove" type="submit" value="Remove" /&gt;<br />  &lt;/p&gt;<br />  &lt;p&gt;<br />    Item type:<br />    &lt;select template:multiple-choice-list-field="type,type-enum,value" name="..." multiple="multiple"&gt;<br />      &lt;option template:multiple-choice-list-value="type-enum,value,selected" value="..." /&gt;<br />    &lt;/select&gt;<br />  &lt;/p&gt;<br />  <span style="font-weight: bold;">&lt;p template:element="options"&gt;</span><br style="font-weight: bold;" /><span style="font-weight: bold;">    &lt;span </span><span style="font-weight: bold;">template:element="comment"&gt;</span><span style="font-weight: bold;">Comment:</span><br style="font-weight: bold;" /><span style="font-weight: bold;">      &lt;textarea template:attribute-area="value,insert" name="..." cols="40" rows="3"&gt;</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        Some comment</span><br style="font-weight: bold;" /><span style="font-weight: bold;">      &lt;/textarea&gt;<br />    &lt;/span&gt;<br style="font-weight: bold;" /></span><span style="font-weight: bold;">  &lt;/p&gt;</span><br />  &lt;p&gt;<br />    Itself containing more items:<br />  &lt;/p&gt;<br />  &lt;p template:element="subitem"&gt;<br />    Sub-item: &lt;input template:attribute-field="subvalue" name="..." type="text" value="..." /&gt;<br />    &lt;input name="..." template:selector-field="remove2" type="submit" value="Remove" /&gt;<br />  &lt;/p&gt;<br />  &lt;p&gt;<br />    &lt;input name="..." template:selector-field="add2,subitem" type="submit" value="Add subitem" /&gt;<br />  &lt;/p&gt;<br />&lt;/div&gt;</pre>
paulb@615 17
<p>Here, a <code>textarea</code> element has been added with a special <code>template:attribute-area</code> annotation being used to state that the contents of the element are to be mapped to the <code>value</code> attribute, and that the attribute contents are to be inserted inside the <code>textarea</code> element (replacing the <code>Some Comment</code> text).</p><p>The newly-added <code>textarea</code> field might actually be
paulb@615 18
presented in the application in its current state, despite the lack of any <code>options</code> or <code>comment</code>
paulb@270 19
elements
paulb@176 20
manipulated by the
paulb@270 21
application, due to the document initialisation mechanism employed by
paulb@270 22
the application. However, what would be more interesting is the
paulb@270 23
possibility of only showing the comment field if something else in the
paulb@270 24
document had a certain value or state.</p>
paulb@270 25
<p>Let us imagine that if the type of an item was set to "Personal",
paulb@270 26
the comment field would appear and permit the recording of some text
paulb@615 27
for that item. One approach that would make this possible is to
paulb@270 28
add a transformation which checks the type values set for each of the
paulb@615 29
items and removes the <code>options</code> and <code>comment</code> elements for items which do not qualify. In the Web resource, we make the following
paulb@270 30
changes:</p>
paulb@270 31
<pre>    transform_resources = {<br />        "comments" : ["structure_comments.xsl"]<br />        }</pre>
paulb@270 32
<p>What this does is to state that when we carry out the <code>comments</code>
paulb@270 33
transformation, the specified stylesheet is employed, filtering out the
paulb@270 34
comments for non-qualifying items and preserving them for qualifying
paulb@270 35
items.</p><p>Further down in the code, we add a transformation:</p><pre>        # After the document initialisation...<br /><br />        # Add the comments.<br /><br />        comments_xsl_list = self.prepare_transform("comments")<br />        structure = self.get_result(comments_xsl_list, structure)</pre>
paulb@176 36
<p>This new stylesheet works according to the following principles:</p>
paulb@176 37
<ol>
paulb@176 38
  <li>Descend into the form data structure, copying all elements,
paulb@176 39
attributes and text that the stylesheet is not programmed to recognise.</li>
paulb@615 40
  <li>When encountering an <code>item</code> element (which the
paulb@176 41
stylesheet is programmed to recognise), do the following:<br />
paulb@176 42
    <ol>
paulb@615 43
      <li>Copy the element "skeleton" and its attributes so that
paulb@615 44
the <code>value</code> attribute is retained.</li>
paulb@176 45
      <li>Produce a new <code>options</code> element and process it.</li>
paulb@176 46
    </ol>
paulb@176 47
  </li>
paulb@176 48
  <li>When processing a new <code>options</code> element, do the
paulb@176 49
following:<br />
paulb@176 50
    <ol>
paulb@176 51
      <li>Inside this new <code>options</code> element, investigate
paulb@615 52
the values associated with the <code>type</code> element.</li>
paulb@444 53
      <li>If any of the selected type values represents the "Personal" category, make a new <code>comment</code>
paulb@615 54
element, then add any attributes that may be found on
paulb@179 55
existing <code>comment</code> elements within the current <code>type</code>
paulb@176 56
element.</li>
paulb@176 57
    </ol>
paulb@176 58
  </li>
paulb@176 59
</ol>
paulb@270 60
<p>Since this stylesheet is used after the document initialisation,
paulb@270 61
we may (and even must) take advantage of the results of that activity, including noting that selected values on <code>type-enum</code>
paulb@176 62
elements are marked with the <code>value-is-set</code> attribute.</p>
paulb@615 63
<p>The stylesheet source code can be found in <code>examples/Common/VerySimple/Resources/structure_comments.xsl</code>.</p>
paulb@176 64
<h2>Limitations and Enhancements</h2>
paulb@176 65
<p>Whilst the above modifications adds a comment field for each item
paulb@176 66
with a type of "Personal", and whilst the comment field will appear and
paulb@176 67
disappear for items as their type changes, such updates only take place
paulb@176 68
when items and subitems are added and removed. We could add an update
paulb@176 69
button to the page which performs an explicit refresh of the page
paulb@176 70
without adding or removing anything, and for the sake of usability, we
paulb@615 71
probably should add such a button (just below the <code>Add item</code>
paulb@176 72
button):</p>
paulb@176 73
<pre>&lt;p&gt;<br />  &lt;input name="update" type="submit" value="Update" /&gt;<br />&lt;/p&gt;</pre>
paulb@176 74
<p>However, we could also add an in-page update to make each comments
paulb@176 75
field appear and disappear as soon as we have changed the type of an
paulb@176 76
item.</p>
paulb@176 77
<h3>Template Changes</h3>
paulb@176 78
<p>We must first define a region of the template where a comment fields
paulb@176 79
can be added and removed, regardless of whether such a field existed
paulb@176 80
there before. The above template code needs modifying slightly to
paulb@176 81
permit this:</p>
paulb@624 82
<pre>  <span style="font-weight: bold;">&lt;p template:element="options" template:section="comment-node" id="{template:this-element()}"&gt;</span><br style="font-weight: bold;" /><span style="font-weight: bold;">    &lt;span template:element="comment"&gt;</span>Comment:<br />      &lt;textarea template:attribute-area="value,insert" name="..." cols="40" rows="3"&gt;<br />        Some comment<br />      &lt;/textarea&gt;<br />    <span style="font-weight: bold;">&lt;/span&gt;</span><br />  &lt;/p&gt;</pre>
paulb@176 83
<p>Here, we have added this region definition to the paragraph
paulb@176 84
surrounding the comment field, annotating the paragraph with the
paulb@176 85
following attributes:</p>
paulb@176 86
<ul>
paulb@624 87
  <li>The <code>template:section</code> attribute is used to define a
paulb@176 88
template fragment used only to prepare the updated part of the Web
paulb@176 89
page. Here we define the fragment or region as being just this
paulb@176 90
paragraph.</li>
paulb@615 91
  <li>The standard HTML <code>id</code> attribute is used to
paulb@176 92
define which part of the active Web page will be replaced when
paulb@176 93
performing an in-page update. This attribute needs to have a unique
paulb@176 94
value, but the easiest basis for such a value is a selector-style
paulb@615 95
reference to the <code>options</code> element within which the <code>comment</code>
paulb@176 96
element resides.</li>
paulb@176 97
</ul>
paulb@615 98
<p>Another change has been to put the <code>template:element</code>
paulb@176 99
annotation inside the above fragment or region annotations. Had we not
paulb@615 100
done this, the lack of a <code>comment</code> element in the form
paulb@615 101
data could have prevented the <code>id</code> attribute from
paulb@176 102
appearing in the Web page, this preventing any hope of an in-page
paulb@176 103
update since there would be no way of knowing where such an update
paulb@176 104
should be applied.</p>
paulb@176 105
<h3>Adding JavaScript</h3>
paulb@176 106
<p>Since we rely on JavaScript support in the browser, the following
paulb@176 107
references to scripts must also be added to the template, as shown in
paulb@176 108
the following excerpt:</p>
paulb@270 109
<pre>&lt;head&gt;<br />  &lt;title&gt;Example&lt;/title&gt;<br />  <span style="font-weight: bold;">&lt;script type="text/javascript" src="scripts/sarissa.js"&gt; &lt;/script&gt;</span><br style="font-weight: bold;" /><span style="font-weight: bold;">  &lt;script type="text/javascript" src="scripts/XSLForms.js"&gt; &lt;/script&gt;</span><br />&lt;/head&gt;</pre>
paulb@615 110
<p>These special script files can be found in <code>examples/Common/VerySimple/Resources/scripts</code>.</p>
paulb@176 111
<p>Now we can concentrate on adding the event which triggers an in-page
paulb@176 112
update. Since it is the type values that cause each comment field to be
paulb@176 113
added or removed, we add an event attribute on the form field
paulb@176 114
responsible for displaying the type values:</p>
paulb@482 115
<pre>  &lt;p&gt;<br />    Item type:<br />    &lt;select template:multiple-choice-list-field="type,type-enum,value" name="..." multiple="multiple"<br />      <span style="font-weight: bold;">onchange="requestUpdate(</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        'comments',</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        '{template:list-attribute('type-enum', 'value')}',</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        '{template:other-elements(../options)}',</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        '{template:child-attribute('value', template:child-element('comment', 1, template:other-elements(../options)))}',</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        '/structure/item')"</span>&gt;<br />      &lt;option template:multiple-choice-list-value="type-enum,value,selected" value="..." /&gt;<br />    &lt;/select&gt;<br />  &lt;/p&gt;</pre>
paulb@176 116
<p>This complicated string calls a special update request JavaScript
paulb@176 117
function which triggers the in-page update, and it specifies the
paulb@176 118
following things:</p>
paulb@180 119
<dl>
paulb@183 120
  <dt><span style="font-weight: bold;">'comments'</span></dt>
paulb@180 121
  <dd>The URL which will serve the in-page update requested by this
paulb@183 122
field. Since the value stated is a relative reference to a resource, it
paulb@183 123
will produce something like the following:<br />
paulb@180 124
    <pre>http://localhost:8080/comments</pre>
paulb@180 125
So the request for an in-page update will be sent to this
paulb@180 126
generated URL.</dd>
paulb@180 127
  <dt><span style="font-weight: bold;">'{template:list-attribute('type-enum',
paulb@180 128
'value')}'</span></dt>
paulb@180 129
  <dd>The fields which are going to be used in the processing of the
paulb@176 130
update. Since the presence of the comment field depends on a
paulb@615 131
specific <code>type</code> element and its <code>type-enum</code>
paulb@615 132
elements' <code>value</code> attributes, we specify the names of
paulb@180 133
the fields which yield these values.</dd>
paulb@180 134
  <dt><span style="font-weight: bold;">'{template:other-elements(../options)}'</span></dt>
paulb@180 135
  <dd>The region which is to be updated. Here, we recall that we
paulb@176 136
defined the region using a special reference to the <code>options</code>
paulb@615 137
element holding <code>comment</code> element. Thus, we use a
paulb@176 138
special value which also refers to that element from the context of
paulb@615 139
the <code>type</code> element.</dd>
paulb@180 140
  <dt><span style="font-weight: bold;">'{template:child-attribute('value',
paulb@180 141
template:child-element('comment', 1,
paulb@180 142
template:other-elements(../options)))}'</span></dt>
paulb@180 143
  <dd>Even when the types are changed, it may be the case that an
paulb@176 144
exposed comment field does not disappear (for example, if we already
paulb@176 145
have "Personal" selected but select "Important" in addition), and so we
paulb@176 146
need to provide the details of the field which holds the value of the
paulb@179 147
comment text. We find such details by referencing the <code>options</code>
paulb@179 148
element from the <code>type</code> element and stating that we want
paulb@179 149
the <code>value</code> attribute on any <code>comment</code>
paulb@179 150
element that may exist. Note that we cannot reference the <code>comment</code>
paulb@179 151
element directly since it may not exist at first, but then come into
paulb@179 152
being after an update, but not be referenced here in this parameter;
paulb@179 153
therefore, we need to make up the final part of the reference using the
paulb@179 154
special <code>template:child-attribute</code>
paulb@180 155
and <code>template:child-element</code> functions.</dd>
paulb@482 156
  <dt><span style="font-weight: bold;">'/structure/item'</span></dt>
paulb@180 157
  <dd>Finally, we need to provide some context to the application to
paulb@176 158
tell it something about where in the complete form data structure the
paulb@180 159
updated information resides.</dd>
paulb@180 160
</dl>
paulb@180 161
<p>Of course, all this is pretty complicated and at some point in the
paulb@180 162
future, a simplified way of triggering in-page updates will be
paulb@180 163
introduced.</p>
paulb@180 164
<h3>Updating the Web Application</h3>
paulb@180 165
<p>To support both normal requests for Web pages and the special
paulb@180 166
in-page requests, we must make some modifications to the Web
paulb@180 167
application. First, we must introduce some infrastructure to handle the
paulb@180 168
requests for the JavaScript files separately from the requests for
paulb@180 169
pages from our application. Some standard WebStack resources can be
paulb@180 170
used to help with this, and we add some imports at the top of our
paulb@180 171
source file:</p>
paulb@353 172
<pre>#!/usr/bin/env python<br /><br />"A very simple example application."<br /><br />import WebStack.Generic<br />import XSLForms.Resources.WebResources<br />import XSLForms.Utils<br />import os<br /><br /><span style="font-weight: bold;"># Site map imports.</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">from WebStack.Resources.ResourceMap import MapResource</span><br style="font-weight: bold;" /><span style="font-weight: bold;">from WebStack.Resources.Static import DirectoryResource</span></pre>
paulb@180 173
<p>Then, we define the resource class as <a href="Web-resource.html">before</a>,
paulb@180 174
but with an additional attribute:</p>
paulb@510 175
<pre># Resource classes.<br /><br />class VerySimpleResource(XSLForms.Resources.WebResources.XSLFormsResource):<br /><br />    "A very simple resource providing a hierarchy of editable fields."<br /><br />    resource_dir = os.path.join(os.path.split(__file__)[0], "Resources")<br />    encoding = "utf-8"<br />    template_resources = {<br />        "structure" : ("structure_multivalue_template.xhtml", "structure_output.xsl")<br />        }<br />    init_resources = {<br />        "structure" : ("structure_multivalue_template.xhtml", "structure_input.xsl")<br />        }<br />    transform_resources = {<br />        "comments" : ["structure_comments.xsl"]<br />        }<br />    document_resources = {<br />        "types" : "structure_types.xml"<br />        }<br /><span style="font-weight: bold;">    in_page_resources = {</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        "comments" : (</span><span style="font-weight: bold;">"structure", </span><span style="font-weight: bold;">"structure_output_comments.xsl", "comment-node")</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        }</span></pre>
paulb@180 176
<p>This new attribute provides information about the in-page request to
paulb@180 177
retrieve comment regions of the Web form, and it consists of the
paulb@180 178
stylesheet filename that will be generated to produce the page
paulb@180 179
fragments for such comment regions, along with the region marker that
paulb@180 180
we defined above.</p>
paulb@615 181
<p>The <code>respond_to_form</code> method now also includes some
paulb@180 182
additional code:</p>
paulb@270 183
<pre>    def respond_to_form(self, trans, form):<br /><br />        """<br />        Respond to a request having the given transaction 'trans' and the given<br />        'form' information.<br />        """<br /><br />        <span style="font-weight: bold;">in_page_resource = self.get_in_page_resource(trans)</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        parameters = form.get_parameters()</span><br />        documents = form.get_documents()<br /></pre>
paulb@180 184
<p>Here, we find out whether an in-page update is requested, along with
paulb@180 185
the raw parameters of the request, some of which will be used later on
paulb@180 186
in the method.</p>
paulb@180 187
<p>The discovery of the form data structure and the addition and
paulb@180 188
removal of elements happens as before, as does the merging of type
paulb@180 189
values and the comment field, if applicable:</p>
paulb@270 190
<pre>        # Ensure the presence of a document.<br /><br />        if documents.has_key("structure"):<br />            structure = documents["structure"]<br />        else:<br />            structure = form.new_instance("structure")<br /><br />        # Add and remove elements according to the selectors found.<br /><br />        selectors = form.get_selectors()<br />        XSLForms.Utils.remove_elements(selectors.get("remove2"))<br />        XSLForms.Utils.add_elements(selectors.get("add2"), "subitem")<br />        XSLForms.Utils.remove_elements(selectors.get("remove"))<br />        XSLForms.Utils.add_elements(selectors.get("add"), "item")<br /><br />        # Initialise the document, adding enumerations/ranges.<br /><br />        structure_xsl = self.prepare_initialiser("structure")<br />        types_xml = self.prepare_document("types")<br />        structure = self.get_result([structure_xsl], structure, references={"type" : types_xml})<br /><br />        # Add the comments.<br /><br />        comments_xsl_list = self.prepare_transform("comments")<br />        structure = self.get_result(comments_xsl_list, structure)<br /></pre>
paulb@180 191
<p>The significant changes begin when presenting the result of the
paulb@180 192
request processing:</p>
paulb@510 193
<pre>        # Start the response.<br /><br />        trans.set_content_type(WebStack.Generic.ContentType("application/xhtml+xml", self.encoding))<br /><br /><span style="font-weight: bold;"></span><span style="font-weight: bold;">        # Ensure that an output stylesheet exists.</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">        if in_page_resource in self.in_page_resources.keys():</span><br style="font-weight: bold;" /><span style="font-weight: bold;">            trans_xsl = self.prepare_fragment(in_page_resource)</span><span style="font-weight: bold;"></span><br style="font-weight: bold;" /><span style="font-weight: bold;">            stylesheet_parameters = self.prepare_parameters(parameters)</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        else:</span><br style="font-weight: bold;" /><span style="font-weight: bold;">            trans_xsl = self.prepare_output("structure")<br /></span>    <span style="font-weight: bold;"></span><span style="font-weight: bold;">        stylesheet_parameters = {}</span></pre>
paulb@615 194
<p>Instead of just obtaining a stylesheet for the <code>structure</code>
paulb@180 195
document, we instead check to see if an in-page update is being
paulb@180 196
requested and, if so, prepare the stylesheet representing the fragment
paulb@444 197
of the Web form to be presented. Additionally, we obtain special
paulb@444 198
stylesheet parameters using the raw request parameters; this introduces
paulb@444 199
information that will be used to control the
paulb@180 200
stylesheet when making the final Web page output.</p>
paulb@183 201
<p>Finally, we send the output to the user but employing the additional
paulb@183 202
stylesheet parameters to configure the result:</p>
paulb@270 203
<pre><span style="font-weight: bold;">        # Complete the response.<br /><br />        self.send_output(trans, [trans_xsl], structure<span style="font-weight: bold;">, stylesheet_parameters</span>)</span></pre>
paulb@180 204
<p>In order to introduce the infrastructure mentioned above which
paulb@180 205
separates requests for Web pages from requests for JavaScript files, we
paulb@615 206
need to provide a more sophisticated implementation of the <code>get_site</code>
paulb@180 207
function:</p>
paulb@270 208
<pre><span style="font-weight: bold;"># Site map initialisation.</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">def get_site():</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    "Return a simple Web site resource."</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    # Get the main resource and the directory used by the application.</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    very_simple_resource = VerySimpleResource()</span><br style="font-weight: bold;" /><span style="font-weight: bold;">    directory = very_simple_resource.resource_dir</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    # Make a simple Web site.</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    resource = MapResource({</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        # Static resources:</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        "scripts" : DirectoryResource(os.path.join(directory, "scripts"), {"js" : "text/javascript"}),</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        # Main page and in-page resources:</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        None : very_simple_resource</span><br style="font-weight: bold;" /><span style="font-weight: bold;">        })</span><br style="font-weight: bold;" /><br style="font-weight: bold;" /><span style="font-weight: bold;">    return resource</span></pre>
paulb@180 209
<p>What this does is to create a resource for the application, as
paulb@180 210
before, but then to place the resource into a special WebStack resource
paulb@180 211
which examines the path or URL on the incoming requests and directs
paulb@180 212
such requests according to the following scheme:</p>
paulb@180 213
<ul>
paulb@615 214
  <li>If the request mentions something under <code>scripts</code>
paulb@180 215
in its URL, we employ the WebStack <code>DirectoryResource</code> to
paulb@615 216
send the file from the <code>scripts</code> subdirectory of the
paulb@180 217
application's <code>Resources</code> directory.</li>
paulb@180 218
  <li>Otherwise, we pass the request on to our application resource in
paulb@180 219
order to produce a Web page for the user.</li>
paulb@180 220
</ul>
paulb@180 221
<p>Thus, when the user's browser asks for a script file, it gets a
paulb@180 222
script file; otherwise it gets a Web page showing either all of the
paulb@180 223
form (if a normal request is received), or a part of the form (if an
paulb@180 224
in-page request is received).</p>
paulb@615 225
</body></html>