Yii 1.1: CGridView: Render customized/complex datacolumns

82 followers

If you have to display a overview of related data in a table, you can use the CListView, create your view with table/tr/td tags and implement db-queries in the view to display detaildata of the current (master-)record.

But the CGridView offers the possibility to add custom columns too. In the class reference of the CGridView you will find this example.

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array
    (
        ...
        array(            // display 'create_time' using an expression
            'name'=>'create_time',
            'value'=>'date("M j, Y", $data->create_time)',
        ),
        array(            // display 'author.username' using an expression
            'name'=>'authorName',
            'value'=>'$data->author->username',
        ),
        array(            // display a column with "view", "update" and "delete" buttons
            'class'=>'CButtonColumn',
        ),
    ),
));

Because Yii checks the 'value' of a column definition by using CComponent.evaluateExpression you can assign a PHP expression string - that's great.

If you take a look at the source of CComponent.evaluateExpression you will see, that also PHP call_user_func_array() is supported - and that's very great.

You can add a column that displays data generated by a object method, for example a method of your controller.

All you have to do is to assign the object method to the value:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        ...
        array(            
            'name'=>'newColumn',
            //call the method 'gridDataColumn' from the controller
            'value'=>array($this,'gridDataColumn'), 
        ),
        array(            
            'name'=>'Address',
            //call the method 'renderAddress' from the model
            'value'=>array($model,'renderAddress'), 
        ),
    ),
));
class MyController extends Controller 
{
    ...
 
     //called on rendering the column for each row 
     protected function gridDataColumn($data,$row)
     {
          // ... generate the output for the column
 
          // Params:
          // $data ... the current row data   
         // $row ... the row index    
         return $theCellValue;    
    }       
   ...  
 
}
class Address extends CActiveRecord 
{
    ...
 
     //called on rendering the column for each row 
     public function renderAddress($data,$row)
     {
          // ... generate the output for a full address
 
          // Params:
          // $data ... the current row data   
         // $row ... the row index    
         return $theCellValue;    
    }       
   ...  
 
}

It's easier to handle/manage the cell value assigned by a method than to implement a PHP expression string. If you want you can use renderPartial to maintain the datacolumn.

Example

Assume you have related tables 'product' and 'category' with a MANY_MANY relation 'product_category' and want to display a table with the product and the related categories. You have to generate the product model and your dataProvider by using 'with', 'join', 'order' ...

I don't explain the working with the database and related models here. Please take a look a the Relational Active Record tutorial if you need more information.

A) Eager loading - load all data at once in a single query

You will (internally) generate a SQL statement like

SELECT p.id,p.name, p.description, c.name as category FROM product p JOIN product_category pc ON pc.product = p.id JOIN category c ON c.id = pc.category

Your standard gridview with the columns (id not displayed) product, description category will look like this:

PRODUCT    | DESCRIPTION           | CATEGORY
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Phone
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Offer
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Apple
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Communication
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Lifestyle
----------------------------------------------

Now we want display the production information only in the first row of each product:

PRODUCT   |     DESCRIPTION         | CATEGORY
----------------------------------------------
iPhone 4S |  Dual Core A5 Chip ...  | Phone
----------------------------------------------
          |                         | Offer
----------------------------------------------
          |                         | Apple
----------------------------------------------

If you need to hide the repeated value you have to create your own GridView and override the CGridView.renderTableRow method.

Or you use the power of the 'value' property:

Create two protected methods 'gridProductName' and 'gridProductDescription' in your controller and keep the latest rendered row as private variable.

class ProductController extends Controller 
{
    $_lastProductId = null;
    ....
 
   //called on rendering a grid row
   //the first column
   //the params are $data (=the current rowdata) and $row (the row index)
    protected function gridProductName($data,$row)
    {
       return $this->_lastProductId != $data->id ? $data->name : '';            
    }
 
    //called on rendering a grid row
    //the second column
    protected function gridProductDescription($data,$row)
    { 
        if($this->_lastProductId != $data->id)
        {
            $this->_lastProductId = $data->id; //remember the last product id
            return $data->name;
        }
        else
             return '';
    }   
 
}

Add the custom columns to the gridview:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
            //call the method 'gridUniqueProductName' of the controller
            //the params extracted to the method are $data (=the current rowdata) and $row (the row index)
            'value'=>array($this,'gridProductName')
        ),
        array(            
            'name'=>'description',
            'value'=>array($this,'gridProductDescription')
        ),
 
        'category', //display the category as a default column
    ),
));

B) Lazy loading - load the categories for each product

You can load you products and select the assigned categories in an extra query for each row. I know you may think this is a bad performance, but it depends how complex your db schema is (how many joins you need) or type/size of the redundant data of the single query. See the remarks at 7. Relational Query Performance of the Relational Active Record tutorial. If you load product images from the db too, lazy loading can be the better choice.

And you can display a grid like below. The categories should be displayed as links to a page that lists all products with the specified category.

PRODUCT            |    CATEGORIES
----------------------------------------------
iPhone 4S          |  Phone
Dual Core A5 Chip  |  Offer
....               |  Apple
....               |  Communication
....               |  Lifestyle
----------------------------------------------
Motorola Milestone |  ...

The controller code:

class ProductController extends Controller 
{
    ...
 
    //return the value for the product column
    protected function gridProduct($data,$row) 
    {
      return  CHtml::encode($data->name) .'<br/>' . CHtml::encode($data->description);
    }
 
    //called on rendering a single gridview row
    protected function gridProductCategories($data,$row)
    {
      $sql = 'SELECT c.id,c.name FROM product_category pc JOIN category c ON c.id = pc.category ';
      $sql .= 'WHERE pc.product = ' . data->id; //the product id    
      $rows = Yii::app()->db->createCommand($sql)->queryAll();
 
      $result = '';
       if(!empty($rows))
        foreach ($rows as $row) 
           {
              $url = $this->createUrl('bycategory',array('category'=>$row['id']));
              $result .= CHtml::link($row['name'],$url) .'<br/>'; 
       }      
       return $result;    
    }
 
        ...
 
     public function actionByCategory($category) 
     {
           ... display a page with all products of the specified category ...
     }
 
    ... 
 
}

Render the CGridView in your view like this:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
            'type'=>'raw', //because of using html-code <br/>
            //call the controller method gridProduct for each row
            'value'=>array($this,'gridProduct'), 
        ),
        array(            
            'name'=>'category',
            'type'=>'raw', //because of using html-code
            'value'=>array($this,'gridProductCategories'), //call this controller method for each row
        ),
    ),
));

C) The fun part - render view files into a cell

In a controller method called on rendering a row of the gridview you can do more ...

You can use 'renderPartial' to manage the view of a datacell in a viewfile. For example, try to reuse the default 'view' generated by gii.

If you want to render the full address of a user in a cell you can do like this. The user has an attribute 'address', the addresses are stored in a table 'Address'

class UserController extends Controller 
{
 
  //the default admin action
  public function actionAdmin()
  {
        $model=new User('search');
 
        $model->unsetAttributes();  
        if(isset($_GET['User']))
            $model->attributes=$_GET['User'];
 
        $this->render('admin',array(
            'model'=>$model,
        ));
  }
 
  //assume you have generated the Address model with gii too
  protected function gridAddress($data,$row)
  {     
    $model = Address::model()->findByPk($data->address); //$data->address is the FK from the user table
 
   //get the view from the address CRUD controller (generated with gii)
    return $this->renderPartial('../address/view',array('model'=>$model),true); //set $return = true, don't display direct
  }
 
}

The CGridView in the view: admin.php

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
 
        ... user attributes here ...
 
        // add a column to display the full address
        array(            
            'name'=>'address',
            'type'=>'raw', //because of using html-code from the rendered view
            'value'=>array($this,'gridAddress'), //call this controller method for each row
        ),
    ),
));

Now you have rendered an embedded CDetailView inside the cells of a CGridView. You can try to render a CListView (default actionIndex of a CRUD controller) into a cell too.

Access column properties

In the examples above, I use different methods for each gridcolumn. So I didn't have to take care about the column properties.

If you want to use a single method for each column or need more information about the currently rendered column, add a third param (for example $dataColumn) to your called method:

//view
$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
            'value'=>array($this,'renderGridCell')
        ),
        array(            
            'name'=>'description',
            'value'=>array($this,'renderGridCell')
        ),
 
        'category', //display the category as a default column
    ),
));
 
 
//controller code
class ProductController extends Controller 
{
    ...
 
 
    protected function renderGridCell($data,$row,$dataColumn) 
    {
      //var_dump($dataColumn);
      //$dataColumn is an instance of a [CDataColumn](http://www.yiiframework.com/doc/api/1.1/CDataColumn "CDataColumn")
      //you have access to the properties and methods here (name, id, type, value, grid ...)
 
      //implement the rendering
      switch($dataColumn->name) {
          case 'product': ....
             break;
 
          case 'description': ....
             break;
 
         ....
      } 
 
      ....
 
    }

Tip

Generate you own views for the grid datacolums. You can add buttons, links, ajax/CJuiDialog in the viewfile which were rendered into a datacell: See my other CGridview articles.

Total 13 comments

#15520 report it
Gerhard Liebenberg at 2013/11/18 02:49pm
Search, filter and sort

Depending on what you have in your custom column, it might be searchable and sortable.

This wiki might help in some instances: wiki

#15505 report it
Gerhard Liebenberg at 2013/11/16 02:27am
Example where lazy loading is required

I have a financial site.

Depending on the modules my clients use, records in the "General Ledger Accounts" table are all linked to several DIFFERENT secondary tables.

In this scenario, eager loading will retrieve no records, because no account is linked to ALL secondary tables. So lazy loading is my only option - first retrieving all accounts and then reading each account's secondary info individually.

Thanx for this top class wiki.

#13591 report it
Nur Rochim at 2013/06/09 03:01pm
Good

Wow, good wiki. Thank you

#13486 report it
fiela at 2013/05/31 06:22am
search

very helpful, but can you help me some more In your "protected function gridProductCategories($data,$row)" is there anyway to make that column searchable as well. By the name and not id. I know this will happen in the model, but don't know how to even start

#13479 report it
turi at 2013/05/31 01:41am
Nice and Easy to understand

Thanks for this article... very helpful for me.

#13325 report it
asoqa at 2013/05/22 11:08am
problem with filter

it's funny!but seems not work well with filter

#12398 report it
Cymon Xrisomngam at 2013/03/18 08:46am
Excellent work!, for dataColumn parameter.

Manny Thanks for quick respond. It is excellent work!

#12396 report it
Joblo at 2013/03/18 07:57am
Col params

Thanks for your question. Added section "Access column properties" to this article.

#12394 report it
Cymon Xrisomngam at 2013/03/18 06:13am
where is $col params??

this article why has only $row params where is $col params?? ** i need to get column id or column name pass to function too, use for calculate or lookup data.

#11619 report it
Sukhwinder at 2013/01/23 05:48am
Simply Superb

This shows the real power of Yii, I wonder how flexible and extendable Yii is !

#10372 report it
Th3N3rD at 2012/10/23 12:41pm
Best Easier Method Ever!

Very Good article especially the part that use:

'value' => array($this,'gridProductCategories')

So I just changed this to use closure (or anonimous function)!!!

'value' => function () {
      return "Whatever you want"; // it works both with echo and return
}

or if you need to use some widget could be:

'value' => function () {
      Yii::app()->controller->widget(...);
}

That's the best way if you don't want to make a method to handle rendering stuff.

#8719 report it
Nacesprin at 2012/06/21 06:03am
Very Helpful

Thanks!, this has been very helpful for me!!!

#5949 report it
horizons at 2011/11/28 08:05am
another method

If you just want to render the data you can also use render partitial diretly this way

'value'=>'$this->grid->getOwner()->renderPartial(\'tableviews/_column_view\',array(\'data\'=>$data),true)',

Leave a comment

Please to leave your comment.

Write new article
  • Written by: Joblo
  • Category: Tips
  • Yii Version: 1.1
  • Votes: +72
  • Viewed: 109,690 times
  • Created on: Nov 27, 2011
  • Last updated: Mar 18, 2013
  • Tags: CGridView