Search

Paging mechanism using XML+XSLT

1 views

Why a Pure Client‑Side Paging System Is Worth Exploring

In most web applications the burden of displaying a long list of items falls on the server. PHP, JSP or ASP code runs on the backend, slices the data set, and sends only the relevant rows to the browser. This pattern works well, but it forces a round‑trip to the server every time a user changes the page. If the data set is huge, the server must load it, filter it, and render it into HTML before any response reaches the client. Even when the backend is fast, the network round‑trip can become a bottleneck for users on slow connections.

What if the server could hand the entire data set to the browser once, and let the browser itself decide which rows to show next? Modern browsers are powerful, and XSLT engines built into them can transform XML into HTML on the fly. By moving the paging logic to the client, you eliminate the server round‑trip and reduce the server load. The trade‑off is that the browser has to download the entire XML file, which may not be ideal for very large data sets. However, for moderately sized lists - say a few thousand items - the benefit of instant navigation often outweighs the cost of a larger initial payload.

In this article we walk through a complete example that shows how to implement such a client‑side paging system using only XML, XSLT, and JavaScript. No server‑side scripting is required. The example demonstrates three key ideas:

  • How to create a lightweight “controller” XML that passes paging parameters to an XSLT.
  • How to use the XSLT document() function and XPath predicates to pull only the items that belong on the current page.
  • How to build and load that controller XML dynamically from JavaScript, driven by the URL query string.

    By the end you’ll have a reusable pattern you can drop into any project that needs quick, server‑free pagination. The code snippets below use the exact files you can download from the author’s website, but you can adapt the approach to your own data model.

    Preparing the Item List in XML

    The data that will be paged is stored in a simple XML document named mylist.xml. Its structure is intentionally minimal: a root element called <list> that contains a sequence of <item> elements. Each item has two child elements, element1 and element2, which hold the actual information that will be displayed on the page. The following excerpt shows the first ten items; the real file contains many more in the same pattern.

    Prompt
    <?xml version="1.0"?></p> <p><list></p> <p> <item></p> <p> <element1>item 1 - element 1</element1></p> <p> <element2>item 1 - element 2</element2></p> <p> </item></p> <p> <item></p> <p> <element1>item 2 - element 1</element1></p> <p> <element2>item 2 - element 2</element2></p> <p> </item></p> <p> <item></p> <p> <element1>item 3 - element 1</element1></p> <p> <element2>item 3 - element 2</element2></p> <p> </item></p> <p> …</p> <p></list></p>

    Each item is a self‑contained record, so the XSLT can operate on a slice of the list without caring about the rest. The file is small enough to be downloaded in a single HTTP request, yet it can grow to thousands of records if needed. When the browser loads this file, it remains in memory, ready for the XSLT to pick whatever part of it is required.

    One important detail is that the XML file does not contain any paging metadata. That data lives elsewhere - in the controller XML we introduce next. Keeping the two files separate means the list can be reused with different paging settings, and the list itself can stay static while the controller changes.

    Designing a Tiny Controller XML

    The controller XML acts as a parameter container for the XSLT. Think of it as a mini configuration file that tells the stylesheet which subset of mylist.xml to render. It is intentionally simple: a single <xml-doc-name> element whose text value is the name of the source XML file, and two attributes - start and limit - that control paging.

    <xml-controller>

    <xml-doc-name start="1" limit="3">mylist.xml</xml-doc-name>

    </xml-controller>

    In this example, the controller requests three items, beginning with the first one. The start attribute uses a one‑based index that matches the XPath position() function. The limit attribute specifies how many items to show per page.

    When the user clicks “Next” or “Prev”, the JavaScript code will generate a new controller XML on the fly, updating the start value accordingly. This new controller is then fed to the XSLT engine, which applies the same transformation logic but with different parameters. The beauty of this pattern is that the same XSLT file can handle all paging states without modification.

    Because the controller XML is generated entirely in the browser, it stays lightweight. Even for a long list, the controller is only a few dozen bytes, so the overhead of transmitting it between the JavaScript and the XSLT engine is negligible.

    Writing the XSLT That Reads the Controller and Builds the Page

    The XSLT file is the heart of the paging logic. It performs four main tasks: it extracts the paging parameters from the controller, loads the source XML using document(), selects the correct slice of items with an XPath predicate, and renders the results into a simple HTML table with navigation links.

    Below is the complete mylist.xsl file. The stylesheet starts by matching the xml-controller/xml-doc-name element so it can read the attributes and text node that specify the source file and paging indices.

    Prompt
    <xsl:stylesheet version="1.0"</p> <p> xmlns:xsl="http://www.w3.org/1999/XSL/Transform"></p> <p> <xsl:output method="html" indent="no" omit-xml-declaration="yes"/></p> <p> <xsl:template match="xml-controller/xml-doc-name"></p> <p> <!-- Pull the paging parameters from the controller --></p> <p> <xsl:variable name="sourceDoc" select="." /></p> <p> <xsl:variable name="start" select="@start" /></p> <p> <xsl:variable name="limit" select="@limit" /></p> <p> <xsl:variable name="end" select="$start + $limit - 1" /></p> <p> <xsl:variable name="docName" select="." /></p> <p> <!-- Load the full XML list into the variable $list --></p> <p> <xsl:variable name="list"</p> <p> select="document($docName)/list/item[position() >= $start and position() <= $end]" /></p> <p> <!-- Generate the HTML table header --></p> <p> <html><body></p> <p> <table border="1" cellpadding="2" cellspacing="0"></p> <p> <tr bgcolor="#cccccc"></p> <p> <th>Element 1</th></p> <p> <th>Element 2</th></p> <p> </tr></p> <p> <!-- Render each item in the current page --></p> <p> <xsl:for-each select="$list"></p> <p> <tr></p> <p> <td><xsl:value-of select="element1" /></td></p> <p> <td><xsl:value-of select="element2" /></td></p> <p> </tr></p> <p> </xsl:for-each></p> <p> </table></p> <p> <!-- Navigation links – only if needed --></p> <p> <div style="margin-top:10px;"></p> <p> <xsl:if test="$start > 1"></p> <p> <a href="?page={($start - $limit)}">Prev</a></p> <p> <span style="margin:0 10px;">|</span></p> <p> </xsl:if></p> <p> <xsl:if test="$end < count(document($docName)/list/item)"></p> <p> <a href="?page={$end + 1}">Next</a></p> <p> </div></p> <p> </body></html></p> <p> </xsl:template></p> <p></xsl:stylesheet></p>

    The line that matters most for pagination is the XPath predicate inside the document() call:

    Prompt
    document($docName)/list/item[position() >= $start and position() <= $end]</p>

    Because XPath evaluates the predicate against the entire list, it returns only the subset that satisfies the index constraints. The rest of the items are ignored, so the browser does not have to process them further.

    Navigation links are built dynamically. The “Prev” link appears only when the current page is not the first one, and the “Next” link appears only when there are more items after the current slice. The links point back to the same HTML page but include a ?page= query parameter that tells the JavaScript which new start value to request. Notice that the XSLT itself does not need to know the total number of items; it relies on the XPath count() function to determine whether a “Next” link is appropriate.

    Coordinating Everything From the Browser with JavaScript

    Because the XSLT engine runs in the browser, a small JavaScript routine is responsible for wiring together the three moving parts: the URL, the controller XML, and the transformation engine. The JavaScript performs the following steps each time the page loads:

    1. Parse the current URL to extract the page query parameter, which represents the first item index to display.
    2. Construct a minimal controller XML that contains the source file name, the current start value, and the fixed limit (items per page).
    3. Load the controller XML and the XSLT file using an XMLHttpRequest or the legacy Microsoft.XMLDOM object.
    4. Instantiate a XSLTProcessor (or equivalent) to apply the XSLT to the controller XML.
    5. Inject the resulting HTML into the page’s document.body

      Below is the JavaScript code that accomplishes these tasks. It is written for Internet Explorer 7/8/9 using Microsoft.XMLDOM, but the logic can be ported to other browsers by swapping the XML loading code for XMLHttpRequest and using the standard XSLTProcessor

      Prompt
      // Get the base path for relative URLs</p> <p>var basePath = document.location.href.replace(/[^\/]+$/, '');</p> <p>// Parse the 'page' query string, default to 1</p> <p>var query = window.location.search.substring(1);</p> <p>var params = {};</p> <p>query.split('&').forEach(function(part) {</p> <p> var keyVal = part.split('=');</p> <p> if (keyVal[0]) {</p> <p> params[keyVal[0]] = decodeURIComponent(keyVal[1] || '');</p> <p> }</p> <p>});</p> <p>var startIndex = parseInt(params.page) || 1;</p> <p>// Items per page (same as in XSLT)</p> <p>var itemsPerPage = 3;</p> <p>// Build controller XML as a string</p> <p>var controllerXML = ' <p> '<xml-controller>' +</p> <p> '<xml-doc-name start="' + startIndex + '" limit="' + itemsPerPage + '">mylist.xml</xml-doc-name>' +</p> <p> '</xml-controller>';</p> <p>// Load XML and XSL</p> <p>var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");</p> <p>xmlDoc.async = false;</p> <p>xmlDoc.loadXML(controllerXML);</p> <p>var xslDoc = new ActiveXObject("Microsoft.XMLDOM");</p> <p>xslDoc.async = false;</p> <p>xslDoc.load(basePath + 'mylist.xsl');</p> <p>// Perform the transformation</p> <p>var processor = new ActiveXObject("Msxml2.FreeThreadedDOMDocument.6.0");</p> <p>processor.setProperty("SelectionLanguage", "XPath");</p> <p>processor.loadXML(controllerXML);</p> <p>processor.transformNodeToObject(xslDoc, document.body);</p>

      When the page first loads, startIndex will be 1, so the controller requests items 1‑3. The “Prev” link will not appear because startIndex > 1 is false. The “Next” link will point to ?page=4. Clicking that link reloads the page with the new query string, causing the JavaScript to rebuild the controller with start="4", and the XSLT will now render items 4‑6.

      Because every navigation step reuses the same transformation logic, the code remains simple and easy to maintain. Adding a new parameter - such as a search filter or sort order - requires only a small tweak to the controller XML generation, without touching the XSLT.

      Practical Tips and Gotchas When You Deploy This Approach

      While the client‑side paging system is elegant, several practical considerations deserve attention before using it in a production environment.

      • XML Size and Initial Load Time – The entire XML list is downloaded once, so for very large data sets the initial page load may become noticeable. In such cases, consider paging at the server level or implementing a hybrid approach where the server serves a smaller XML chunk on demand.
      • Browser Compatibility – The example uses the legacy Microsoft.XMLDOM object, which works only in older versions of Internet Explorer. Modern browsers support the standard XMLHttpRequest and XSLTProcessor. If you need to support both, write conditional code that detects the browser and selects the appropriate API.
      • Security Restrictions – Loading external XML files via document() may trigger the Same‑Origin Policy. Ensure that the XML file resides on the same domain as the HTML page, or configure the server to send the correct Access-Control-Allow-Origin header if cross‑domain requests are necessary.
      • Cache Strategy – Because the XML and XSL are static, you can take advantage of browser caching. Set appropriate Cache-Control headers so that subsequent visits load the files from cache, reducing bandwidth usage.
      • SEO Considerations – Search engines may not execute JavaScript or XSLT, so they might not see the paged content. If SEO is critical, consider server‑side rendering or providing static HTML snapshots.

        Despite these caveats, the approach offers a clean, code‑free solution for small to medium‑sized lists. It demonstrates how to pass arguments to XSLT via a tiny controller XML, how to slice a large XML dataset with XPath, and how to tie everything together using plain JavaScript - all without any server‑side scripting.

        Source files for this example can be downloaded from the author’s website: mylist.xml, mylist.xsl, and driver.html (works in Internet Explorer). Feel free to experiment with the code, tweak the item count, or extend it to include additional features such as sorting or filtering.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles