Reputation: 9284
I'm working on rebuilding the UI portion of our website which is all heavily javascript/ajax based (for no good reason and in a fairly inefficient way) such that the backend will now do most of the content generation. It is a C# .net application.
Almost all of our pages (which there are probably 40-50 pages) have the same basic layout. I am brand new to XSLT but I've done a lot of work with MVC frameworks such as Spring (java, using Sitemesh for layout), Symfony (PHP), a bit of rails as well as a few others. I love having the ability to have one or several common templates and then have a specific "content" section where the page specific stuff goes. I can't figure out how this is done with XSLT. In the case of this application I have a value available to me in the xml backing the xslt page, lets call it ContentXSL, who's value is the name of the xsl file I want to use for the content section of the page. I know it's not possible but it would be nice to use:
<xsl:call-template name="{$ContentXSL}" />
Then I could simply put that in the content section.. However this isn't possible, so instead I will need a massive choose statement that calls the correct template based on the ContentPage variable.. This also means in my Layout.xsl file I would have to include all 40-50 xsl documents.. I would think the overhead would be pretty big, but I'm not sure about that. Is this a reasonable thing to do if the site gets a lot of traffic?
What are other common ways of doing this? It seems like most modern frameworks allow you to use this pattern to decorate content. In the case of Symfony it worked really well and was pretty flexibile (with slots and all that).
I know the other potential solution is to have 40 independant files that all have similar markup and include special sections like the header and footer. This means if I want to change the overall structure of the layout of my site I'd have to edit all 40-50 pages though (very annoying).
Update -- More Explanation
I want to further explain this because I have certain requirements which would take considerable engineering to change. First of all, the backend is going to pass to me some xml which will let me know of query args are in the URL of the website.. Also, it will pass to me the data I need to build my page (data in the form of business data, no html or anything like that). The data looks similar to this:
<xml>
<section>Blogs</section>
<page>showAll</section>
<data>
<blogs>
<blog>
<author>somebody</author>
<title></title>
<content>..</content>
</blog>
</blog>..</blog>
</blogs>
</data>
</xml>
Now what want is to have a page template like this:
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl='urn:schemas-microsoft-com:xslt'>
<xsl:output omit-xml-declaration='yes' method='html' media-type='text/html' indent='yes' />
<xsl:include href="Header.xsl"/>
<xsl:include href="Nav.xsl"/>
<xsl:template name='MainLayout' match='*'>
<html>
<head>
<title></title>
</head>
<body>
<div id="header"><xsl:call-template name="Header" /></div>
<div id="nav"><xsl:call-template name="Nav" /></div>
<div id="content">
[here is where i want to use the xsl from {/xml/section}/{/xml/page}.xsl]
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Now in for the content of this page I would have the following file: Blogs/showAll.xsl
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl='urn:schemas-microsoft-com:xslt'>
<xsl:output omit-xml-declaration='yes' method='html' media-type='text/html' indent='yes' />
<xsl:template name='Blogs_ShowAll'>
<div id="blogs-showAll">
..iterate over /xml/data/blogs converting to html
</div>
</xsl:template>
</xsl:stylesheet>
The solutions so far have been good but only one of them was I able to fully digest (the one which mentions including all the xsl files and using a xsl:choose to select the right one). I am not sure how to apply the FXSL method to the problem at hand. Note that I would not be opposed to using a sitemesh type approach which I specify the html/body tags and all that in the child and have it replace what I have in the body section of the child into the layout's content div (also, if there is a title tag in the child replace the title tag in the layout -- stuff like that).
Upvotes: 2
Views: 2829
Reputation: 243549
The OP has provided additional details of his problem and this answer provides the additional solution that is now requested.
I. The idea:
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<html>
<xsl:apply-templates select="*/page"/>
</html>
</xsl:template>
<xsl:template match="page[. = 'showAll']">
<!-- Transform all data to html -->
<xsl:apply-templates select="../*/blogs" mode="showAll"/>
</xsl:template>
<xsl:template match="page[. = 'showBrief']">
<!-- Transform the data to Summary html -->
<xsl:apply-templates select="../*/blogs" mode="showBrief"/>
</xsl:template>
<xsl:template match="blogs" mode="showAll">
<h1>All Blogs: </h1>
<table border="1">
<xsl:apply-templates mode="showAll"/>
</table>
</xsl:template>
<xsl:template match="blog" mode="showAll">
<tr>
<td>Blog of <xsl:value-of select="author"/></td>
<td><xsl:value-of select="title"/></td>
</tr>
<tr>
<td colspan="2"><xsl:apply-templates select="content/node()" mode="showAll"/></td>
</tr>
<xsl:if test="not(position()=last())">
<tr><td colspan="2"> </td></tr>
</xsl:if>
</xsl:template>
<xsl:template match="blogs" mode="showBrief">
<h1>Blogs Summary: </h1>
<table border="1">
<xsl:apply-templates mode="showBrief"/>
</table>
</xsl:template>
<xsl:template match="blog" mode="showBrief">
<tr>
<td>
<xsl:value-of select="concat(author, ': ', title)"/>
</td>
</tr>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document (based on the provided XML text, but making it well-formed and more substantial):
<xml>
<section>Blogs</section>
<page>showAll</page>
<data>
<blogs>
<blog>
<author>John Smith</author>
<title>All about golden fish</title>
<content>
Here I publish my latest achievements
in raising golden fish.
</content>
</blog>
<blog>
<author>Mary Jones</author>
<title>Knitting, Knitting, Knitting</title>
<content>
How to knit a sharf.
</content>
</blog>
</blogs>
</data>
</xml>
produces the desired "show-all" type of output:
<html>
<h1>All Blogs: </h1>
<table border="1">
<tr>
<td>Blog of John Smith</td>
<td>All about golden fish</td>
</tr>
<tr>
<td colspan="2">
Here I publish my latest achievements
in raising golden fish.
</td>
</tr>
<tr>
<td colspan="2"> </td>
</tr>
<tr>
<td>Blog of Mary Jones</td>
<td>Knitting, Knitting, Knitting</td>
</tr>
<tr>
<td colspan="2">
How to knit a sharf.
</td>
</tr>
</table>
</html>
Now we change the XML document and replace the page
element with this one:
<page>showBrief</page>
When the same transformation is applied on the updated XML document, it now produces the desired summary output:
<html>
<h1>Blogs Summary: </h1>
<table border="1">
<tr>
<td>John Smith: All about golden fish</td>
</tr>
<tr>
<td>Mary Jones: Knitting, Knitting, Knitting</td>
</tr>
</table>
</html>
II. The next step
In practice all templates in a given mode will be in their separate xsl file and will be imported by the primary stylesheet:
The transformation (primary stylesheet) thus becomes:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:import href="showAll.xsl"/>
<xsl:import href="showBrief.xsl"/>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<html>
<xsl:apply-templates select="*/page"/>
</html>
</xsl:template>
</xsl:stylesheet>
Do note:
The transformation doesnt know in advance what templates will be applied -- the transformation is completely data-driven.
Templates that do not exist now can be written in the future and will be applied without the need to change the primary stylesheet.
There is no conditional logic, <xsl:choose>
instructions etc. This is the true power of xsl templates in action.
This transformation is based on the same idea on which FXSL is based on.
Upvotes: 1
Reputation:
Besides excellent Dimitre's answer recommending a method to implement higher-order functions, you could also use a method with master pages and child pages combining with some sort of code behind, like this:
MasterContent.xml:
<title>Test for XSLT</title>
MasterLayout.xml:
<html>
<head>
<title></title>
</head>
<body>
<p>This is master page</p>
</body>
</html>
Master.xsl:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="pMasterLayout" select="document('MasterLayout.xml')"/>
<xsl:param name="pMasterContent" select="document('MasterContent.xml')"/>
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:apply-templates select="$pMasterLayout/*"/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="title">
<xsl:copy>
<xsl:value-of select="$pMasterContent/title"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
ChildLayout:
<html>
<head>
<title></title>
</head>
<body>
<h1></h1>
</body>
</html>
So, this transformation ("Child.xsl"):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:include href="Master.xsl"/>
<xsl:param name="pChildLayout" select="document('ChildLayout.xml')"/>
<xsl:param name="pChildContent" select="/"/>
<xsl:template match="body">
<xsl:copy>
<xsl:apply-templates select="$pChildLayout/html/body/*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="h1">
<xsl:copy>
<xsl:value-of select="$pChildContent/header"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
With this input ("ChildContent"):
<header>Child Content</header>
Output:
<html>
<head>
<title>Test for XSLT</title>
</head>
<body>
<h1>Child Content</h1>
</body>
</html>
Note:
Check a better living example at aranedabienesraices.com.ar
I recommend to use @id
as anchors to populate the layout with content (you can strip those with empty templates). This method does not tie you to any vendor IDE (with notion of XSLT) to built your layout pages.
Upvotes: 0
Reputation: 1042
Dimitre's example is good..
Here is a way to do this also.. a little ugly solution but does the trick
primary.xsl
<xsl:variable name="ContentXSL" select="/your/xml/settings/@content" />
<!-- Reference templates -->
<xsl:include href="template1.xsl" />
<xsl:include href="template2.xsl" />
<xsl:include href="template3.xsl" />
<xsl:include href="template4.xsl" />
<xsl:template match="/">
<html>
<head>
<title>..</title>
</head>
</html>
<body>
<xsl:call-template name="getcontent" />
</body>
</xsl:template>
<xsl:template name="getcontent">
<xsl:choose>
<xsl:when test="$ContentXSL = 'template1'">
<xsl:apply-templates match="/your/xml/structure" mode="template1" />
</xsl:when>
<xsl:when test="$ContentXSL = 'template2'">
<xsl:apply-templates match="/your/xml/structure" mode="template2" />
</xsl:when>
<xsl:when test="$ContentXSL = 'template3'">
<xsl:apply-templates match="/your/xml/structure" mode="template3" />
</xsl:when>
<xsl:when test="$ContentXSL = 'template4'">
<xsl:apply-templates match="/your/xml/structure" mode="template4" />
</xsl:when>
<xsl:otherwise>
<!-- Default template? -->
<xsl:apply-templates match="/your/xml/structure" mode="template1" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
template1.xsl
<xsl:template match="/your/xml/structure" mode="template1">
Template 1<br />
</xsl:template>
template2.xsl
<xsl:template match="/your/xml/structure" mode="template2">
Template 2<br />
</xsl:template>
template3.xsl
<xsl:template match="/your/xml/structure" mode="template3">
Template 3<br />
</xsl:template>
template4.xsl
<xsl:template match="/your/xml/structure" mode="template4">
Template 4<br />
</xsl:template>
Upvotes: 0
Reputation: 243549
<xsl:call-template name="{$ContentXSL}" />
While this is syntactically illegal in all versions of XSLT, using XSLT templates as higher-order functions has been implemented and used in the FXSL library since ten years ago.
Here is a somewhat simplified idea of how this can be achieved:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:param name="pFunction1">
<fun name="increment"/>
</xsl:param>
<xsl:param name="pFunction2">
<fun name="double"/>
</xsl:param>
<xsl:variable name="vFunIncrement" select=
"document('')/*/xsl:param[@name='pFunction1']/*"/>
<xsl:variable name="vFunDouble" select=
"document('')/*/xsl:param[@name='pFunction2']/*"/>
<xsl:variable name="vInput" select="."/>
<xsl:template match="/">
increment(<xsl:value-of select="$vInput"/>) = <xsl:text/>
<xsl:apply-templates select="$vFunIncrement">
<xsl:with-param name="parg1" select="$vInput"/>
</xsl:apply-templates>
double(<xsl:value-of select="$vInput"/>) = <xsl:text/>
<xsl:apply-templates select="$vFunDouble">
<xsl:with-param name="parg1" select="$vInput"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="fun[@name='double']">
<xsl:param name="parg1"/>
<xsl:value-of select="2*$parg1"/>
</xsl:template>
<xsl:template match="fun[@name='increment']">
<xsl:param name="parg1"/>
<xsl:value-of select="$parg1+1"/>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<num>2</num>
the result is:
increment(2) = 3
double(2) = 4
Do note:
The <fun>
elements can be passed externally to the transformation through global-level parameters. This means that the transformation doesn't know what functions will be executed.
Functions are simulated by templates matching fun
elements that have a specific value for their name
attribute.
In case if you want to read and understand FXSL, these are the two best materials:
Upvotes: 2