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!