So I read this article a while back http://www.workingsoftware.com.au/page/Your_templating_engine_sucks_and_everything_you_have_ever_written_is_spaghetti_code_yes_you
And I 100% agree with the theory behind it. What I haven’t managed to find is a decent implementation of this separation of concerns.
It generally results in an awful lot of messy template manipulation code outside the template, which I’ve always found worse than having the logic inside the template. We already do this with CSS, separate the presentation from the markup and that should be the goal here… so then it hit me, why can’t we use a simplistic CSS style syntax to provide our display logic externally from the markup.
So here’s what I came up with. There are 3 components:
-
The template. This is pure markup and does not contain any processing instructions or display logic (such as loops and if statements)
-
A set of data that will be used to fill the template
-
A CDS (Cascading Data-sheet) which describes how the data should be applied to the template. This is as close to CSS grammar and concepts as possible
So here’s an example:
$xml = '<template>
<h1>Header goes here</h1>
</template>';
$cds = 'h1 {content: data();}';
$data = 'hello world!';
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
Which then prints:
<h1>hello world</h1>
Of course, it needs to be able to handle more complex data structures:
$xml = '<template>
<h1>Header goes here</h1>
</template>';
//Read the header attribute from the available data
$cds = 'h1 {content: data(header);}';
$data = new \stdclass;
$data->header = 'hello world!';
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
Which generates the same output:
<h1>hello world</h1>
It currently supports most of the basic CSS selectors e.g.
$xml = '<template>
<h1 id="foo">Header goes here</h1>
</template>';
//Read the header attribute from the available data
$cds = '#foo {content: data(header);}';
$data = new \stdclass;
$data->header = 'hello world!';
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
and
$xml = '<template>
<h1 class="foo">Header goes here</h1>
</template>';
//Read the header attribute from the available data
$cds = '.foo {content: data(header);}';
$data = new \stdclass;
$data->header = 'hello world!';
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
as well as nesting:
$xml = '<template>
<main>
<h1 class="foo">Header goes here</h1>
</main>
</template>';
$cds = 'main .foo {content: data(header);}';
$data = new \stdclass;
$data->header = 'hello world!';
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
Loops
So one of the most important things to be able to do in a template is loop through a data set. This can be achieved by the repeat
command and instead of reading content
from data()
it can be read from iteration()
which points at the current iteration’s data:
$xml = '<template>
<main>
<ul>
<li>List item</li>
</ul>
</main>
</template>';
$cds = 'ul li {repeat: data(); content: iteration();}';
$data = ['One', 'Two', 'Three'];
$template = new \CDS\Builder($xml, $cds, $data);
echo $template->output();
Which generates:
<main>
<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
</main>
And it’s possible to build all this up to target specific nodes inside the repeated element:
$data = new stdclass;
$data->list = [];
$one = new stdclass;
$one->name = 'One';
$one->id = '1';
$data->list[] = $one;
$two = new stdclass;
$two->name = 'Two';
$two->id = '2';
$data->list[] = $two;
$three = new stdclass;
$three->name = 'Three';
$three->id = '3';
$data->list[] = $three;
$template = '<template name="">
<ul>
<li>
<h2>an id</h2>
<span>and a name</span>
</li>
</ul>
</template>';
$css = 'ul li {repeat: data(list);}
ul li h2 {content: iteration(id)}
ul li span {content: iteration(name); }';
$template = new \CDS\Builder($template, $css, $data);
notice the use of iteration(value)
which reads the value from the iterated object.
Which prints
<ul>
<li>
<h2>1</h2>
<span>One</span>
</li><li>
<h2>2</h2>
<span>Two</span>
</li><li>
<h2>3</h2>
<span>Three</span>
</li>
</ul>
That’s as far as I’ve got for now. The code is available here
and its very alpha
edit: Why won’t this link work??
Some extension possibilities are:
/* Sets the content to a specific value rather than reading from `data()` or `iteration()` */
#whatever {content: "whatever";}
/* Append/prepend content to the node rather than overwriting it. */
#whatever:before {content: data(foo); }
#whatever:after {content: data(foo); }
/* Loop through the users but hide any where `$user->type == 'admin'` */
#whatever {repeat: data(user); }
#whatever:iteration[type="admin"] {display: none}
``
/* Set an attribute value/add a class based on data or specific value */
/* set the data-id attribute to `$data->id` */
#whatever {attribute: data-id data(id);}
/* add a class name based on a value from the object */
#element {addclass: iteration(type);}
/* some formatting options: */
/* Formats content to 2 decimal places */
#element {content: data(total) decimal 2}
/* This would probably be better if the class name was lower case */
#element {addclass: iteration(type) lowercase;}
None of this is implemented of course and there are few obvious problems left to solve:
- How do I call methods on objects in data()?
- How do I build a string, for example I might want to set a href to be: /products/data(id)
But these should be easy enough to overcome!
As I said, this is entirely proof-of-concept and most of the features I’ve thought of aren’t implemented but I’d be interested in thoughts/opinions of this approach.
As was highlighted in the article that inspired me, the obvious advantage is that the display logic can be reused with multiple templates. By using class rather than element names, it’s easy to write multiple completely different templates that can use the same logic.
Post edited by cpradio to resolve the link issue