dataStore and dataGrid view helpers.

Introduction

Grid’s are used everywhere and one thing the Zend Framework really lacks is a useful view helper to quickly generate grids and their stores. I saw that  Matthew Weier O’Phinney had a proposal for a dojox.grid.DataGrid view helper but no work had been done on it. I send him a tweet and learned that he had intended to finish it but is currently too busy to work on it so I wrote a few view helpers to follow his proposal. The dataStore view helper is fairly simplistic but the dataGrid view helper has quite a bit to it. I plan on refactoring the dataGrid view helper to add support for enhancedGrid and treeGrid but I wanted to share what I had done already. Both view helpers follow the use-cases as presented by Matthew in his dojox.grid.DataGrid view helper proposal.

The dataStore() helper

The dataStore() helper provides support for generating programmatic/declarative data stores and requires an id as well as a dojoType.

Usage

<?=$this->dataStore('store', 'dojox.data.QueryReadStore', array('url' => '/path/to/json'));?>

Code

<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Dojo_View_Helper_DataStore
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */

/**
 * Zend_Calendar
 *
 * @category   Zend
 * @package    Zend_Dojo_View_Helper_DataStore
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Dojo_View_Helper_DataStore extends Zend_Dojo_View_Helper_Dijit
{
	/**
	 * DataStore view helper.
	 * 
	 * @param string $id JavaScript id for the data store.
	 * @param string $dojoType DojoType of the data store (e.g., dojox.data.QueryReadStore)
	 * @param array $attribs Attributes for the data store.
	 */
	public function dataStore($id = '', $dojoType = '', array $attribs = array())
	{
		if (!$id || !$dojoType) {
			throw new Zend_Exception('Invalid arguments: required jsId and dojoType.');
		}
		
		$this->dojo->requireModule($dojoType);
		
		// Programmatic
		if ($this->_useProgrammatic()) {
			if (!$this->_useProgrammaticNoScript()) {
				$this->dojo->addJavascript('var ' . $id . ";\n");
				$js = $id . ' = ' . 'new ' . $dojoType . '(' . Zend_Json::encode($attribs) . ");";
				$this->dojo->_addZendLoad("function(){{$js}}");
			}
			return '';
		}
		
		// Set extra attribs for declarative
		if (!array_key_exists('id', $attribs)) {
			$attribs['id'] = $id;
		}
		
		if (!array_key_exists('jsId', $attribs)) {
			$attribs['jsId'] = $id;
		}
		
		if (!array_key_exists('dojoType', $attribs)) {
			$attribs['dojoType'] = $dojoType;
		}
		
		return '<div' . $this->_htmlAttribs($attribs) . "></div>\n";
	}
}

The dataGrid() helper

The dataGrid helper provides support for generating programmatic/declarative dataGrids. Currently, the only supported grid is dojox.grid.DataGrid but I’ll be adding EnhancedGrid and TreeGrid in the future.

Usage

<?php
echo $this->dataStore('store', 'dojox.data.QueryReadStore', array('url' => '/admin/articles/grid.json'));
$grid = $this->dataGrid('myGrid',
	array(
		'selectionMode' => 'none',
		'style' => 'height: 350px; width: 100%;',
		'store' => 'store',
		'fields' => array(
			array('field' => 'title', 'label' => 'Title', 'options' => array('formatter' => 'titleFormatter', 'width' => '60%')),
			array('field' => 'published', 'label' => 'Published', 'options' => array('formatter' => 'primaryFormatter', 'width' => '10%')),
			array('field' => 'updated_at', 'label' => 'Updated', 'options' => array('width' => '15%')),
			array('field' => 'created_at', 'label' => 'Created', 'options' => array('width' => '15%')))));
?>
<?php $grid->scriptCaptureStart('dojo/method', 'onMouseOver', array('args' => 'row'));?>
var g = dojo.byId("grid-menu-item-"+row.rowIndex);
if (g) { dojo.toggleClass(g, "hide-text"); }
<?php $grid->scriptCaptureEnd();?>
<?php $grid->scriptCaptureStart('dojo/method', 'onMouseOut', array('args' => 'row'));?>
var g = dojo.byId("grid-menu-item-"+row.rowIndex);
if (g) { dojo.toggleClass(g, "hide-text"); }
<?php $grid->scriptCaptureEnd();?>
<?php echo $grid;?>

Code

<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Dojo_View_Helper_DataGrid
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */

/**
 * Zend_Calendar
 *
 * @category   Zend
 * @package    Zend_Dojo_View_Helper_DataGrid
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Dojo_View_Helper_DataGrid extends Zend_Dojo_View_Helper_Dijit
{
	/**
	 * Ensures only one script capture is active at a time.
	 * @var bool
	 */
	protected $_captureLock = false;
	
	/**
	 * Holds the attributes for the grid.
	 * @var array
	 */
	protected $_attribs = array();
	
	/**
	 * Holds the field data.
	 * @var array
	 */
	protected $_fields = array();
	
	/**
	 * Holds the data for the current script capture.
	 */
	protected $_currentScript = array();
	
	/**
	 * Holds the data for script captures.
	 * @var array
	 */
	protected $_scriptCapture = array();
	
	/**
	 * Holds the tab character.
	 * @var string
	 */
	protected $_tab = '    ';
	
	/**
	 * Holds the current theme.
	 */
	protected static $_theme = 'tundra';
	
	/**
	 * Holds the base path for scripts.
	 */
	protected static $_scriptBase = null;
	
	/**
	 * __toString();
	 */
	public function __toString()
	{
		return $this->render();
	}
	
	/**
	 * DataGrid view helper.
	 * 
	 * @param string $id JavaScript id for the data store.
	 * @param array $attribs Attributes for the data store.
	 */
	public function dataGrid($id = '', array $attribs = array())
	{
		if (!$id) {
			throw new Zend_Exception('Invalid arguments: required jsId.');
		}
		
		if (!array_key_exists('id', $attribs)) {
			$attribs['id'] = $id;
		}
		
		if (array_key_exists('fields', $attribs)) {
			foreach ($attribs['fields'] as $f) {
				$this->addField($f['field'], $f['label'], isset($f['options']) ? $f['options'] : array());
			}
			unset($attribs['fields']);
		}
		
		$this->_attribs = $attribs;
		
		return $this;
	}
	
	/**
	 * Static setter for theme.
	 * @param string $theme Name of the current them.
	 */
	public static function setTheme($theme)
	{
		self::$_theme = $theme;
	}
	
	/**
	 * Static getter for theme.
	 * @return string
	 */
	public static function getTheme()
	{
		return self::$_theme;
	}
	
	/**
	 * Static setting for script base.
	 * @param string $scriptBase Path to the base script directory.
	 */
	public static function setScriptBase($scriptBase)
	{
		self::$_scriptBase = $scriptBase;
	}
	
	/**
	 * Static getter for script base.
	 * @return string
	 */
	public static function getScriptBase()
	{
		return self::$_scriptBase;
	}
	
	/**
	 * Adds field data.
	 * @param string $field Field name.
	 * @param string $label Label of the field.
	 * $param array $attribs Optional parameters for the field.
	 */
	public function addField($field, $label, array $attribs = array())
	{
		$this->_fields[] = array(
			'label' => $label, 
			'attribs' => array_merge(array('field' => $field), $attribs));
		return $this;
	}
	
	/**
	 * Adds script captures.
	 * @param array $data
	 */
	public function addScriptCapture(array $script = array())
	{
		if (!array_key_exists('data', $script)) {
			throw new Zend_Exception('Script data must include keys data and attribs');
		}
		
		$this->_scriptCapture[] = $script;
		return $this;
	}
	
	/**
	 * Begins script capturing.
	 */
	public function scriptCaptureStart($type, $event, array $attribs = array())
	{
		if ($this->_captureLock) {
			throw new Zend_Exception('Cannot nest captures.');
		}
		
		$this->_currentScript = array('type' => $type, 'event' => $event, 'attribs' => $attribs);
		
		$this->_captureLock = true;
		ob_start();
		return;
	}
	
	/**
	 * Ends script capturing.
	 */
	public function scriptCaptureEnd()
	{
		$data = ob_get_clean();
		$this->_captureLock = false;
		
		$this->_currentScript['data'] = $data;
		
		$this->addScriptCapture($this->_currentScript);
		$this->_currentScript = array();
		
		return true;
	}
	
	/**
	 * Renders the grid based on programmatic setting.
	 */
	public function render()
	{
		$this->dojo->requireModule('dojox.grid.DataGrid');
		
		// Setup the stylesheet base path
		if (null === self::getScriptBase()) {
			if ($this->dojo->useLocalPath()) {
				self::setScriptBase($this->dojo->getLocalPath());
			} else {
				self::setScriptBase($this->dojo->getCdnBase() . $this->dojo->getCdnVersion());
			}
		}
		
		$this->dojo->addStylesheet(self::getScriptBase() . '/dojox/grid/resources/Grid.css');
		$this->dojo->addStylesheet(
			self::getScriptBase() . '/dojox/grid/resources/' . self::getTheme() . 'Grid.css');
		
		// Programmatic
		if ($this->_useProgrammatic()) {
			if (!$this->_useProgrammaticNoScript()) {
				$this->_renderJavascript();
			}
			return '<div id="' . $this->_attribs['id'] . '"></div>';
		}
		
		return $this->_renderDeclarative();
	}
	
	/**
	 * Renders a table for declarative grids.
	 */
	protected function _renderDeclarative()
	{
		if (!array_key_exists('jsId', $this->_attribs)) {
			$this->_attribs['jsId'] = $this->_attribs['id'];
		}
		
		$table = '<table dojoType="dojox.grid.DataGrid"' . $this->_htmlAttribs($this->_attribs) . ">\n";
		
		foreach ($this->_scriptCapture as $s) {
			$attribs = array_merge($s['attribs'], array('event' => $s['event'], 'type' => $s['type']));
			$table .= "\t<script" . $this->_htmlAttribs($attribs) . ">\n";
			$table .= "\t\t" . $s['data'];
			$table .= "\t</script>\n";
		}
		
		$table .= "\t<thead>\n";
		$table .= "\t\t<tr>\n";
		
		foreach ($this->_fields as $f) {
			$table .= "\t\t\t<th" . $this->_htmlAttribs($f['attribs']) . '>' . $f['label'] . "</th>\n";
		}
		
		$table .= "\t\t</tr>\n";
		$table .= "\t</thead>\n";
		$table .= "</table>\n";
		
		return $table;
	}
	
	/**
	 * Renders javascript for programmatic declaration.
	 */
	protected function _renderJavascript()
	{
		$tab = $this->_tab;
		
		// Grid names
		$gridName = $this->_attribs['id'] . 'Grid';
		$layoutName = $this->_attribs['id'] . 'Layout';
		
		$this->dojo->addJavascript('var ' . $gridName . ";\n");
		
		// Setup layout
		$js = $tab . 'var ' . $layoutName . " = [\n";
		foreach ($this->_fields as $f) {
			$f['attribs']['name'] = $f['label'];
			
			$f['attribs'] = $this->_jsonExpr($f['attribs']);
			$js .= "{$tab}{$tab}{$tab}" . Zend_Json::encode($f['attribs'], false, 
				array('enableJsonExprFinder' => true)) . ",\n";
		}
		
		$js = substr($js, 0, -2);
		$js .= "];\n\n";
		
		// Use expressions for structure, store, formatter, and get
		$this->_attribs = $this->_jsonExpr($this->_attribs);
		$this->_attribs['structure'] = new Zend_Json_Expr($layoutName);
		
		// Generate grid
		$js .= $tab . $tab . $gridName . ' = ' . 'new dojox.grid.DataGrid(' . Zend_Json::encode(
			$this->_attribs, false, array('enableJsonExprFinder' => true)) . "), document.createElement('div');\n";
		
		$js .= $tab . $tab . 'dojo.byId("' . $this->_attribs['id'] . '").appendChild(' . $gridName . '.domNode);' .
			 "\n";
		$js .= $tab . $tab . $gridName . ".startup();\n";
		
		// Generate connects for script captures
		foreach ($this->_scriptCapture as $s) {
			$s['data'] = trim($s['data'], "\r\n");
			$args = isset($s['attribs']['args']) ? $s['attribs']['args'] : '';
			
			$js .= "{$tab}{$tab}dojo.connect({$gridName}, \"{$s['event']}\", function({$args}){{$s['data']}});\n";
		}
		
		$this->dojo->_addZendLoad("function(){\n{$tab}{$js}\n{$tab}}");
	}
	
	/**
	 * Parses an array looking for keys that should be Zend_Json_Expr()'s and coverts them.
	 */
	protected function _jsonExpr(array $data)
	{
		$jsonExprAttr = array('formatter', 'get', 'query', 'store');
		
		foreach ($jsonExprAttr as $exp) {
			if (array_key_exists($exp, $data)) {
				$data[$exp] = new Zend_Json_Expr($data[$exp]);
			}
		}
		
		return $data;
	}
}

Final Thoughts

These view helpers are by no means complete but I will say that I’m currently using them in a production environment with no issues. I tested both declarative/programmatic usage and both functioned as expected. I also wanted to note that I’m by no means intending to step on Matthew’s toes but I know how busy he is with Zend Framework 2.0 (among other things). If anyone has any thoughts or use for this please comment below and, who knows, maybe I’ll make a proposal for it (or take over Matthew’s).

Reblog this post [with Zemanta]

Tags: , , , , , , ,

24 Responses to “dataStore and dataGrid view helpers.”

  1. ojjpo says:

    This looks cool. But the helper should be universal enough to support jQuery as well.

  2. SpiffyJr says:

    @Ojjpo:
    I disagree. jQuery is part of the extended Zend Framework and should have its own ZendX_jQuery_View_Helper.

    note: I suppose the above classes should be called Zend_Dojo_View_Helper_DataStore and Zend_Dojo_View_Helper_DataGrid.

  3. Cameron says:

    Your view helpers are much prettier than the grotesque crap I slopped together for my store and grid helpers, maybe I should use yours!

  4. SpiffyJr says:

    There is still quite a bit of work to do yet. Most notably, stylesheets and expanding for the new grids in Dojo 1.4.

  5. Georgy says:

    Goog work!

    Have you any unit tests for these classes?
    It required for including to Zend Framework.

  6. SpiffyJr says:

    No, this is why I’ve renamed the library to SZend in order to differentiate it from the Zend core framework. My plan is to submit a proposal for these (and other) view helpers.

    The updated source can be found at http://github.com/SpiffyJr/SZend

  7. Chris says:

    Spiffy – in order to use this then we need to create the SZend directory structure? Or should we just rename all SZend to Zend in the code? Thanks!

  8. SpiffyJr says:

    You can call them whatever you want as long as the helper path is setup properly.

  9. Chris says:

    Ahhh.. Initially I just dumped them into the regular zend path, so clearly I did that wrong :) What should the path be?

  10. SpiffyJr says:

    If the prefix for the classes is SZend then you need to put the files in SZend/View/Helper. If you rename them all to Zend dropping them in Zend/View/Helper will work just fine.

    For SZend, add the following to application.ini
    ———————————————————-
    resources.view.helperPath.SZend_View_Helper = “SZend/View/Helper”

  11. Chris says:

    Awesomeness.

  12. Chris says:

    Well I can’t get the dialog or this to work :) With the dialog I am using your example:
    echo $this->dialog(‘myDialog’,…

    Nothing happens, no errors or changes in display. Dojo is already working on the view I am testing this with. With the grid, I get an error when i create the grid, but don’t put any data in it and then echo it.

    Fatal error: Call to undefined method Zend_Dojo_View_Helper_Dojo_Container::_addZendLoad() i

  13. SpiffyJr says:

    You should probably grab the latest code from http://www.github.com/spiffyjr/SZend.

  14. Chris says:

    yeah i did download the code from there last week thursday maybe? I think that is up to date.

  15. Chris says:

    is _addZendLoad() a valid function call? is there a better way to ask questions than spamming up your comments? :)

  16. SpiffyJr says:

    _addZendLoad() is a Zend Framework defined method. You can AIM me at SpiffyJr if you wish.

  17. Chris says:

    I sent you a msg on aim, but I commented out the code that uses programmatic, which then forces it to set up the rendering to use declarative. This works in displaying the grid headers at least.

  18. Tridem says:

    Thanks for these great helpers. I found them before I started my own work.

    I would like to move the DataGrid and DataStore to my library’s components. The idea is to create objects by an extending a Model (like with mappers or DbTable) or using a Zend_Config (configs/DataGrid/foo.ini).

    Send me an email if you want me to keep you updated.

    Cheers

  19. Tridem says:

    Have you done any refactoring yet? I’m excited about the enhancedGrid features.

    How about parsing an array to the ‘query’ parameter which will be automatically parsed to a json notation by the view helper?

    dataGrid(‘myGrid’,
    array(
    ‘query’ => array(‘Title’ => ‘*’)
    ));
    ?>

    parsed:
    query=”{ Title: ‘*’ }”

  20. Tridem says:

    Another nice feature would be the translation lookup for the labels.

    Instead of defining it inside the template

    array(‘field’ => ‘Year’, ‘label’ => ‘Year’, ‘options’ => array(‘width’ => ’10%’))

    you could write
    array(‘field’ => ‘Year’, ‘label’ => ‘en_year_label_from_translation’, ‘options’ => array(‘width’ => ’10%’))

  21. Thanks for you code SpiffyJr. It is much appreciated.

  22. Sunil says:

    Thank you for the code. I am using this and have a question. When I move the mouse on the records, the color of the row doesn’t show up like how the grid would show by default. I just see a colored line. Any clue.

    Thank you!

  23. Ludri says:

    Hi,

    Really nice helpers.
    Integrated these into my project.

    Some modifications i made for myself.

    1. If you have 2 grids in one controller the second one gets all the fields added from the 1.
    For removing this i empty the field list before adding new ones (Since no new DataGrid object is made)
    if (array_key_exists(‘fields’, $attribs)) {
    $this->_fields = array(); // Added
    foreach ($attribs['fields'] as $f) {
    $this->addField(
    }
    unset($attribs['fields']);
    }
    Maybe i just create the helpers the wrong way but worked for me :)

    2. I added Cell edditing js to a method aswell to support cell edditing :D

    Ty and all the best,
    Ludri

  24. Beryl Jaso says:

    Hi there! I just would like to give a huge thumbs up for the excellent info you have here on this article. I will be coming back to your website for more soon.

Leave a Reply