Menu Renderer

With the RepeaterFlex you may not only manage your content but define your site menu as well. The approach used with the MenuRenderer separates menu definition from its markup generation and behaves more like base class. When you look at the code, it simply outputs headers and links, nothing similar to the menu at the head of this site.

render/MenuRenderer.inc

<?php namespace ProcessWire;
include_once('DepthRenderer.inc');
/*
 * Menu Renderer for RepeaterFlex
 *
 * This baseclass provides an item API to create menus. Methods provided
 * include main menu titles, submenus, creating link markup and generation
 * mobile menu.
 *
 * This implementation outputs rudimentary text markup only and may be used for testing.
 */
class MenuRenderer extends DepthRenderer {
	public static function getModuleInfo() { return [
		'title' => __('Menu Renderer', __FILE__), // Module Title
		'summary' => __('Renders RepeaterFlex with depth information for a Menu.', __FILE__), // Module Summary
		'version' => 80,
		];
	}
	protected $bForOffCanvas = false;
	public function setForOffCanvas($bForOffCanvas) { $this->bForOffCanvas = $bForOffCanvas; }
	public function getForOffCanvas() { return($this->bForOffCanvas); }
	public function getSeparator() { return('<hr/>'); }
	public function getWrap() { return('<br/>'); }
	public function getMainTitle($TextLine, $directLink='') { return("<h2>{$TextLine}</h2>"); }
	public function getItemTitle($TextLine) { return("{$TextLine}<br/>"); }
	public function getExpandedAttributes($targetOrAttributeArray)
		{
		if(!is_array($targetOrAttributeArray))
			{
			if($targetOrAttributeArray == '')
				return('');
			$targetOrAttributeArray = [ 'target' => $targetOrAttributeArray ];
			}
		$atts = [];
		foreach($targetOrAttributeArray as $k => $v)
			$atts[] = "{$k}='{$v}'";
		return(implode(' ', $atts));
		}
	public function getLink($title, $url, $targetOrAttributeArray='_blank')
		{
		$atts = $this->getExpandedAttributes($targetOrAttributeArray);
		return("<a href='{$url}' {$atts}>{$title}</a>");
		}
	public function getPageLink(Page $pg, $bEnumerateChildren=false, $bIsHeadline=false) {
		if($pg->id) return($this->getLink($pg->title, $pg->url, '', $bForOffCanvas));
		return('');
	}
}

But this implementation is sufficient enough to implement various plugins to define a menu, which may look like this in admin:

To create a real menu a specialized MenuRenderer is required, like the one used on this site, which creates markup for UiKit3 framework from the menu items. You may even implement a specialization for bootstrap or completely do your own, but this has no influence on the menu definition itself. In fact you may simply switch from UiKit to bootstrap markup by just selecting a different renderer for your flex menu field.

You may have noticed the $bForOffCanvas member which may be set to render a menu for mobile devices instead of a regular menu. This flag is controlled from your template code and evaluated from the specialization of the selected MenuRenderer. This may need additional parameters which will show in the field's configuration:

Parameters of the first line are used from the login menu plugin which renders a complete context menu when logged in, otherwise it will redirect you to the specified login page. The UiKit menu renderer only need an ID for the menu and if a mobile version should be rendered as well.

The actual code of the UiKit menu renderer looks like this:

render/UkMenuRenderer.inc

<?php namespace ProcessWire;
include_once('MenuRenderer.inc');
/*
 * Menu Renderer for RepeaterFlex using UiKit3
 *
 * This variant of a Menu Renderer generates markup for UiKit3 menus.
 */
class UkMenuRenderer extends MenuRenderer {
	public static function getModuleInfo() { return [
		'title' => __('Menu Renderer for UiKit', __FILE__), // Module Title
		'summary' => __('Renders RepeaterFlex with depth information for a UiKit Menu.', __FILE__), // Module Summary
		'version' => 1,
		];
	}

	public static function getConfigFields() { return [
		'menuid' => [
				'type' => 'InputfieldText',
				'label' => __('Menu ID'),
//				'required' => 1,
				'columnWidth' => 50,
			],
		'collapsemobile' => [
				'type' => 'InputfieldCheckbox',
				'label' => __('Collapse on small devices'),
//				'required' => 1,
				'columnWidth' => 50,
			],
		]; }

	private $menuID = '';
	protected $appendMarkup = '';

	public function renderPrefix() {
		$menuAppend = '';
		if(isset($this->menuid))
			$this->menuID = $this->menuid;
		else
			$this->menuID = 'menu'.wire('page')->id;	// use some default menu id if none defined
		$this->offCanvas = '';
		if($this->bForOffCanvas)	// Generate markup for mobile devices?
			return("
<div id='{$this->menuID}-oc' uk-offcanvas='mode: push; overlay: true' class='uk-offcanvas'>
 <div class='uk-offcanvas-bar'>
   <ul class='uk-nav-default uk-nav-parent-icon' uk-nav>
");

		return( "
 <div id='{$this->menuID}' uk-sticky='media: 960; sel-target: .uk-navbar-container; cls-active: uk-navbar-sticky' class='uk-box-shadow-medium'>
 <nav class='uk-navbar-container' uk-navbar>
"); }
	public function renderSuffix() {
		if($this->bForOffCanvas)
			return( "
    </ul>
 </div>
</div>
");
		return('
 </nav>
</div>
'.$this->appendMarkup); }

	public function menuAppend(string $markup) { $this->appendMarkup .= $markup; }
	public function getMenuID() { return($this->menuID); }
	public function getMainTitle($TextLine, $directLink='')
		{
		if(!$directLink)
			$directLink = '#';
		if($this->bForOffCanvas)
			{
			$this->setCloseTag("</ul></li>");
			return("<li class='uk-parent'><a href='{$directLink}'>{$TextLine}</a><ul class='uk-nav-sub'>");
			}
		$this->setCloseTag("</ul></div></div></div></li>");
		$atts = '';
		if($this->collapsemobile)
			$atts = ' class="uk-visible@m"';
		return("<li{$atts}><a href='{$directLink}'>{$TextLine}</a><div class='uk-width-large' uk-dropdown><div class='uk-dropdown-grid' uk-grid><div class='uk-width-1-1'><ul class='uk-dropdown-nav uk-nav-parent-icon' uk-nav>");
		}
	public function getLink($title, $url, $targetOrAttributeArray='_blank')
		{
		$atts = $this->getExpandedAttributes($targetOrAttributeArray);
//		if($this->bForOffCanvas)
//			return("<a href='{$url}' $atts>{$title}</a><br/>");
		return("<li><a href='{$url}' $atts>{$title}</a></li>");
		}

	public function getWrap()
		{
		if($this->bForOffCanvas)
			return("<hr/>");
		return("</ul></div><div><ul class='uk-nav uk-dropdown-nav'>");
		}
	public function getItemTitle($TextLine)
		{
		return(" <li class='uk-nav-header'>{$TextLine}</li>");
		}
	public function getSeparator()
		{
		return(" <li class='uk-nav-divider'></li>");
		}
	public function getPageLink(Page $pg, $bEnumerateChildren=false, $bIsHeadline=false) {
		$out = '';
		if($bIsHeadline)
			$targetAtt = [ 'style' => 'font-weight:bold;' ];
		else
			$targetAtt = '';
		$sel = '';
		if(wire('user')->isSuperuser())
			$sel = 'include=hidden';
		if($pg->id)
			{
			if($bEnumerateChildren && (count($pg->children()) > 0))
				{
				if($this->bForOffCanvas)
					{
					$out = $this->getLink($pg->title, $pg->url, $targetAtt, $this->bForOffCanvas);
					}
				else
					{
					$out .= "<li class='uk-parent'><a href='#'>{$pg->title}</a><ul class='uk-nav-sub'>";
					$out .= $this->getPageLink($pg, false);
					foreach($pg->children($sel) as $child)
						{
						$out .= $this->getPageLink($child, $bEnumerateChildren);
						}
					$out .= "</ul></li>";
					}
				}
			else
				$out = $this->getLink($pg->title, $pg->url, $targetAtt, $this->bForOffCanvas, $bIsHeadline);
			}
		return($out);
		}
	}

You may have noticed the two methods renderPrefix and renderSuffix. The markup returned from these methods is prepended, resp. appended to the markup collected during repeater enumeration. Rendering such a menu returns the complete markup, ready for insertion right behind the opening <body> tag.

I tend to place the navigation in the home template, along with other global settings, so rendering the menu bar looks like this:

$home = $pages->get("/");
$fm = $home->flex_menu;
echo $fm->render();

In case you've checked the Collapse on small devices checkbox, you also need to insert the off-canvas version of the menu right before the closing </body> tag. Since there only is a single render method, you'll first have to instruct the renderer to switch to an alternate output. The MenuRenderer provides a method setForOffCanvas for this purpose, so there are two more lines:

$fm->getContext()->setForOffCanvas(true);
echo $fm->render();

 

prepared in 60ms (content 27ms, header 0ms, Menu 21ms, Footer 10ms)