Hey folks…
I've created my first extension! It wraps the jqGrid (which is a jQuery grid) in an easy to use widget.
Looking for feedback/bugs/comments before I post to extensions. Plus I have to write some doc.
I've installed the jqGrid into a directory on my server at /extra. So the base URL for jqGrid is /extra/jqGrid. This can be overridden with the baseUrl option (see below).
The class (CjqGridWidget.php) should be placed in your /protected/extensions/jqGrid directory. If you put it somewhere else, be sure to change the widget invocation string to the appropriate path.
So, here's the class (CjqGridWidget.php):
<?php
/**
* CjqGridWidget class file.
*
* @author Jerry Ablan <jablan@pogostick.com>
* @link http://www.pogostick.com/
* @copyright Copyright © 2009 Pogostick, LLC
* @license http://www.gnu.org/licenses/gpl.html
*
* Install in <yii_app_base>/extensions/jqGrid
*/
/**
* The CjqGridWidget allows the jqGrid (@link http://www.trirand.com/blog/) to be used in Yii.
* Thanks to MetaYii for some ideas on valid options and callbacks.
*
* @author Jerry Ablan <jablan@pogostick.com>
* @version $Id: CjqGridWidget.php 1 2009-03-31 00:30:25Z jablan $
* @package applications.extensions.CjqGridWidget
* @since 1.0.3
*/
class CjqGridWidget extends CInputWidget
{
//********************************************************************************
//* Member Variables
//********************************************************************************
/**
* Where the the base jqGrid files are installed.
*
* @var string
*/
protected $m_sBaseUrl = '/extra/jqGrid';
/**
* Css file to override default style
*
* @var string
*/
protected $m_sCssFile = null;
/**
* Indicates whether or not to validate options
*
* @var boolean
*/
protected $m_bCheckOptions = true;
/**
* Indicates whether or not to validate callbacks
*
* @var boolean
*/
protected $m_bCheckCallbacks = true;
/**
* Valid options for this widget
*
* @var array
*/
protected $m_arValidOptions = array(
'altRows' => array( 'type' => 'boolean' ),
'caption' => array( 'type' => 'string' ),
'cellEdit' => array( 'type' => 'boolean' ),
'cellsubmit' => array( 'type' => 'string', 'valid' => array( 'remote', 'clientarray' ) ),
'cellurl' => array( 'type' => 'string' ),
'colModel' => array( 'type' => 'array' ),
'colNames' => array( 'type' => 'array' ),
'datastr' => array( 'type' => 'string' ),
'datatype' => array( 'type' => 'string', 'valid' => array( 'xml', 'xmlstring', 'json', 'jsonstring', 'clientside' ) ),
'deselectAfterSort' => array( 'type' => 'boolean' ),
'editurl' => array( 'type' => 'string' ),
'expandcolumn' => array( 'type' => 'boolean' ),
'forceFit' => array( 'type' => 'boolean' ),
'gridstate' => array( 'type' => 'string', 'valid' => array( 'visible', 'hidden' ) ),
'hiddengrid' => array( 'type' => 'boolean' ),
'hidegrid' => array( 'type' => 'boolean' ),
'height' => array( 'type' => array( 'string', 'integer' ) ),
'imgpath' => array( 'type' => 'string' ),
'jsonReader' => array( 'type' => 'array' ),
'loadonce' => array( 'type' => 'boolean' ),
'loadtext' => array( 'type' => 'string' ),
'loadui' => array( 'type' => 'string', 'valid' => array( 'disable', 'enable', 'block' ) ),
'multiselect' => array( 'type' => 'boolean' ),
'mtype' => array( 'type' => 'string', 'valid' => array( 'GET', 'PUT' ) ),
'multikey' => array( 'type' => 'string' ),
'multiboxonly' => array( 'type' => 'boolean' ),
'pagerId' => array( 'type' => 'string' ),
'prmNames' => array( 'type' => 'array' ),
'postData' => array( 'type' => 'array' ),
'resizeclass' => array( 'type' => 'string' ),
'rowNum' => array( 'type' => 'integer' ),
'rowList' => array( 'type' => 'array' ),
'scroll' => array( 'type' => 'boolean' ),
'scrollrows' => array( 'type' => 'boolean' ),
'sortclass' => array( 'type' => 'string' ),
'shrinkToFit' => array( 'type' => 'boolean' ),
'sortascimg' => array( 'type' => 'string' ),
'sortdescimg' => array( 'type' => 'string' ),
'sortname' => array( 'type' => 'string' ),
'sortorder' => array( 'type' => 'string' ),
'theme' => array( 'type' => 'string', valid => array( 'basic', 'coffee', 'green', 'sand', 'steel' ) ),
'toolbar' => array( 'type' => 'array' ),
'treeGrid' => array( 'type' => 'boolean' ),
'tree_root_level' => array( 'type' => 'integer' ),
'url' => array( 'type' => 'string' ),
'userData' => array( 'type' => 'array' ),
'viewrecords' => array( 'type' => 'boolean' ),
'width' => array( 'type' => 'integer' ),
'xmlReader' => array( 'type' => 'array' ),
);
/**
* The valid callbacks for this widget
*
* @var mixed
*/
protected $m_arValidCallbacks = array(
'afterInsertRow',
'gridComplete',
'loadBeforeSend',
'loadComplete',
'loadError',
'onCellSelect',
'ondblclickRow',
'onHeaderClick',
'onRighClickRow',
'onselectAll',
'onselectRow',
'onSortCol'
);
/**
* Placeholder for widget options
*
* @var array
*/
public $m_arOptions = array();
/**
* Placeholder for callbacks
*
* @var array
*/
protected $m_arCallbacks = array();
//********************************************************************************
//* Methods
//********************************************************************************
/***
* Runs this widget
*
*/
public function run()
{
// Validate baseUrl
if ( empty( $this->m_sBaseUrl ) )
throw new CHttpException( 500, 'CjqGridWidget: baseUrl is required.');
// Get the id/name of this widget
list( $_sName, $_sId ) = $this->resolveNameID();
// Register the scripts/css
$this->registerClientScripts( $_sId );
// Generate the HTML for this widget
echo $this->generateHtml( $_sId );
}
/**
* Registers the needed CSS and JavaScript.
*
* @param string $sId
*/
public function registerClientScripts( $sId = 'list' )
{
// If image path isn't specified, set to current theme path
if ( ! array_key_exists( 'imgpath', $this->m_arOptions ) || empty( $this->m_arOptions[ 'imgpath' ] ) )
$this->m_arOptions[ 'imgpath' ] = "{$this->m_sBaseUrl}/themes/{$this->m_arOptions[ 'theme' ]}/images";
// Register scripts necessary
$_oCS = Yii::app()->getClientScript();
$_oCS->registerScriptFile( "{$this->m_sBaseUrl}/jquery.jqGrid.js" );
$_oCS->registerScriptFile( "{$this->m_sBaseUrl}/js/jqModal.js" );
$_oCS->registerScriptFile( "{$this->m_sBaseUrl}/js/jqDnR.js" );
// Get the javascript for this widget
$_sScript = $this->generateJavascript( $sId );
$_oCS->registerScript( 'Yii.' . get_class( $this ) . '#' . $sId, $_sScript, CClientScript::POS_READY );
// Register css files...
$_oCS->registerCssFile( "{$this->m_sBaseUrl}/themes/{$this->m_arOptions[ 'theme' ]}/grid.css", 'screen' );
$_oCS->registerCssFile( "{$this->m_sBaseUrl}/themes/jqModal.css", 'screen' );
if ( ! empty( $this->m_sCssFile ) )
$_oCS->registerCssFile( Yii::app()->baseUrl . "{$this->m_sCssFile}", 'screen' );
}
//********************************************************************************
//* Property Accessors
//********************************************************************************
/**
* Get the BaseUrl property
*
*/
public function getBaseUrl()
{
return( $this->m_sBaseUrl );
}
/**
* Set the BaseUrl property
*
* @param mixed $sUrl
*/
public function setBaseUrl( $sUrl )
{
$this->m_sBaseUrl = $sUrl;
}
/***
* Get the Css File
*
*/
public function getCssFile()
{
return( $this->m_sCssFile );
}
/***
* Set the Css file
*
* @param mixed $_sFile
*/
public function setCssFile( $_sFile )
{
$this->m_sCssFile = $_sFile;
}
/**
* Setter
*
* @var array $value options
*/
public function setOptions( $arOptions )
{
if ( ! is_array( $arOptions ) )
throw new CException( Yii::t( 'CjqGridWidget', 'options must be an array' ) );
if ( $this->m_bCheckOptions )
self::checkOptions( $arOptions, $this->m_arValidOptions );
$this->m_arOptions = $arOptions;
}
/**
* Gets the CheckOptions option
*
*/
public function getCheckOptions()
{
return( $this->m_bCheckOptions );
}
/**
* Sets the CheckOptions option
*
* @param mixed $_bValue
*/
public function setCheckOptions( $_bValue )
{
$this->m_bCheckOptions = $_bValue;
}
/**
* Gets the CheckCallbacks option
*
*/
public function getCheckCallbacks()
{
return( $this->m_bCheckCallbacks );
}
/***
* Sets the CheckCallbacks option
*
* @param mixed $_bValue
*/
public function setCheckCallbacks( $_bValue )
{
$this->m_bCheckCallbacks = $_bValue;
}
/**
* Getter
*
* @return array
*/
public function getOptions()
{
return( $this->m_arOptions );
}
/**
* Setter
*
* @param array $value callbacks
*/
public function setCallbacks( $arCallbacks )
{
if ( ! is_array( $arCallbacks ) )
throw new CException( Yii::t( 'CjqGridWidget', 'callbacks must be an associative array' ) );
if ( $this->m_bCheckCallbacks )
self::checkCallbacks( $arCallbacks, $this->m_arValidCallbacks );
$this->m_arCallbacks = $arCallbacks;
}
/**
* Getter
*
* @return array
*/
public function getCallbacks()
{
return $this->m_arCallbacks;
}
//********************************************************************************
//* Private methods
//********************************************************************************
/**
* Check the options against the valid ones
*
* @param array $value user's options
* @param array $validOptions valid options
*/
protected static function checkOptions( $arOptions, $arValidOptions )
{
if ( ! empty( $arValidOptions ) )
{
foreach ( $arOptions as $_sKey => $_oValue )
{
if ( ! array_key_exists( $_sKey, $arValidOptions ) )
throw new CException( Yii::t( 'CjqGridWidget', '"{x}" is not a valid option', array( '{x}' => $_sKey ) ) );
$_sType = gettype( $_oValue );
if ( ( ! is_array( $arValidOptions[ $_sKey ][ 'type' ] ) && ( $_sType != $arValidOptions[ $_sKey ][ 'type' ] ) ) || ( is_array( $arValidOptions[ $_sKey ][ 'type' ] ) && ! in_array( $_sType, $arValidOptions[ $_sKey ][ 'type' ] ) ) )
throw new CException( Yii::t( 'CjqGridWidget', '"{x}" must be of type "{y}"', array( '{x}' => $_sKey, '{y}' => ( is_array( $arValidOptions[ $_sKey ][ 'type' ] ) ) ? implode( ', ', $arValidOptions[ $_sKey ][ 'type' ] ) : $arValidOptions[ $_sKey ][ 'type' ] ) ) );
if ( array_key_exists( 'valid', $arValidOptions[ $_sKey ] ) )
{
if ( ! in_array( $_oValue, $arValidOptions[ $_sKey ][ 'valid' ] ) )
throw new CException( Yii::t( 'CjqGridWidget', '"{x}" must be one of: "{y}"', array( '{x}' => $_sKey, '{y}' => implode( ', ', $arValidOptions[ $_sKey ][ 'valid' ] ) ) ) );
}
if ( ( $_sType == 'array' ) && array_key_exists( 'elements', $arValidOptions[ $_sKey ] ) )
self::checkOptions( $_oValue, $arValidOptions[ $_sKey ][ 'elements' ] );
}
}
}
/**
*
* @param array $value user's callbacks
* @param array $validCallbacks valid callbacks
*/
protected static function checkCallbacks( $arCallbacks, $arValidCallbacks )
{
if ( ! empty( $arValidCallbacks ) )
{
foreach ( $arCallbacks as $_sKey => $_oValue )
{
if ( ! in_array( $_sKey, $arValidCallbacks ) )
throw new CException( Yii::t( 'CjqGridWidget', '"{x}" must be one of: {y}', array( '{x}' => $_sKey, '{y}' => implode( ', ', $arValidCallbacks ) ) ) );
}
}
}
/**
* Generates the javascript code for the widget
*
* @return string
*/
protected function generateJavascript( $sId = 'list' )
{
$_arOptions = $this->makeOptions();
$_sScript =<<<CODE
jQuery("#{$sId}").jqGrid( {$_arOptions} );
CODE;
return( $_sScript );
}
/**
* Generates the javascript code for the widget
*
* @return string
*/
protected function generateHtml( $sId = 'list', $sPagerId = 'jqPager' )
{
$_sHtml =<<<CODE
<table id="{$sId}" class="scroll"></table>
<div id="{$sPagerId}" class="scroll" style="text-align:center;"></div>
CODE;
return( $_sHtml );
}
/**
* Generates the options for the widget
*
* @return string
*/
protected function makeOptions()
{
$_arOptions = array();
foreach ( $this->m_arCallbacks as $_sKey => $_oValue )
$_arOptions[ "cb_{$_sKey}" ] = $_sKey;
$_sEncodedOptions = CJavaScript::encode( array_merge( $_arOptions, $this->m_arOptions ) );
// Fix up the pager...
$_sEncodedOptions = str_replace( "'pagerId':'{$this->m_arOptions['pagerId']}'", "'pager': jQuery('#{$this->m_arOptions['pagerId']}')", $_sEncodedOptions );
foreach ( $this->m_arCallbacks as $_sKey => $_oValue )
$_sEncodedOptions = str_replace( "'cb_{$_sKey}':'{$_sKey}'", "'{$_sKey}': {$_oValue}", $_sEncodedOptions );
return( $_sEncodedOptions );
}
}
Here is the method for your controller to generate the needed XML. Obviously you'll need to change the column names and whatnot:
/**
* Returns Xml data suitable for jqGrid
*
*/
public function actionXmlData()
{
$_iPage = 1;
$_iLimit = 25;
$_iSortCol = 1;
$_sSortOrder = 'asc';
// Get any passed in arguments
if ( isset( $_REQUEST[ 'page' ] ) )
$_iPage = $_REQUEST[ 'page' ];
if ( isset( $_REQUEST[ 'rows' ] ) )
$_iLimit = $_REQUEST[ 'rows' ];
if ( isset( $_REQUEST[ 'sidx' ] ) )
$_iSortCol = $_REQUEST[ 'sidx' ];
if ( isset( $_REQUEST[ 'sord' ] ) )
$_sSortOrder = $_REQUEST[ 'sord' ];
// Get a count of rows for this result set
$_dbc = new CDbCriteria();
$_dbc->condition = 'user_uid = :user_uid';
$_dbc->params = array( ':user_uid' => Yii::app()->user->id );
$_iRowCount = InventoryItem::model()->count( $_dbc );
// Calculate paging info
if ( $_iRowCount > 0 )
$_iTotalPages = ceil( $_iRowCount / $_iLimit );
else
$_iTotalPages = 0;
// Sanity check
if ( $_iPage > $_iTotalPages )
$_iPage = $_iTotalPages;
if ( $_iPage < 1 )
$_iPage = 1;
// Calculate starting offset
$_iStart = $_iLimit * $_iPage - $_iLimit;
// Sanity check
if ( $_iStart < 0 )
$_iStart = 0;
// Adjust the criteria for the actual query...
$_dbc->order = "{$_iSortCol} {$_sSortOrder}";
$_dbc->select = "inv_uid, inv_type_uid, name_text, sku_id_text, upc_code_text, qty_on_hand_nbr";
$_dbc->limit = $_iLimit;
$_dbc->offset = $_iStart;
$_oRows = InventoryItem::model()->findAll( $_dbc );
// Set appropriate content type
if ( stristr( $_SERVER[ 'HTTP_ACCEPT' ], "application/xhtml+xml" ) )
header( "Content-type: application/xhtml+xml;charset=utf-8" );
else
header( "Content-type: text/xml;charset=utf-8" );
// Now create the Xml...
$_sOut = "<?xml version='1.0' encoding='utf-8'?>";
$_sOut .= CHtml::openTag( "rows" );
$_sOut .= CHtml::tag( 'page', array(), $_iPage );
$_sOut .= CHtml::tag( 'total', array(), $_iTotalPages );
$_sOut .= CHtml::tag( 'records', array(), $_iRowCount );
if ( $_oRows )
{
// Create the row data...
foreach ( $_oRows as $_oRow )
{
$_sOut .= CHtml::openTag( 'row', array( 'id' => $_oRow->inv_uid ) );
$_sOut .= CHtml::tag( 'cell', array(), CHtml::cdata( $_oRow->inventoryType->type_name_text ) );
$_sOut .= CHtml::tag( 'cell', array(), CHtml::cdata( $_oRow->sku_id_text ) );
$_sOut .= CHtml::tag( 'cell', array(), CHtml::cdata( $_oRow->upc_code_text ) );
$_sOut .= CHtml::tag( 'cell', array(), CHtml::cdata( $_oRow->name_text ) );
$_sOut .= CHtml::tag( 'cell', array(), $_oRow->qty_on_hand_nbr );
$_sOut .= CHtml::closeTag( 'row' );
}
}
// Close our tag...
$_sOut .= CHtml::closeTag( 'rows' );
// Spit it out...
echo $_sOut;
}
Here is the view that creates the grid. Again, you'll need to change the column names to match your needs:
<?php
$_arOptions = array(
'url' => Yii::app()->createUrl( 'InventoryItem/XmlData' ),
'datatype' => 'xml',
'mtype' => 'GET',
'pagerId' => 'jqPager',
'rowNum' => 25,
'rowList' => array( 10, 25, 50, 100 ),
'sortname' => 'name_text',
'sortorder' => 'asc',
'viewrecords' => true,
'theme' => 'steel',
'width' => 800,
'height' => 'auto',
'colNames' => array( "Type", "SKU", "UPC Code", "Name", "Quantity" ),
'colModel' => array(
array( 'name' => 'inv_type_uid', 'index' => 'inv_type_uid', 'width' => 25 ),
array( 'name' => 'sku_id_text', 'index' => 'sku_id_text', 'width' => 30 ),
array( 'name' => 'upc_code_text', 'index' => 'upc_code_text', 'width' => 30 ),
array( 'name' => 'name_text', 'index' => 'name_text', 'width' => 125 ),
array( 'name' => 'qty_on_hand_nbr', 'index' => 'qty_on_hand_nbr', 'width' => 25, 'align' => 'right' ),
),
);
$_arCallbacks = array(
'onselectRow' => 'function( id ) { location.href = "update/id/" + id; }',
);
$this->widget( 'application.extensions.jqGrid.CjqGridWidget', array(
'cssFile' => '/css/grid.css',
'name' => 'list',
'id' => 'list',
'baseUrl' => Yii::app()->baseUrl . '/extra/jqGrid',
'options' => $_arOptions,
'callbacks' => $_arCallbacks,
)
);
?>
I also put in an option to override the default CSS of the grid, it's the cssFile option at the component level.
Please let me know what you think!! Thanks!