2

I'm relatively new to xslt and have been trying to create a table using an xml structure, but I am finding it difficult to limit the amount of fields in each row..

<report>
<status>
    <statuscheck>
        <node>node1</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area1</area>
    </statuscheck>
    <statuscheck>
        <node>node2</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area1</area>
    </statuscheck>
    <statuscheck>
        <node>node3</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area1</area>
    </statuscheck>
    <statuscheck>
        <node>node4</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area1</area>
    </statuscheck>
    <statuscheck>
        <node>node5</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area1</area>
    </statuscheck>
    <statuscheck>
        <node>node1</node>
        <RAG>red</RAG>
        <url>http://www.google.com</url>
        <area>area2</area>
    </statuscheck>
</status>
<area>
    <area_name>area1</area_name>
    <area_name>area2</area_name>
</area>

I have the following xslt but is there a way that I can get it to start a new row after every 4 items? the actual xml will have up to 20 components per area.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/">
        <html>
            <head>
                <style>
                </style>
            </head>

            <body style="font-family: Sky Text;">
            <xsl:for-each select="/report/area/area_name">
                            <div style="font-size: 20px; font-weight: bold; margin: 10px 0 10px 0;"><xsl:value-of select="."/></div>
                            <table style="font-family: Sky Text; border-collapse: collapse; width: 960px;">
                                <tbody>
                                    <xsl:variable name="active_area" select="./text()"></xsl:variable>
                                    <xsl:for-each select="/report/status/statuscheck[area/text() = $active_area]">
                                        <td style="width: 240px; border: 1px solid black; text-align: center;" valign="middle">
                                            <xsl:attribute name="class">
                                                <xsl:value-of select="RAG"/>
                                            </xsl:attribute>
                                            <div style="margin: 10px; font-size: 16px;">
                                            <a>
                                                <xsl:attribute name="href">
                                                    <xsl:value-of select="url"/>
                                                </xsl:attribute>
                                                <xsl:value-of select="node"/>
                                            </a>
                                            </div>
                                        </td>
                                    </xsl:for-each>
                                </tbody>
                            </table>
                        </xsl:for-each> 
            </body>
        </html>
    </xsl:template>

</xsl:stylesheet>

thanks in advance Gavin

gav1nb
  • 21
  • 1
  • See [XSLT 1.0 Grouping Key for different Nodes and Elements](https://stackoverflow.com/q/7061614/205233) for a very similar question. [Related search](https://www.google.com/search?q=site%3Astackoverflow.com+xslt+group+by+key) with more similar questions. – Filburt Jun 29 '17 at 11:16
  • 1
    Is there any reason you really need this to be a table? Honestly from your HTML it looks like you're far better off just creating a div block for each `statuscheck`, and laying those out using CSS. – Flynn1179 Jun 29 '17 at 12:40
  • thanks.. I'm quite new with html but googled the CSS layout, looks like it will do what I need.. thank you – gav1nb Jun 29 '17 at 13:24
  • It should be fairly straightforward- just set the individual divs to `display:inline` (or possibly `display:inline-block`), and if they and the containing div have appropriate fixed widths, you should end up with four per row. – Flynn1179 Jun 29 '17 at 14:06

1 Answers1

1

I'm inclined to agree with @Flynn1179's comment from a stylistic perspective. HTML tables should be used to present tabular data, not strictly for layout purposes. If your data were inherently tabular then mapping them to columns and rows would be natural. But that doesn't mean XSLT can't do the job as presented.

Before talking about how XSLT might be applied to the task, however, I should first point out that your XSLT is written like a procedural program, which is not XSLT's natural paradigm. Even within that, it fails to make use of some of XSLT features that could make it clearer and simpler. General recommendations:

  • Do not use xsl:for-each where xsl:apply-templates and a separate template could serve instead.
  • Along with that, do not hesitate to use multiple top-level templates. Splitting a big template into multiple smaller ones is like breaking a long function into several shorter ones, with many of the same advantages for readability, maintainability, and reusability.
  • Prefer literal result elements and attributes over using xsl:element and xsl:attribute. The latter are rarely, if ever, needed except when the element / attribute name needs to be computed by the transform.
  • In particular, be aware that the values of literal result attributes are "attribute value templates", within which you can compute XPath expressions.
  • Learn about xsl:key and the key() function. These are much more useful than a newcomer might think. Among other things, they are very useful for grouping.

With that said, then, consider this refactoring of your stylesheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="html"/>

  <!-- group the statuscheck elements by their areas -->    
  <xsl:key name="area" match="/report/status/statuscheck" use="area"/>

  <!-- Provides the top-level document structure -->
  <xsl:template match="/">
    <html>
      <head>
        <style>
        </style>
      </head>
      <body style="font-family: Sky Text;">
        <!-- generate the body contents by transforming the area_name elements -->
        <xsl:apply-templates select="report/area/area_name"/>
      </body>
    </html>
  </xsl:template>

  <!-- Transforms area_name elements to produce area information.  Note that
       The template is not restricted to drawing on the subtree rooted at the
       context node. -->
  <xsl:template match="area_name">
    <xsl:variable name="active_area" select="string(.)"/>
    <div style="font-size: 20px; font-weight: bold; margin: 10px 0 10px 0;"><xsl:value-of select="$active_area"/></div>
    <table style="font-family: Sky Text; border-collapse: collapse; width: 960px;">
      <tbody>
        <!-- each row is generated by transforming a distinguished element;
             specifically, the first -->
        <xsl:apply-templates select="key('area', $active_area)[position() mod 4 = 1]" mode="row-head">
          <!-- this is one way to tell the template we're about to apply which are
               the other statuscheck element's in the context node's group: -->
          <xsl:with-param name="area-checks" select="key('area', $active_area)"/>
        </xsl:apply-templates>
      </tbody>
    </table>
  </xsl:template>

  <!-- transform a statuscheck node by emitting a <tr> element with a <td> child
       for each item in the row -->
  <xsl:template match="statuscheck" mode="row-head">
    <xsl:param name="area-checks"/>
    <xsl:variable name="row-start" select="position() * 4 - 3"/>
    <tr>
      <!-- the <td> elements are generated by a separate template -->
      <xsl:apply-templates select="$area-checks[position() >= $row-start and position() &lt; $row-start + 4]"/>
    </tr>
  </xsl:template>

  <!-- This template and the other matching the same elements are distinguished
       by their modes. -->
  <xsl:template match="statuscheck">
    <!-- Note how the value of the 'class' literal result attribute is expressed
         via an XPath expression.  You don't need xsl:attribute for that. -->
    <td class="{RAG}" style="width: 240px; border: 1px solid black; text-align: center;" valign="middle">
      <div style="margin: 10px; font-size: 16px;">
        <a href="{url}"><xsl:value-of select="node"/></a>
      </div>
    </td>
  </xsl:template>

</xsl:stylesheet>
John Bollinger
  • 160,171
  • 8
  • 81
  • 157