Multi-column CListView

CListView widget is a great piece of code for quickly rendering lists of items that:

  • are based on your own provided view for each item (and thus let you customize final view in nearly every aspect) and
  • are providing fully functional paginator in the same time.

However, they lack support for rendering items in more than one column. This article shows one of many workarounds to fix this problem.

Before writing my own text I found this forum thread and this article. None of provided solutions satisfied me. Upon further searching, I found this blog post. It is written in Russian, provided code is poorly written and given solution is unfinished. Thus, I have translated it into English, fixed all the code issues and provided fully working solution.

This approach assumes, that you create your own widget, named ListView, that extends base CListView.

Code for this widget could by anything like that:

Yii::import('zii.widgets.CListView');

class ListView extends CListView
{
    public $columns = array('leftblock', 'midblock', 'rightblock');

    public function renderItems()
    {
        $y = 0;
        $owner = $this->getOwner();
        $columns = sizeof($this->columns);
        $render = $owner instanceof CController ? 'renderPartial' : 'render';

        if($columns > 0)
        {
            foreach ($this->columns as $column)
            {
                echo CHtml::openTag('div', array('class' => $column))."n";

                $data = $this->dataProvider->getData();

                if(count($data) > 0)
                {
                    foreach($data as $i => $item)
                    {
                        if( ($i+ ($columns - $y)) % $columns == 0)
                        {
                            $data = $this->viewData;

                            $data['index'] = $i;
                            $data['data'] = $item;
                            $data['widget'] = $this;

                            $owner->$render($this->itemView, $data);
                        }
                    }
                }
                else $this->renderEmptyText();

                echo CHtml::closeTag('div')."n";

                $y++;
            }
        }
    }
}

You should save this file to a location of your choice (/protected/extenders/ListView.php in my case) and change your code, where you render CListView widget to use application.extenders.ListView widget instead (correct path, if you saved above code in different location).

After that you should add one extra property to widget configuration array: 'columns'=>array('listview-column', 'listview-column', 'listview-column'). Where number of items in this array denotes number of columns, to which you want to divide your entire items set and names of each item in array denotes class names that will be added to each column.

The entire code for this widget could look like that:

$this->widget('application.extenders.ListView', array
(
    'dataProvider'=>$dataProvider,
    'itemsTagName'=>'div',
    'itemView'=>'list_base',
    'template'=>"{items}n{pager}",
    'itemsCssClass'=>'contents-list',
    'columns'=>array('listview-column', 'listview-column', 'listview-column')
));

When using above code, your list view will render all items (per each page) in three columns and each column will have listview-column class.

To make entire solution work, you also have to provide proper styling for listview-column class (or any other name, that you used). You can add it in your own style sheet (preferred way) or directly in your view, above widget call (lazy way):

<style>
.listview-column {
    width: 33%;
    float: left;
}
</style>

Adjust width: 33%; to width: 25%;, if you are using four columns, to width: 50%;, if you’re using two columns or anything like that. You may of course use also a fixed value (i.e. width: 300px;), if your layout requires this.

You can use same class for each column (like in above example), when you don’t need to add special styling, different to each column. If you do, you can name each column differently (like in original blog post), i.e.: 'columns'=>array("one", "two", "three", "four"),. However, I found this option not necessary and producing strange formatting effects (see below).

That would be all for solution mentioned in this article. If you’re interested, then here is a list of changes I made in my code toward original post:

  1. Author uses if block (if(sizeof($this->columns))) and else without inner brackets, which is a bad practice, because it makes reading and debugging code very nasty. I’ve added all missing brackets and indentation.
  2. Author reads value of $columns, $owner and $render variables inside foreach loop, which makes them being executed within each iteration of this loop and makes script performance lower. I have moved these three variables to the beginning of widget code.
  3. Author uses if(sizeof($this->columns)), where he expect to have true/false value (required by if), but is in the same time reading it from function, that returns integer value (sizeof()). It is known, that integer value > 0 is treated by PHP as true, while value = 0 is treated as false. But writing code, that relies on such internal variable types conversions is very bad, because prevents quick understanding and debugging and is prone to making mistakes.

Aside of above, I also noticed, that original widget renders a strange result (items in each column and each column itself overlapping on previous one), if naming each column with different class, like in original example. I solved this problem, by using the same class name for each column. However, since I’m a complete newbie to CSS, I don’t know, if this is caused by HTML code generated by original widget or is this something in my own layout.

I have also removed adding column class inside original widget. It is not required for this solution to work and name, that you used is too common and may introduce some strange results, when list view will acquire some external styling for column class, not mentioned for this view.

Leave a Reply