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:
RepeaterFlex may not only be used for page content, rendering sitemaps or menus work nice as well.
Item configurations may be transported easily between installations.
Output is generated from a simple call to the render method, no iteration of the matrix required.
Item types may easily be added or removed from the matrix field.
Field definition and render code for each item is defined in a single, compact file.
Due to the concept modifying item behaviour or adding new items can be done easily.
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.
<?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.
<?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.
<?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.
<?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);
}