• plainTemplates - CSS driven templating engine

    by Tobiasz Cudnik on 2007-07-30 17:16:18

    Introduction

    plainTemplates is a PHP and JS library that aims to divide template into 2 separate layers:

    1. CSS selectors driven data attaching mechanism
    2. Plain HTML

    It's simply creates your templates from plain HTML files on-the-fly, filling it with data and fetching parts you like.

    It's basen on phpQuery - a jQuery port to PHP.

    Pros

    • True separation of data and visual layer
    • HTML can be filled with example data
    • High reusability (several templates from one HTML, one template from several HTMLs)
    • Framework independed template organisation
    • Transparent refreshes (including PHP code changes!)
    • phpQuery integration
    • No need to lear new, template-only language
    • Saves a lot of time ;)

    Cons

    • You have to know CSS selectors (XPath will also help)
    • In most cases you have to know jQuery/phpQuery
    • You have to format data outside template (eg date).

    Here is simple example, which inserts data from $data array into template in template.htm:

    // get the class
    require_once('../../plainTemplates/plainTemplates.php');
    // always end dir with a slash
    plainTemplates::$cacheDir = '../../plainTemplates/cache/';
    $data = array(
    	array(
    		'title' => 'News 1 title',
    		'body'	=> 'News 1 body',
    	),
    	array(
    		'title' => 'News 2 title',
    		'body'	=> 'News 2 body',
    	),
    	array(
    		'title' => 'News 3',
    		'body'	=> 'News 3 body',
    	),
    );
    // include to fully preserve scope
    include(
    	plainTemplates::createTemplate(
    		// first the template source
    		'template.htm',
    		// second the var info
    		array(
    			// var name in scope
    			'$data',
    			// and var value
    			$data,
    			// finally the selectors (of course there are defaults, that is UL and LI)
    			array(
    				'container' => '.someclass ul',
    				'row'		=> 'li'
    			)
    		)
    	)
    );

    Here is template.htm itself:

    <div class='someclass'>
    	<p>UL below will be container, and every LI will be searched agains classes with field names.</p>
    	<ul>
    		<li>
    			<p>Only tags with field classes are replaced.</p>
    			<p class='title'>This will be replaces by the title</p>
    			<p class='body'>And this by the body...</p>
    		</li>
    		<li>
    			Only first row will be in the result and this wont be considered by any chance... so You can use as many row examples as You want.
    		</li>
    	</ul>
    </div>

    Now the result's source:

    <ul><?php foreach( $data as $dataRow ): ?>
    			<li>
    			<p>Only tags with field classes are replaced.</p>
    			<p class="title"><?php print $dataRow['title']; ?></p>
    			<p class="body"><?php print $dataRow['body']; ?></p>
    		</li><?php endforeach; ?></ul>
    

    Those are the basics, it can do a lot more, but i think its good that it can do simple things in a simple way...

    Intermediate examples

    EXAMPLE: Nested data

    In this example you will learn about nested data, which is very common thing in eg CakePHP. So let's get started!

    Code (examples/nested-data/code.php)

    $data = array(
    	array(
    		'Post' => array(
    			'title' => 'Im the title',
    			'body' => 'Im the body',
    		),
    		'Comment' => array(
    			array(
    				'author' => 'Im the 1st comment of 1st post',
    				'body' => "This field has same name as post's one",
    			),
    			array(
    				'author' => 'Im the 2nd comment of 1st post',
    				'body' => 'So am i',
    			),
    		),
    	),
    	array(
    		'Post' => array(
    			'title' => 'Im the post without comments',
    			'body' => 'His right',
    		),
    		'Comment' => array(),
    	),
    );
    // include to fully preserve scope
    include(
    	plainTemplates::createTemplate(
    		// first the template source
    		'template.htm',
    		// second the var info
    		array(
    			// var name in scope
    			'$data',
    			// and var value
    			$data,
    			// finally the selectors (of course there are defaults, that is UL and LI)
    			array(
    				// selectors for 'Post' row
    				'container' => '.someclass > ul',
    				'row'		=> '> li',
    				// selectors for 'Comment'. we have to group them inside 'Comment' index because it's another data (list), not row
    				'Comment' => array(
    					'container' => '.someclass > ul',
    				),
    			)
    		)
    	)
    );

    Template (examples/nested-data/template.htm)

    <div class='someclass'>
    	<p>UL below will be container, and every LI will be searched agains classes with field names.</p>
    	<ul>
    		<li>
    			<p>In nested row, class names must have parent index prepended.</p>
    			<p class='Post-title'>This will be replaces by the title</p>
    			<p class='Post-body'>And this by the body...</p>
    			<div>
    				<ul>
    					<li>
    						<p>Here is different situation - we have nested data (list), with container etc, so we DONT prepend index to class name.</p>
    						<p class='author'>Comment's author field</p>
    						<p class='body'>And the body</p>
    					</li>
    				</ul>
    			</div>
    		</li>
    		<li>
    			Only first row will be in the result and this wont be considered by any chance... so You can use as many row examples as You want.
    		</li>
    	</ul>
    </div>

    Result::

    <ul><?php foreach( $data as $dataRow ): ?>
    			<li>
    			<p>In nested data class names must have parent index prepended.</p>
    			<p class="Post-title"><?php print $dataRow['Post']['title']; ?></p>
    			<p class="Post-body"><?php print $dataRow['Post']['body']; ?></p>
    			<div>
    				<ul><?php foreach( $dataRow['Comment'] as $dataRowComment ): ?>
    				<li>
    						<p class="author"><?php print $dataRowComment['author']; ?></p>
    						<p class="body"><?php print $dataRowComment['body']; ?></p>
    					</li><?php endforeach; ?></ul></div>
    		</li><?php endforeach; ?></ul>
    

    EXAMPLE: Row insertion

    Previous examples was about inserting a list of data. Now you will learn about inserting only one row, eg to show this post.

    Code (examples/row/code.php)

    $data = array(
    	'Post' => array(
    		'title' => 'Im the title',
    		'body' => 'Im the body',
    	),
    	'Comment' => array(
    		array(
    			'author' => 'Im the 1st comment of 1st post',
    			'body' => "This field has same name as post's one",
    		),
    		array(
    			'author' => 'Im the 2nd comment of 1st post',
    			'body' => 'So am i',
    		),
    	)
    );
    // include to fully preserve scope
    include(
    	plainTemplates::createTemplate(
    		// first the template source
    		'template.htm',
    		// second the var info
    		array(
    			// var name in scope
    			'$data',
    			// and var value
    			$data,
    			// finally the selectors (of course there are defaults, that is UL and LI)
    			array(
    				// empty container means that we dont want a loop
    				'container' => '',
    				// you can't use plain 'li', because all elements selected by this seletor except first one will be removed
    				// in this case LI for comment row
    				// there are other ways to write this selector, but this is the simplest
    				'row'		=> '.someclass > ul > li',
    				// this hasn't changed
    				'Comment' => array(
    					'container' => '.someclass > ul ul',
    				),
    			)
    		)
    	)
    );

    Template (examples/row/template.htm) is the same as previous one.

    Result

    <p>In nested row, class names must have parent index prepended.</p>
    <p class="Post-title"><?php print $data['Post']['title']; ?></p>
    <p class="Post-body"><?php print $data['Post']['body']; ?></p>
    <div>
    				<ul><?php foreach( $data['Comment'] as $dataComment ): ?>
    				<li>
    						<p>Here is different situation - we have nested data (list), with container etc, so we DONT prepend index to class name.</p>
    						<p class="author"><?php print $dataComment['author']; ?></p>
    						<p class="body"><?php print $dataComment['body']; ?></p>
    					</li><?php endforeach; ?></ul></div>
    

    EXAMPLE: Callback

    In this example i will introduce callback function and it's often use to hidding uneeded content (comments listing wrapper in this case).

    Callback is a function/method which gets phpQuery object at the end of template creation, but before saving file, so it can affect result.

    Code (examples/callback/code.php), $data is same as in Nested data example

    function callbackName($_) {
    	// find the only div in template
    	$_->find('div')
    		// insert condition before div
    		->beforePHP('if ( $dataRow[\'Comment\'] ):')
    		// close condition after div
    		->afterPHP('endif;');
    }
    // include to fully preserve scope
    include(
    	plainTemplates::createTemplate(
    		// first the template source
    		'template.htm',
    		// second the var info
    		array(
    			// var name in scope
    			'$data',
    			// and var value
    			$data,
    			// finally the selectors (of course there are defaults, that is UL and LI)
    			array(
    				'container' => 'ul:first',
    				'row'		=> '> li',
    				'Comment' => array(
    					'container' => 'ul ul',
    				),
    			)
    		),
    		// now the new thing - callback
    		// callback function will have phpQuery object as only arg
    		'callbackName'
    	)
    );

    Template (examples/callback/template.htm)

    <p>UL below will be container, and every LI will be searched agains classes with field names.</p>
    <ul>
    	<li>
    		<p>In nested row, class names must have parent index prepended.</p>
    		<p class='Post-title'>This will be replaces by the title</p>
    		<p class='Post-body'>And this by the body...</p>
    		<div>
    			<h1>Comments</h1>
    			<ul>
    				<li>
    					<p>Here is different situation - we have nested data (list), with container etc, so we DONT prepend index to class name.</p>
    					<p class='author'>Comment's author field</p>
    					<p class='body'>And the body</p>
    				</li>
    			</ul>
    		</div>
    	</li>
    	<li>
    		Only first row will be in the result and this wont be considered by any chance... so You can use as many row examples as You want.
    	</li>
    </ul>

    Result

    <ul><?php foreach( $data as $dataRow ): ?>
    	<li>
    		<p>In nested row, class names must have parent index prepended.</p>
    		<p class="Post-title"><?php print $dataRow['Post']['title']; ?></p>
    		<p class="Post-body"><?php print $dataRow['Post']['body']; ?></p>
    		<?php if ( $dataRow['Comment'] ): ?><div>
    			<h1>Comments</h1>
    			<ul><?php foreach( $dataRow['Comment'] as $dataRowComment ): ?>
    			<li>
    					<p>Here is different situation - we have nested data (list), with container etc, so we DONT prepend index to class name.</p>
    					<p class="author"><?php print $dataRowComment['author']; ?></p>
    					<p class="body"><?php print $dataRowComment['body']; ?></p>
    				</li><?php endforeach; ?></ul></div><?php endif; ?>
    	</li><?php endforeach; ?></ul>
    

    Advanced examples

    I will post some advanced examples here near future, so stay tuned and for now you can read generated phpdocs or those in the source code.

    Download and links

    Comments: 0

    PHP phpQuery project plainTemplates plainTemplates release