Reusable XSL Stylesheets and Templates
By Tony Marston2005-04-20
Levels of Reusability
4.1 A library of standard templates
Using the techniques described above has allowed me to achieve a significant amount of reusability with my XSL code. By maintaining the contents of standard templates in separate files which can be referenced by the
<xsl:include>statement I have effectively created a library of standard subroutines.
4.2 A single "detail" stylesheet for multiple modes
Another area of reusability I have incorporated into my design revolves around the use of form families where a typical database table requires 6 forms to handle its maintenance - a LIST/BROWSE form, a SEARCH form, an INSERT form, an UPDATE form, an ENQUIRY form and a DELETE form. The LIST/BROWSE form deals with multiple rows displayed horizontally whereas all the others deal with a single row which is displayed vertically. Because this last set of forms all use the same layout I am able to satisfy them all with a single "detail" stylesheet. By passing a $mode parameter at runtime (containing the value "search", "insert" "update", "read" or "delete") I am able to determine at runtime whether the fields are to be amendable or read-only and therefore create the relevant HTML code.
4.3 Stylesheets do not specify HTML controls for fields
In an early version of my code when a stylesheet referred to a fieldname it had to specify which HTML control (eg. text box, dropdown list, radio group) was to be used for that field. This worked OK until I came across a situation where the same field needed to be handled in different ways depending on circumstances which were determined at runtime within my PHP code. In my first resolution of this problem I got my PHP code to specify the required control type as an attribute to the field in the XML file, and I changed my stylesheet to call the relevant template depending on this attribute. This worked OK, but when I looked at the code I realised that instead of getting my stylesheet to call the relevant template depending on the control attribute I could create an intermediate template which would do the job for me. Consequently all my stylesheets now call a standard datafield template for each field, and it is this template which checks the value in the control attribute and calls the next template which deals with that particular control type. I currently make use of the following attributes in the XML file for each field:
| size | Determines the size of the text box. |
| pkey | Identifies the field as part of the primary key. |
| noedit | Makes the field read-only. |
| nodisplay | Excludes the field from the HTML output |
| control | Identifies the control type. The default is "text", but can be "dropdown", "boolean", "radiogroup", "popup" or "multiline". |
| password | Does not echo each character as it is typed in. |
| required | Indicates that this is a required field. |
4.4 Stylesheets do not contain table or field names
Until recently each physical screen in my web application required its own separate stylesheet as this had hard-coded into it the names of the database table(s) and the field(s) which were to be included in the HTML output. Although it was a relatively simple process to take a copy of the basic stylesheet and customise it as required I began to wonder if it were possible to use a generic stylesheet and pass it the table and field names at runtime within the XML file. It did not take much experimentation to discover that this was definitely a workable proposition.
4.4.1 Screen structure in XML format
The first step was to include in my XML file the necessary elements to identify which tables and fields were to be processed, and in what order. Here is a sample of the extra information which appears in the XML file for detail screens:
<structure>This structure identifies the following:
<main id="person">
<columns>
<column width="150"/>
<column width="•"/>
</columns>
<fieldlist>
<field id="person_id" label="ID"/>
<field id="first_name" label="First Name"/>
<field id="last_name" label="Last Name"/>
<field id="initials" label="Initials"/>
<field id="nat_ins_no" label="Nat. Ins. No."/>
<field id="pers_type_id" label="Person Type"/>
<field id="star_sign" label="Star Sign"/>
<field id="email_addr" label="E-mail"/>
<field id="value1" label="Value 1"/>
<field id="value2" label="Value 2"/>
<field id="start_date" label="Start Date"/>
<field id="end_date" label="End Date"/>
<field id="selected" label="Selected"/>
</fieldlist>
</main>
</structure>
• It identifies the data for a single zone called "main".
• This zone is to be filled with a record from the "person" table.
• It will require 2 columns of the specified widths.
• The fieldlist identifies which fields are to be painted on the screen and in what order.
• The fieldlist also supplies the label for each field.
4.4.2 Screen structure in PHP format
This information is supplied to my application in the form of a PHP "include" file, as shown in the following sample. I have a standard PHP function which reads this in and adds the details to the current XML file.
<?php4.4.3 Process screen structure - verticallyIn detail screens the fields for a single record are displayed vertically, with the field label shown in front of each field value as shown in figure 2. The code to achieve this, which is used in the XSL file for a DETAIL screen, is explained below.
$structure['xsl_file'] = 'std.detail1.xsl';
$structure['tables']['main'] = 'person';
$structure['main']['columns'][] = array('width' => 150);
$structure['main']['columns'][] = array('width' => '•');
$structure['main']['fields'] = array('person_id' => 'ID',
'first_name' => 'First Name',
'last_name' => 'Last Name',
'initials' => 'Initials',
'nat_ins_no' => 'Nat. Ins. No.',
'pers_type_id' => 'Person Type',
'star_sign' => 'Star Sign',
'email_addr' => 'E-mail',
'value1' => 'Value 1',
'value2' => 'Value 2',
'start_date' => 'Start Date',
'end_date' => 'End Date',
'selected' => 'Selected');
?>
<xsl:variable name="main" select="//structure/main/@id"/>This line of code creates a variable called $main and selects the contents of the "id" attribute of the "main" node which is a child of the "structure" node. In this example the resulting value is "person".
<xsl:call-template name="column_group">This calls the column-group template with a parameter called "table" which has the value "main".
<xsl:with-param name="table" select="'main'"/>
</xsl:call-template>
<xsl:template name="column_group">This is the column-group template. The select portion of the
<xsl:param name="table"/>
<xsl:for-each select="//structure/•[name()=$table]/columns/column">
<colgroup>
<xsl:if test="@width">
<xsl:attribute name="width"><xsl:value-of select="@width"/></xsl:attribute>
</xsl:if>
<xsl:if test="@class">
<xsl:attribute name="class"><xsl:value-of select="@class"/></xsl:attribute>
</xsl:if>
</colgroup>
</xsl:for-each>
</xsl:template>
<xsl:for-each>statement is the method whereby a string is converted into a node set. In this example it is equivalent to "//structure/main/columns/column" and steps through all the designated "column" nodes in the XML document. For each node it will create an HTML "<colgroup>" tag with either a "width" attribute or a "class" attribute.
<xsl:for-each select="//•[name()=$main][1]">The select portion of the
<xsl:call-template name="display_vertical">
<xsl:with-param name="zone" select="'main'"/>
</xsl:call-template>
</xsl:for-each>
<xsl:for-each>statement in this example is equivalent to "//person[1]" and selects the first occurrence of the node with the name "person". It then calls the display_vertical template to process this node and passes it the zone name of "main" (remember that this same template may be used for other zones in other stylesheets).
<xsl:template name="display_vertical">This is the start of the display_vertical template. It is passed a single parameter called $zone
<xsl:param name="zone"/> <!-- could be 'main', 'inner', 'outer', etc -->
<xsl:variable name="table" select="name()"/> <!-- current table name -->It creates two variables. In this example $table will contain "person" and $position will contain "1".
<xsl:variable name="position" select="position()"/> <!-- current row within table -->
<xsl:for-each select="//structure/•[name()=$zone]/fieldlist/field">This line of code is equivalent to the XPath expression "//structure/main/fieldlist/field" and will loop through each field in the fieldlist of the main entity of the structure element. In this example it will start at "person_id" and finish at "selected".
<xsl:variable name="fieldname" select="@id" />The first line of code creates a variable called $fieldname using the "id" attribute of the current node. As this is inside a for-each loop it will start at "person_id" and finish at "selected".
<xsl:variable name="field"
select="//•[name()=$table][position()=$position]/•[name()=$fieldname]" />
The second line of code extracts the value for $fieldname. As it progresses through the for-each loop it will be equivalent to the XPath expression "//person[1]/person_id" all the way down to "//person[1]/selected".
<xsl:if test="$field">This line will ignore the current $field if it is not defined within the XML file.
<xsl:if test="not($field/@nodisplay)">This line will ignore the current $field if it is has the "nodisplay" attribute set.
<tr class="{$zone}">
<td>
<xsl:attribute name="class">
<xsl:value-of select="concat($zone,'label')"/>
</xsl:attribute>
<xsl:value-of select="@label"/>
</td>
<td>
<xsl:call-template name="datafield">
<xsl:with-param name="item" select="$field"/>
<xsl:with-param name="itemname" select="$fieldname"/>
<xsl:with-param name="path" select="$table"/>
<xsl:with-param name="position" select="$position"/>
</xsl:call-template>
</td>
</tr>This code will create a single row within an HTML table. The "<tr>" tag has its "class" attribute set to the value of $zone, which in this example is "main".
The first table cell has its "class" attribute set to "mainlabel", and its value is set to the "label" attribute of the current "field" node.
The second table cell is filled with the value for the current node. This is done by calling the datafield template and passing it parameters which contain the field value, the field name, the table name ("person"), and the table row ("1"). This will utilise the field attributes within the XML file to determine which HTML control to use for this field.
</xsl:if>The remaining lines simply close the existing XSL statements.
</xsl:if>
</xsl:for-each>
</xsl:template>
4.4.4 Process screen structure - horizontally
In list screens there are multiple occurrences which are displayed horizontally. The first row in the HTML table contains all the field labels in the form of column headings, and this is followed by rows of field values, one line for each occurrence in the XML file. An example of this is shown in figure 1. The code to achieve this, which is used in the XSL file for a LIST screen, is explained below.
<thead>This calls the column-headings template with a parameter called "table" which has the value "main".
<xsl:call-template name="column_headings">
<xsl:with-param name="table" select="'main'"/>
</xsl:call-template>
</thead>
<xsl:template name="column_headings">This is the column-headings template. The select portion of the <xsl:for-each> statement in this example is equivalent to "//structure/main/fieldlist/field" and steps through all the designated "field" nodes in the XML document. For each node it will call the column-hdg template.
<xsl:param name="table"/>
<tr>
<xsl:for-each select="//structure/•[name()=$table]/fieldlist/field">
<th>
<xsl:call-template name="column_hdg">
<xsl:with-param name="item" select="@id"/>
<xsl:with-param name="label" select="@label"/>
</xsl:call-template>
</th>
</xsl:for-each>
</tr>
</xsl:template>
<tbody>The select portion of the
<xsl:for-each select="//•[name()=$main][count(•)>0]">
<xsl:call-template name="display_horizontal">
<xsl:with-param name="zone" select="'main'"/>
</xsl:call-template>
</xsl:for-each>
</tbody>
<xsl:for-each>statement in this example is equivalent to "//person[count(•)>0]" and selects all non-empty occurrences of the node with the name "person". It then calls the display_horizontal template to process each node and passes it the zone name of "main" (remember that this same template may be used for other zones in other stylesheets).
<xsl:template name="display_horizontal">It creates two variables. In this example $table will contain "person" and $position will start at "1" and end at the last row number in the XML file.
<xsl:param name="zone"/> <!-- could be 'main', 'inner', 'outer', etc -->
This is the start of the display_horizontal template. It is passed a single parameter called $zone.
<xsl:variable name="table" select="name()"/> <!-- current table name -->
<xsl:variable name="position" select="position()"/> <!-- current row within table -->
<tr>This will set the "class" attribute of the "<tr>" tag to 'odd' or 'even' to determine the colour.
<xsl:attribute name="class">
<xsl:choose>
<xsl:when test="position()mod 2">odd</xsl:when>
<xsl:otherwise>even</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:for-each select="//structure/•[name()=$zone]/fieldlist/field">This line of code is equivalent to the XPath expression "//structure/main/fieldlist/field" and will loop through each field in the fieldlist of the main entity of the structure element.
<xsl:variable name="fieldname" select="@id" />The first line of code creates a variable called $fieldname using the "id" attribute of the current node. As this is inside a for-each loop the value will change for each iteration.
<xsl:variable name="field"
select="//•[name()=$table][position()=$position]/•[name()=$fieldname]" />
The second line of code extracts the value for the current field identified in $fieldname. Note that one loop controls the value for $position while another loop controls the value for $fieldname.
<td>This calls the datafield template and passes it parameters which contain the field value, the field name, the table name ("person"), and the table row (which starts at "1" and increments with each iteration). This will utilise the field attributes within the XML file to determine which HTML control to use for this field.
<xsl:call-template name="datafield">
<xsl:with-param name="item" select="$field"/>
<xsl:with-param name="itemname" select="$fieldname"/>
<xsl:with-param name="path" select="$table"/>
<xsl:with-param name="position" select="$position"/>
</xsl:call-template>
</td>
</xsl:for-each>
</tr>
</xsl:template>
Tutorial Pages:
» Introduction
» The structure of an XML file
» The structure of an XSL file
» Levels of Reusability
» Summary
