Yii 1.1: Dynamic Sidebar using CClipWidget

20 followers

The Problem

Most applications will have one or two sidebars and often you want to control the content that should appear in the sidebar based on the action. For example you will want to show something different on the homepage as opposed to some view page. While achieving this you generally also want to avoid putting the layout into each view, as that would mean if you wanted to change the layout (e.g. put the sidebar on the left instead of the right or do some funky layout which requires an additional div tag to be added) you'd have to go through every view, which does not achieve good code re-use. It is also bad practice to have lots of if statements in column2 layout just so that you can generate the sidebar correctly.

The Solution

The best option is to generate the sidebar content as part of your view, but instead of rendering it, capture what is generated for the sidebar, so that it can be rendered in the column2 layout.

There are a couple of ways to achieve this, but the first is preferred.

CClipWidget

To achieve the desired outcome with a dynamic sidebar all we need to do is use the CClipWidget and in the view capture the sidebar output into a clip and render it in the column2 layout.

To achieve this, firstly change your column2.php file to have something along these lines...

<div class="container">
  <div class="span-19">
    <div id="content">
      <?php echo $content; ?>
    </div><!-- content -->
  </div>
  <div class="span-5 last">
    <div id="sidebar">
      <?php echo $this->clips['sidebar']; ?>
    </div><!-- sidebar -->
  </div>
</div>

Notice what is being rendered in the sidebar content.

Then, in each your views just use the following:

<?php $this->beginWidget('system.web.widgets.CClipWidget', array('id'=>'sidebar')); ?>
  <div>Some non-common output</div>
  <?php $this->widget('application.components.widgets.SomeReusableWidget'); ?>
  <div>Some more non-common output</div>
<?php $this->endWidget();?>

This will solve the problem in the best possible way.

Your Own Widget

You could create your own widget to capture content specifically for the sidebar and then render it out in the column2 layout, but why go to all this trouble when you can just use CClipWidget.

Output buffering

You could use raw output buffering and add a sidebar variable into the controller, but once again there's not really a need for it as that's what CClipWidget is already doing for you under the covers. If you do want to do it this way (for some weird and unknown reasons) then this is how you do it.

Add the following into your Controller class (from which all other controllers inherit).

public $sidebar;

Then in column2.php layout file use the following:

<div class="container">
  <div class="span-19">
    <div id="content">
      <?php echo $content; ?>
    </div><!-- content -->
  </div>
  <div class="span-5 last">
    <div id="sidebar">
      <?php echo $this->sidebar; ?>
    </div><!-- sidebar -->
  </div>
</div>

Then in each view just do the following:

<?php ob_start(); ?>
  <div>Some non-common output</div>
  <?php $this->widget('application.components.widgets.SomeReusableWidget'); ?>
  <div>Some more non-common output</div>
<?php $this->sidebar = ob_get_clean(); ?>

Whichever of these ways you choose to do it, it's better than lots of if statements in your column2.php layout file or repeating the layout in each view file!

Total 5 comments

#15684 report it
bschooly at 2013/12/06 08:03pm
Bad HTML

What out for the example. Notice the non closed divs! That will cause problems with the footer:

<?php ob_start(); ?>
  <div>Some non-common output<div>
  <?php $this->widget('application.components.widgets.SomeReusableWidget'); ?>
  <div>Some more non-common output<div>
<?php $this->sidebar = ob_get_clean(); ?>
#10547 report it
Cozumel at 2012/11/03 09:22pm
Awesome!

So simple and elegant, I just ran into this issue and knew there'd be something like this. Yii is amazing!

#6551 report it
Peniel at 2012/01/18 09:19am
Very flexiable solution

I want to create some like you mentioned above, It works great, if has some user interface it's very similar to the drupal's region block concepts.

The bellowing two options are very useful.

In the controller:

$this->beginWidget('system.web.widgets.CClipWidget', array('id'=>'shoppingcart'));
        $this->widget('application.components.cart');
        $this->endWidget();

or

Layout::addBlock('sidebar.right', array(
                'id' => 'right_sidebar',
                'content' => $this->renderPartial('right', array(
                        'controller' => $this,
                                ), true)
        ));

And in the layout file

<?php echo $this->clips['shoppingCart']; ?>

or

<?php if (Layout::hasRegion('sidebar.right')): ?>
                                <!-- sidebar-right -->
                                <div id="sidebar-right" class="sidebar">
                                    <div class="sidebar-content">
                                        <?php Layout::renderRegion('sidebar.right'); ?>
                                    </div>
                                </div>
                                <!-- /sidebar-right -->
                            <?php endif; ?>

I prefer the later approach to this issue as it is more intuitive.

#4566 report it
Ivica at 2011/07/21 12:40am
First solution is much better

Idea with clips is much better, then using controller public attribute, because it is more flexible, and you will less repeat same code in your controllers.

In my case, I should have shopping cart info in header, that should appear on all pages, except on pages in checkout controller.

So, in header I just added code:

<?php echo $this->clips['shoppingCart']; ?>

And then I called widgets in controllers(not in views, like Sheldmandu suggested, which is also possible):

$this->beginWidget('system.web.widgets.CClipWidget', array('id'=>'shoppingCart'));
 
$this->widget('application.components.widgets.cart');            
 
$this->endWidget();
#2451 report it
jayrulez at 2011/01/04 09:04am
My solution

I made a solution for this issue sometime last year. I will share my solution here.

What i did was create static helper classes for the layout as follows.

<?php
 
class LayoutBlock
{
    private $_id;
 
    private $_content;
 
    private $_weight;
 
    private $_visible;
 
    private $_htmlOptions = array();
 
    private $_tagName;
 
    private $_defaultHtmlOptions = array(
        'class'=>'block',
    );
 
    const DEFAULT_BLOCK_TAG = 'div';
 
    /**
     *
     */
    public function __construct($id, $content, $weight = 0, $visible = true, $htmlOptions = array(), $tagName = self::DEFAULT_BLOCK_TAG)
    {
        $this->setId($id);
        $this->setContent($content);
        $this->setWeight($weight);
        $this->setVisible($visible);
        $this->setHtmlOptions($htmlOptions);
        $this->setTagName($tagName);
    }
 
    /**
     *
     */ 
    public function getId()
    {
        return $this->_id;
    }
 
    /**
     *
     */ 
    public function getContent()
    {
        return $this->_content;
    }
 
    /**
     *
     */ 
    public function getWeight()
    {
        return $this->_weight;
    }
 
    /**
     *
     */ 
    public function getVisible()
    {
        return $this->_visible;
    }
 
    /**
     *
     */ 
    public function getTagName()
    {
        return $this->_tagName;
    }
 
    /**
     *
     */ 
    public function setId($id)
    {
        if(!is_string($id))
        {
            throw new CException(Yii::t('application','The block id must be a string.'));
        }
        $this->_id = $id;
        return $this;
    }
 
    /**
     *
     */ 
    public function setContent($content)
    {
        if(!is_string($content))
        {
            throw new CException(Yii::t('application','The block content must be a string.'));
        }
        $this->_content = $content;
        return $this;
    }
 
    /**
     *
     */ 
    public function setWeight($weight)
    {
        if(!is_numeric($weight))
        {
            throw new CException(Yii::t('application','The block weight must be a number.'));
        }
        $this->_weight = (int)$weight;
        return $this;
    }
 
    /**
     *
     */ 
    public function setHtmlOptions($htmlOptions, $mergeOld = false)
    {
        if(!is_array($htmlOptions))
        {
            throw new CException(Yii::t('application','The block html options must be a number.'));
        }
        if($mergeOld)
        {
            $this->_htmlOptions = CMap::mergeArray($this->_htmlOptions, $htmlOptions);
        }else{
            $this->_htmlOptions = $htmlOptions;
        }
        return $this;
    }
 
    /**
     *
     */ 
    public function setTagName($tagName)
    {
        if(!is_string($tagName))
        {
            throw new CException(Yii::t('application','The block tag name must be a string.'));
        }
        $this->_tagName = $tagName;
        return $this;
    }
 
    /**
     *
     */ 
    public function setVisible($visible)
    {
        if(!is_bool($visible))
        {
            throw new CException(Yii::t('application','The block visibility must be set to a boolean value.'));
        }
        $this->_visible = $visible;
        return $this;
    }
 
    /**
     *
     */ 
    public function render($return = false, $renderTag = true)
    {
        $block = $this->_content;
        if($renderTag)
        {
            $block = CHtml::openTag($this->_tagName, CMap::mergeArray($this->_defaultHtmlOptions, $this->_htmlOptions)).$block.CHtml::closeTag($this->_tagName);
        }
        if($return === true)
        {
            return $block;
        }else{
            echo $block;
        }
    }
}
 
class Layout
{   
    /**
     *
     */
    private static $_instance;
 
    /**
     *
     */ 
    protected $regions;
 
    /**
     *
     */ 
    public function __construct()
    {
        $this->regions = new CMap;
    }
 
    /**
     *
     */ 
    public static function getInstance()
    {
        if(self::$_instance === null)
        {
            self::$_instance = new self;
        }
        return self::$_instance;
    }
 
    /**
     *
     */ 
    protected static function compareBlocks($blockA, $blockB)
    {
        if($blockA instanceof LayoutBlock && $blockB instanceof LayoutBlock)
        {
            if($blockA->getWeight() === $blockB->getWeight())
            {
                return 0;
            }
            return ($blockA->getWeight() <= $blockB->getWeight()) ? -1 : 1;
        }else{
            throw new CException(Yii::t('application','Both blocks must be instances of LayoutBlock or one of it\'s children.'));
        }
    }
 
    /**
     *
     */ 
    protected static function sortBlocks($blocks)
    {       
        $blocks = $blocks->toArray();
 
        uasort($blocks, array(__CLASS__,'compareBlocks'));
 
        return new CMap($blocks);
    }
 
    /**
     *
     */ 
    public function getBlocks($regionId, $visibleOnly = true)
    {
        $instance = self::getInstance();
 
        $blocks   = new CMap;
 
        if($instance->regions->contains($regionId))
        {           
            foreach($instance->regions[$regionId] as $blockId => $block)
            {
                if($visibleOnly)
                {
                    if($block->getVisible() === false)
                    {
                        continue;
                    }
                }
                $blocks->add($blockId, $block);
            }
        }
 
        return self::sortBlocks($blocks);
    }
 
    /**
     *
     */ 
    public static function addBlock($regionId, $blockData)
    {
        if(isset($blockData['id']))
        {
            $instance    = self::getInstance();
 
            $blockId     = $blockData['id'];
            $content     = $blockData['content'];
 
            $weight      = isset($blockData['weight'])      ? $blockData['weight']      : 0;
            $visible     = isset($blockData['visible'])     ? $blockData['visible']     : true;
            $htmlOptions = isset($blockData['htmlOptions']) ? $blockData['htmlOptions'] : array();
            $tagName     = isset($blockData['tagName'])     ? $blockData['tagName']     : LayoutBlock::DEFAULT_BLOCK_TAG;
 
            $block       = new LayoutBlock($blockId, $content, $weight, $visible, $htmlOptions);
 
            if(!$instance->regions->contains($regionId))
            {
                $instance->regions[$regionId] = new CMap;
            }
            $instance->regions[$regionId]->add($blockId, $block);
        }else{
            throw new CException(Yii::t('application','A block must have at least an id.'));
        }
    }
 
    /**
     *
     */ 
    public static function addBlocks($blocks = array())
    {
        foreach($blocks as $blockData)
        {
            if(isset($blockData['regionId']))
            {
                $regionId = $blockData['regionId'];
                unset($blockData['regionId']);
                self::addBlock($regionId, $blockData);
            }
        }
    }
 
    /**
     *
     */ 
    public static function getBlock($regionId, $blockId)
    {
        $instance = self::getInstance();
        if(($region = self::getRegion($regionId)) !== null)
        {
            if($region->contains($blockId))
            {
                return $region[$blockId];
            }
        }
        return null;
    }
 
    /**
     *
     */ 
    public static function hasBlock($regionId, $blockId)
    {
        return self::getBlock($regionId, $blockId) !== null;
    }
 
    /**
     *
     */ 
    public static function removeBlock($regionId, $blockId)
    {
        if(($region = self::getRegion($regionId)) !== null)
        {
            if($region->contains($blockId))
            {
                $region->remove($blockId);
            }
        }
    }
 
    /**
     *
     */ 
    public static function getRegion($regionId)
    {
        $instance = self::getInstance();
        return $instance->regions->contains($regionId) ? $instance->regions[$regionId] : null;
    }
 
    /**
     *
     */ 
    public static function hasRegion($regionId)
    {
        return self::getRegion($regionId) !== null;
    }
 
    /**
     *
     */ 
    public static function hasRegions()
    {
        $args = func_get_args();
        if(count($args))
        {
            foreach($args as $regionId)
            {
                if(!self::hasRegion($regionId))
                {
                    return false;
                }
            }
            return true;
        }
        throw new CException(Yii::t('application','No region id was specified.'));
    }
 
    /**
     *
     */ 
    public static function renderRegion($regionId, $return = false)
    {
        $regionContent = '';
 
        if(self::hasRegion($regionId))
        {
            $blocks = self::getBlocks($regionId);
 
            foreach($blocks as $block)
            {
                if($block instanceof LayoutBlock)
                {
                    $regionContent .= $block->render(true);
                }
            }
        }
 
        if($return)
        {
            return $regionContent;
        }else{
            echo $regionContent;
        }
    }
 
    /**
     *
     */ 
    public static function removeRegion($regionId)
    {
        $instance = self::getInstance();
 
        if($instance->regions->contains($regionId))
        {
            $instance->regions->remove($regionId);
        }
    }
}

And practical usage example

layout file

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo Yii::app()->getLocale()->getId(); ?>" dir="<?php echo Yii::app()->getLocale()->getOrientation(); ?>">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=<?php echo Yii::app()->charset; ?>" />
<meta name="language" content="<?php echo Yii::app()->getLocale()->getId(); ?>" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->getBaseUrl(); ?>/css/common.css" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->getBaseUrl(); ?>/css/layout.css" />
<title><?php echo CHtml::encode($this->pageTitle); ?></title>
<!--[if lt IE 8]>
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->getBaseUrl(); ?>/css/ie.css" />
<![endif]-->
</head>
<body>
    <div id="page-container">
        <div id="page">
            <div id="header-container">
                <!-- header -->
                <div id="header">
                <?php //$this->widget('HeaderWidget'); ?>
                </div>
                <!-- /header -->
            </div>
 
            <div id="canvas-container">
                <!-- canvas -->
                <div id="canvas">
                    <?php
                    if(Layout::hasRegions('sidebar.left','sidebar.right'))
                    {
                        $tagClass = ' class="sidebars clearfix"';
                    }else if(Layout::hasRegions('sidebar.left'))
                    {
                        $tagClass = ' class="sidebar-left clearfix"';
                    }else if(Layout::hasRegions('sidebar.right'))
                    {
                        $tagClass = ' class="sidebar-right clearfix"';
                    }else{
                        $tagClass = '';
                    }
                    ?>
                    <div id="canvas-content"<?php echo $tagClass; ?>>
 
                        <?php if(Layout::hasRegion('sidebar.left')): ?>
                        <!-- sidebar-left -->
                        <div id="sidebar-left" class="sidebar">
                            <div class="sidebar-content">
                                <?php Layout::renderRegion('sidebar.left'); ?>
                            </div>
                        </div>
                        <!-- /sidebar-left -->
                        <?php endif; ?>
 
                        <div id="content-container">
                            <!-- content -->
                            <div id="content">
                                <?php echo $content; ?>
                            </div>
                            <!-- /content -->
                        </div>
 
                        <?php if(Layout::hasRegion('sidebar.right')): ?>
                        <!-- sidebar-right -->
                        <div id="sidebar-right" class="sidebar">
                            <div class="sidebar-content">
                                <?php Layout::renderRegion('sidebar.right'); ?>
                            </div>
                        </div>
                        <!-- /sidebar-right -->
                        <?php endif; ?>
 
                    </div>
                </div>
                <!-- /canvas -->
            </div>
 
            <div id="footer-container">
                <!-- footer -->
                <div id="footer">
                    <?php //$this->widget('FooterWidget'); ?>
                </div>
                <!-- /footer -->
            </div>
        </div>
    </div>
</body>
</html>

And in the view file

<?php
$this->pageTitle=Yii::t('application', 'All aspects');
?>
<div class="action" id="aspect-index">
    <div class="action-content">
        <!--post-form-->
        <?php //$this->widget('PostFormWidget'); ?>
        <!--/post-form-->
    </div>
</div>
 
<?php Layout::addBlock('sidebar.right', array(
    'id'=>'right_sidebar',
    'content'=>'the content you want to add to your layout', // eg the result of a partial or widget
        /*
$this->renderPartial('/partial/aspect_index_right', array(
        'aspects'=>$user->aspects,
        'controller'=>$this,
    ), true)
*/
)); ?>

Leave a comment

Please to leave your comment.

Write new article