- Example
- B) Lazy loading - load the categories for each product
- C) The fun part - render view files into a cell
- Access column properties
- Tip
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.
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)',
Very Helpful
Thanks!, this has been very helpful for me!!!
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.
Simply Superb
This shows the real power of Yii, I wonder how flexible and extendable Yii is !
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.
Col params
Thanks for your question.
Added section "Access column properties" to this article.
Excellent work!, for dataColumn parameter.
Manny Thanks for quick respond. It is excellent work!
problem with filter
it's funny!but seems not work well with filter
Nice and Easy to understand
Thanks for this article... very helpful for me.
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
Good
Wow, good wiki. Thank you
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.
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
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.