jqGrid Widget

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 &copy; 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!

It would be nice, if there is a demo page.

-majin-

Very nice. Could you please share it at http://www.yiiframew…com/extensions/

I sure will. I just want to put together a demo page that works with the blog sample app and an install guide.

I have a question. I will probably be building many extensions. And I want them all to descend from a common ancestor.

Would it behoove me to put my code under the /framework root so ALL my apps can share it? Or is there a preferred method/best practice for this?

nice tutorial :)

can i get the complete source about this tutorial please, cos i’m a newbie?

This and a bunch of other extensions are in my psYiiExtensions extension. Check in the extensions section for the source.

Can you provide a json datatype example, I am having problem to send json response from the controller.

Update

Here is the json example for anyone having problem:

Controller action:




public function actionData()

    {

        if (Yii::app()->request->isAjaxRequest)

        {

            $page = (isset($_GET['page']) ? $_GET['page']:'1');

            $limit = (isset($_GET['rows']) ? $_GET['rows']:'50');

            $sidx = (isset($_GET['sidx']) ? $_GET['sidx']:'modified_at'); // get index row - i.e. user click to sort

            $sord = (isset($_GET['sord']) ? $_GET['sord']:'desc'); // get the direction


            $model = Model::model();

            // set any conditions

            $model->property = $pro;


            $dataProvider = $model->getData($page, $limit, $sidx, $sord);


            $count = $dataProvider->totalItemCount;

            $total_pages = ($count) ? ceil($count/$limit) : 0;


            // prepare json data for jqGrid

            $response = new stdClass();


            $response->page = $page;

            $response->total = $total_pages;

            $response->records = $count;


            $data=$dataProvider->getData();


            foreach($data as $row)

            {

                $response->rows[]=array(

                    'id'=>$row->id,

                    'cell'=>array(

                        $row->modified_at,

                        $row->other_columns,

                    )

                );

            }

            echo CJSON::encode($response);

        }

    }



Model getData() method




public function getData($page, $limit, $sidx, $sord)

    {

        $criteria=new CDbCriteria;


        $criteria->compare('property', $this->property);


        $dataProvider = new CActiveDataProvider('Model', array(

            'criteria'=>$criteria,

            'pagination'=>array(

                'currentPage'=>$page-1, // CActiveDataProvider is zero-based, jqGrid not

                'pageSize'=>$limit,

            ),

            'sort'=>array(

                'defaultOrder'=>"$sidx $sord",

            )

        ));


        return $dataProvider;

    }