Content Renderer

The primary motivation for implementing RepeaterFlex was to have sort of a standard to create structured content for websites without re-implementing more advanced stuff like slideshows, grids, maps along with their required fields and code over and over. With the RepeaterFlex you can add content only minutes after installation and configuration, since any plugin code once implemented can be re-used without modification.

Structured Content, in this case, refers to the hierarchical structure of the HTML DOM tree. The DepthRenderer takes care of properly closing opened tags and may be used to create content for CSS frontends like bootstrap or UiKit. On this site the DepthRenderer takes care of not only the page content, but also the menu (using MenuRenderer) and the footer.

For generating page content like this, a specialized version of the DepthRenderer is used, the ContentRenderer. The only, yet significant extension is the availability of a tag handler, which manages dynamic creation of HTML tags along with classes and attributes, which simpifies implementation of item code significantly.

Such content may look like this in admin:

The first element shown is a UiKit Div, which just renders the shadow. Its only direct child is a UiKit Grid item having two UiKit ScrollSpy children (next depth level). Each is defined to have a width of 50% for large media sizes. The first ScrollSpy hosts another container which contains an UiKit UL List. The container in the other ScrollSpy defines an UiKit Accordion. This block gets rendered like this:


Some features of the RepeaterFlex as a list

  • Similar to RepeaterMatrix in admin
  • Items defined by script
  • Specialized Options field
  • Various iteration modules
  • Integrates RepeaterItem Copy&Paste from David Karich

Primary targets ... as an Accordion

  • Flexibility

    RepeaterFlex may not only be used for page content, rendering sitemaps or menus work nice as well.

  • Portability

    Item configurations may be transported easily between installations.

  • Simplicity

    Output is generated from a simple call to the render method, no iteration of the matrix required.

  • Modularity

    Item types may easily be added or removed from the matrix field.

  • Locality

    Field definition and render code for each item is defined in a single, compact file.

  • Extensibility

    Due to the concept modifying item behaviour or adding new items can be done easily.

  • Effectivity

    Only required code is loaded during rendering, this reduces PHP memory and performance footprint.


Let's take a closer look at some render methods, since this explains the use and operation of the DepthRenderer much better than trying to explain the few available API methods.

plugins/ContentRenderer/FlexUkGrid.inc

plugins/ContentRenderer/FlexUkGrid.inc

<?php namespace ProcessWire;
/*
 * Module implementing UiKit Grid component
 */
class FlexUkGrid extends RepeaterFlexItem {
	public static function getModuleInfo() {
		return array(
			'title' => __('Uikit Grid', __FILE__), // Module Title
			'version' => 1,
			'renderer' => 'ContentRenderer',
			'head' => '{flex_label} [• {Options*title} {TextOnly} ]',
			'icon' => 'fa fa-th',
			);
	}
	public static function getFields() {
		return([
		'@Options' => RepeaterFlexItem::Options([	// Standard Grid Attributes
				'inputfieldClass' => 'InputfieldAsmSelect',	// We'll use AsmSelect here
				'columnWidth' => 25,
				'label' => __('Grid Options'),
				'options' => [	// Specify options using associative arrays:
					1 => [ 'class' => 'uk-grid-small', 'title' => __('small grid'), ],
					2 => [ 'class' => 'uk-grid-medium', 'title' => __('medium grid'), ],
					3 => [ 'class' => 'uk-grid-large', 'title' => __('large grid'), ],
					4 => [ 'class' => 'uk-grid-collapse', 'title' => __('no gutter'), ],
					5 => [ 'class' => 'uk-grid-divider', 'title' => __('divider'), ],
					6 => [ 'class' => 'uk-grid-match', 'title' => __('match height'), ],
					7 => [ 'class' => 'uk-flex-left', 'title' => __('flex left'), ],
					8 => [ 'class' => 'uk-flex-center', 'title' => __('flex center'), ],
					9 => [ 'class' => 'uk-flex-right', 'title' => __('flex right'), ],
					10 => [ 'class' => 'uk-flex-between', 'title' => __('flex between'), ],
					11 => [ 'class' => 'uk-flex-around', 'title' => __('flex around'), ],
					12 => [ 'class' => 'uk-flex-stretch', 'title' => __('flex stretch'), ],
					13 => [ 'class' => 'uk-flex-top', 'title' => __('flex top'), ],
					14 => [ 'class' => 'uk-flex-middle', 'title' => __('flex middle'), ],
					15 => [ 'class' => 'uk-flex-bottom', 'title' => __('flex bottom'), ],
					16 => [ 'grid' => 'masonry:true', 'title' => __('masonry'), ],
					17 => [ 'grid' => 'parallax:50', 'title' => __('parallax 50'), ],
					18 => [ 'grid' => 'parallax:100', 'title' => __('parallax 100'), ],
					19 => [ 'grid' => 'parallax:150', 'title' => __('parallax 150'), ],
					20 => [ 'class' => 'uk-align-left', 'title' => __('left'), ],
					21 => [ 'class' => 'uk-align-center', 'title' => __('center'), ],
					22 => [ 'class' => 'uk-align-right', 'title' => __('right'), ],
					23 => [ 'class' => 'uk-panel', 'title' => __('panel'), ],
					24 => [ 'class' => 'uk-panel-scrollable', 'title' => __('panel scrollable'), ],
					25 => [ 'class' => 'uk-display-inline-block', 'title' => __('display as inline block'), ],
					26 => [ 'class' => 'uk-border-rounded', 'title' => __('border rounded'), ],
					27 => [ 'class' => 'uk-border-circle', 'title' => __('border circle'), ],
					28 => [ 'class' => 'uk-border-pill', 'title' => __('border pill'), ],
					29 => [ 'class' => 'uk-box-shadow-small', 'title' => __('shadow small'), ],
					30 => [ 'class' => 'uk-box-shadow-medium', 'title' => __('shadow medium'), ],
					31 => [ 'class' => 'uk-box-shadow-large', 'title' => __('shadow large'), ],
					32 => [ 'class' => 'uk-box-shadow-xlarge', 'title' => __('shadow xlarge'), ],
					33 => [ 'class' => 'uk-box-shadow-bottom', 'title' => __('shadow bottom'), ],
					],
			]),
			'@TextOnly' => RepeaterFlexItem::TextOnly([
					'label' => __('Additional Grid Classes'),
					'notes' => __('blank separated list of classes like: uk-child-width-1-2@m'),
					'columnWidth' => 50,
				]),
			'@Checkbox' => RepeaterFlexItem::Checkbox([
					'columnWidth' => 25,
					'label' => __('Skip embedding DIV Tags'),
				]),
		]);
		}
	public function render(Page $pg) {
		$Tag = $this->ctx->getTag('div');		// Get a Tag instance (for simplified class/attribute management)
		$Tag->addClasses($pg->Options->explode('class'));	// Add all selected classes from the Options field
		$Tag->addClasses(explode(' ', $pg->TextOnly));		// Add any given additional classes (like media breakpoints)
		$Tag->addAttribute('uk-grid', $pg->Options->implode(';', 'grid'));	// Setup all options for uk-grid
		if(!$pg->Checkbox)						// Disable generation of encapsulating <div> tags
			$this->ctx->setEmbed('<div>', '</div>');
		$this->ctx->setClosetag($Tag->getCloseTag());		// Setup close tag (effectively </div>)
		return($Tag->getOpenTag());				// Only return opening tag, something like <div class='...' uk-grid='...'>
	}
}
    public function render(Page $pg) {
        $Tag = $this->ctx->getTag('div');        // Get a Tag instance (for simplified class/attribute management)
        $Tag->addClasses($pg->Options->explode('class'));    // Add all selected classes from the Options field
        $Tag->addClasses(explode(' ', $pg->TextOnly));        // Add any given additional classes (like media breakpoints)
        $Tag->addAttribute('uk-grid', $pg->Options->implode(';', 'grid'));    // Setup all options for uk-grid
        if(!$pg->Checkbox)                        // Disable generation of encapsulating <div> tags
            $this->ctx->setEmbed('<div>', '</div>');
        $this->ctx->setClosetag($Tag->getCloseTag());        // Setup close tag (effectively </div>)
        return($Tag->getOpenTag());                // Only return opening tag, something like <div class='...' uk-grid='...'>
    }

$this->ctx always refers to the used renderer, which is the ContentRenderer in this case. Using this reference you may directly communicate with the renderer, in this example we obtain an instance of TagBuilder from $this->ctx->getTag('<div'>), which specifies a <div> tag in this case. The $Tag already is initialized with any class/attribute requested from a potential item on lower depth.

Next we add classes from the Options field. Please check out the complete source (from the icons above) to see, that the Options use associative arrays which may contain class and grid elements, the $pg->Options->explode('class') creates an array of all class options only.

Then we add any further classes defined in the TextOnly field. This can be used for all the media size dependant class definitions, which would make the selection list too long.

Similar to pulling all classes from the Options, we are adding attributes using $pg->Options->implode(';', 'grid'). In this case we create a semicolon-separated string which form the argument for the uk-grid parameter.

Inside a uk-grid any element needs to be encapsulated in <div> tags, this is initiated with the call $this->ctx->setEmbed('<div>', '</div>').This will only affect items on next higher depth. In case you only add already <div>-encapsulated items, the Checkbox allows to omit additional encapsulation.

And finally, before returning the opening <div> (along with any collected class/attribute) to the output we register the closing tag with a call to $this->ctx->setClosetag($Tag->getCloseTag()). This effectively is </div> in this case.

plugins/ContentRenderer/FlexUkUlList.inc

plugins/ContentRenderer/FlexUkUlList.inc

<?php namespace ProcessWire;
/*
 * Module using DepthRenderer to render a UiKit UL list for subitems
 */

class FlexUkUlList extends RepeaterFlexItem {
	public static function getModuleInfo() {
		return array(
			'title' => __('UiKit UL List', __FILE__), // Module Title
			'version' => 1,
			'renderer' => 'ContentRenderer',
			'head' => '{flex_label} [• {Options*title} ]',
			'icon' => 'fa fa-fw fa-list-ul',
			);
		}
	public static function getFields() {
		return([
			'@Options' => RepeaterFlexItem::Options([
					'columnWidth' => 25,
					'label' => __('Style'),
					'inputfieldClass' => 'InputfieldRadios',
					'initValue' => 1,
					'required' => 1,
					'options' => [
						1 => '|'.__('no decoration'),
						2 => 'uk-list-bullet|'.__('Bullet'),
						3 => 'uk-list-divider|'.__('Divider'),
						4 => 'uk-list-striped|'.__('Striped'),
						5 => 'uk-list-large|'.__('Large'),
						],
				]),
			]);
		}
	public function render(Page $pg) {
		$Tag = $this->ctx->getTag('ul');				// Get manager for <ul> tag
		$Tag->addClass('uk-list');						// Prepare for UiKit
		if($pg->Options->value)							// Any option defined?
			$Tag->addClass($pg->Options->value);
		$this->ctx->setEmbed('<li>', '</li>');			// Enclose child items with <li>
		$this->ctx->setClosetag($Tag->getCloseTag());	// Close this depth with </ul>
		return($Tag->getOpenTag());						// Start with <ul>
	}
}

The renderer for the UiKit UL List item is pretty similar to the UiKit Grid, but obviously uses a <ul> tag and defines to encapsulate everything in <li> tags:

    public function render(Page $pg) {
        $Tag = $this->ctx->getTag('ul');                // Get manager for <ul> tag
        $Tag->addClass('uk-list');                      // Prepare for UiKit
        if($pg->Options->value)                         // Any option defined?
            $Tag->addClass($pg->Options->value);
        $this->ctx->setEmbed('<li>', '</li>');          // Enclose child items with <li>
        $this->ctx->setClosetag($Tag->getCloseTag());   // Close this depth with </ul>
        return($Tag->getOpenTag());                     // Start with <ul>
    }

Since this is pretty straightforward, let's better have a look at the ScrollSpy.

plugins/ContentRenderer/FlexUkScrollSpy.inc

plugins/ContentRenderer/FlexUkScrollSpy.inc

<?php namespace ProcessWire;
/*
 * Module using DepthRenderer to generate UiKit scrollspy for nested entities
 */
class FlexUkScrollSpy extends RepeaterFlexItem {
	public static function getModuleInfo() {
		return array(
			'title' => __('Uikit ScrollSpy', __FILE__), // Module Title
			'version' => 1,
			'renderer' => 'ContentRenderer',
			'head' => '{flex_label} [• {Options.title},  Repeat {Checkbox}, {TextOnly}]',
			'icon' => 'fa fa-user-secret',
			);
	}
	public static function getFields() {
		return([
			'@Options' => RepeaterFlexItem::Options([
					'columnWidth' => 25,
					'label' => __('Animation'),
					'inputfieldClass' => 'InputfieldSelect',
					'initValue' => 1,
					'required' => 1,
					'columnWidth' => 25,
					'options' => [
						1 => 'uk-animation-fade|'.__('fade'),
						2 => 'uk-animation-scale-up|'.__('fade and scale up'),
						3 => 'uk-animation-scale-down|'.__('fade and scale down'),
						4 => 'uk-animation-slide-top|'.__('slide from top'),
						5 => 'uk-animation-slide-bottom|'.__('slide from bottom'),
						6 => 'uk-animation-slide-left|'.__('slide from left'),
						7 => 'uk-animation-slide-right|'.__('slide from right'),
						8 => 'uk-animation-slide-top-small|'.__('slide little from top'),
						9 => 'uk-animation-slide-bottom-small|'.__('slide little from bottom'),
						10 => 'uk-animation-slide-left-small|'.__('slide little from left'),
						11 => 'uk-animation-slide-right-small|'.__('slide little from right'),
						12 => 'uk-animation-slide-top-medium|'.__('slide medium from top'),
						13 => 'uk-animation-slide-bottom-medium|'.__('slide medium from bottom'),
						14 => 'uk-animation-slide-left-medium|'.__('slide medium from left'),
						15 => 'uk-animation-slide-right-medium|'.__('slide medium from right'),
						16 => 'uk-animation-kenburns|'.__('Ken Burns'),
						17 => 'uk-animation-shake|'.__('shake'),
						],
				]),
		'@Checkbox' => RepeaterFlexItem::Checkbox([
				'label' => __('Repeat animation'),
				'columnWidth' => 25,
				'checkboxLabel' => '',
 			]),
		'@TextOnly' => RepeaterFlexItem::TextOnly([
				'label' => __('UiKit uk-scrollspy attributes'),
				'notes' => __("hidden:true|false\noffset-top:TopOffsetValue\noffset-left:LeftOffsetValue\ndelay:DelayTime"),
				'columnWidth' => 50,
			]),
		]);
		}
	public function render(Page $pg) {
		$SpyAtts = "cls:{$pg->Options->value};";		// Get animation
		if($pg->Checkbox) $SpyAtts .= " repeat:true;";	// Add repeat, if requested
		if($pg->TextOnly != "")							// Just append any additional parameters
			$SpyAtts .= " {$pg->TextOnly}";
			
		// UiKit ScrollSpy needs to add its attributes to embedded fields
		$this->ctx->setEmbedAttributes(['uk-scrollspy' => $SpyAtts]);
		return('');	// We do not create any output
		}
}

The ScrollSpy differs in that respect, that it does not generate any output:

    public function render(Page $pg) {
        $SpyAtts = "cls:{$pg->Options->value};";        // Get animation
        if($pg->Checkbox) $SpyAtts .= " repeat:true;";  // Add repeat, if requested
        if($pg->TextOnly != "")                         // Just append any additional parameters
            $SpyAtts .= " {$pg->TextOnly}";
            
        // UiKit ScrollSpy needs to add its attributes to embedded fields
        $this->ctx->setEmbedAttributes(['uk-scrollspy' => $SpyAtts]);
        return('');    // We do not create any output
        }

In contrast it registers attributes for any higher level (next depth) item: $this->ctx->setEmbedAttributes(['uk-scrollspy' => $SpyAtts])

Since the ScrollSpy attributes need to be injected into the tags of the child items, this only works if the child elements consequently use the tag manager. For this reason there is a special UiKit Bodytext plugin available.

plugins/ContentRenderer/FlexUkBodytext.inc

plugins/ContentRenderer/FlexUkBodytext.inc

<?php namespace ProcessWire;
/*
 * Plugin to output a textarea for the ContentRenderer.
 * This plugin only checks for defined classes/attributes
 * which need to be included and encapsulate the textarea
 * in a <div>.
 */
class FlexUkBodytext extends RepeaterFlexItem {
	public static function getModuleInfo() {
		return array(
			'title' => __('Uikit Bodytext', __FILE__), // Module Title
			'version' => 1,
			'renderer' => 'ContentRenderer',
			'icon' => 'fa fa-file-text-o',
			);
	}
	public static function getFields() {
		return([
			'@TextAreaMarkup' => RepeaterFlexItem::TextAreaMarkup(),
		]);
		}
	public function textindex(Page $pg, $textIndex)
		{
		$textIndex->index($pg->TextAreaMarkup);
		}
	public function render(Page $pg) {
		if(($this->ctx->getClassesCount() > 0)		// Any class/attribute defined?
		|| ($this->ctx->getAttributesCount() > 0))
			{	// Yes, we need to encapsulate the textarea into a <div>
			$Tag = $this->ctx->getTag('div');
			return($Tag->getOpenTag() . $pg->TextArea . $Tag->getCloseTag());
			}
		return($pg->TextAreaMarkup);	// No attibutes/classes, just output
	}
}

To avoid generation of unnecessary <div> tags, we only encapsulate the textarea if there are classes or attributes defined for this depth, otherwise we simply return the TextArea:

    public function render(Page $pg) {
        if(($this->ctx->getClassesCount() > 0)
        || ($this->ctx->getAttributesCount() > 0))
            {
            $Tag = $this->ctx->getTag('div');
            return($Tag->getOpenTag() . $pg->TextArea . $Tag->getCloseTag());
            }
        return($pg->TextAreaMarkup);
    }

 

prepared in 280ms (content 243ms, header 0ms, Menu 25ms, Footer 11ms)