1

I have some XML structured like below:

<report>
  <columns>
    <column col_id="col_1">Column 1 title</column>
    <column col_id="col_2">Column 2 title</column>
    <column col_id="col_3">Column 3 title</column>
  </columns>
  <rows>
    <row>
      <col_1>Value 1</col_1>
      <!-- No col_2 value exists -->
      <col_3>value 3</col_3>
    </row>
    <row>
      <col_1>Value 1</col_1>
      <col_2>Value 2</col_2>
      <col_3>value 3</col_3>
      <col_4>Value 4</col_4><!-- Not interested in this value -->
    </row>
  </rows>
</report>

I want to output this data as a HTML table using XSLT.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html" encoding="utf-8" indent="yes" />
   <xsl:template match="Report">
      <!-- Get the column names -->
      <xsl:variable name="arr_columns" select="./columns/*" />

      <div id="example_report" class="report">
         <table style="width:100%">
            <thead>
               <tr class="header">
                  <!-- Columns first -->
                  <xsl:for-each select="$arr_columns">
                     <th><xsl:value-of select="." /></th>
                  </xsl:for-each>
               </tr>
            </thead>
            <tbody>
               <!-- Now the actual data -->
               <xsl:for-each select="./rows/row">
                  <xsl:variable name="row" select="." />
                  <tr>
                     <xsl:for-each select="$arr_columns">
                        <xsl:variable name="col_id" select="./@column_id" />
                        <td><xsl:value-of select="$row/$col_id" /></td>
                     </xsl:for-each>
                  </tr>
               </xsl:for-each>
            </tbody>
         </table>
      </div>
   </xsl:template>
</xsl:stylesheet>

I was hoping that <xsl:value-of select="$row/$col_id" /> would let me select the column based on its name as stored in the col_id variable. However, PHP's XSLTProcessor blows up on it instead and the report fails to render.

PHP Warning: XSLTProcessor::transformToXml(): No stylesheet associated to this object

I've tried outputting just $row (which as expected outputs the entire row as a string), and $row/col_1 (which outputs the contents of col_1 as expected), but trying to access a node based on the value of a variable just doesn't seem to work. If I output the value of $col_id, it displays the name of the node I want to output, so that variable is getting correctly set in the loop.

I'm guessing that it is possible and I'm doing this wrong, but I'm having a hard time finding an example of how to do this correctly. I did find examples that let you select nodes based on attributes, but as you can see the nodes I have don't have attributes, their name indicates what they hold.

Can someone help with writing the correct selector for this case?

For completeness, this is the output I'm hoping to generate :

<table>
  <thead>
    <tr>
      <th>Column 1 title</th>
      <th>Column 2 title</th>
      <th>Column 3 title</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Value 1</td>
      <td></td>
      <td>value 3</td>
    </tr>
    <tr>
      <td>Value 1</td>
      <td>value 2</td>
      <td>value 3</td>
    </tr>
  </tbody>
</table>
1
  • What will be your expected output? Do you want one <td> each for row and column? Commented Dec 2, 2014 at 12:31

2 Answers 2

2

Try it this way?

XSLT 1.0

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

<xsl:template match="/report">
    <xsl:variable name="columns" select="columns/column" />
    <table style="width:100%">
        <thead>
            <tr>
                <xsl:for-each select="$columns">
                     <th><xsl:value-of select="." /></th>
                </xsl:for-each>
            </tr>
        </thead>
        <tbody>
            <xsl:for-each select="rows/row">
                <xsl:variable name="current-row" select="." />
                <tr>
                     <xsl:for-each select="$columns">
                        <td><xsl:value-of select="$current-row/*[local-name()=current()/@col_id]"/></td>
                     </xsl:for-each>
                </tr>
            </xsl:for-each>
        </tbody>
    </table>
</xsl:template>

</xsl:stylesheet>
Sign up to request clarification or add additional context in comments.

1 Comment

Looks like a good solution. In the end I'd already gone with renaming my data elements to have the same name and a class_id attribute and used $row/[@column_id=$col_id], which seems to work fine. Thanks for the correct solution though.
1

Especially since you have such a clear mapping between elements in your input XML and elements in your target HTML, consider a more matching-oriented, declarative approach rather than your current procedural for-loop-based approach:

Declarative Approach

[Updated per observation made by @michael.hor257k that a bit of procedural looping after matching row helps to meet the requirement that an omitted col_ cell must result in an empty td when an associated column exists.]

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="utf-8" indent="yes" />

  <xsl:template match="report">
    <div id="example_report" class="report">
      <table style="width:100%">
        <xsl:apply-templates/>
      </table>
    </div>
  </xsl:template>

  <xsl:template match="row">
    <tr>
      <xsl:variable name="row" select="."/>
      <xsl:for-each select="../../columns/column">
        <xsl:variable name="col_id" select="@col_id"/>
        <td>
          <xsl:value-of select="$row/*[local-name()=$col_id]"/>
        </td>
      </xsl:for-each>
    </tr>
  </xsl:template>

  <xsl:template match="columns">
    <thead><xsl:apply-templates/></thead>
  </xsl:template>

  <xsl:template match="column">
    <th><xsl:apply-templates/></th>
  </xsl:template>

  <xsl:template match="rows">
    <tbody><xsl:apply-templates/></tbody>
  </xsl:template>
</xsl:stylesheet>

1 Comment

IMHO, your second template should match row/*. There's no basis to assume that column names start with 'col_'. But the bigger issue you have is that your output is not the requested output (you need an empty td for the missing col_2 in the first row).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.