Archive for April 10th, 2011

Zend_Form_Element_Daterange with jQuery UI datepicker

It’s pretty easy to create a wrapper around Zend_Form_Element_Text to make a datepicker, but creating a good, smart daterange picker is a little more tricky.

The only two requirements are jQuery UI and that you have some way of injecting JavaScript into your site layout (I use a $this->layout()->headJavascript).

Other than that, this should be pretty plug-and-play.

Here are the 3x files you’ll need:
1. Your view script for the actual element (check in here for how I’m dealing with the JavaScript):
/application/views/scripts/misc/form/element/daterange.phtml

2. The actual element
/library/M/Form/Element/Daterange.php

3. And a filter to parse out the actual daterange when you do $form->getValues():
/library/M/Filter/Daterange.php

Have fun!

/library/M/Form/Element/Daterange.php
<?php

class M_Form_Element_Daterange extends Zend_Form_Element_Select {
public function init() {

$this->setDecorators(array(array('ViewScript', array(
'viewScript' => 'misc/form/element/daterange.phtml',
'viewModule' => 'default',
'class' => 'form element'
))));

return parent::init();
}
}

/library/M/Filter/Daterange.php

<?php

class M_Filter_Daterange implements Zend_Filter_Interface {

private $_param = null;

public function __construct($param, $options = array()) {
if (!is_string($param) || strlen($param) < 1) {
throw new Zend_Filter_Exception("param must be a string, and must have a length");
}
$this->_param = $param;
}

/**
* (non-PHPdoc)
* @see Zend_Filter_Interface::filter()
*/
public function filter($val) {
if ($val != 'custom') {
$rangestring = self::stringToRange($val);
if ($rangestring) {
return $rangestring;
}
}
if ($this->_param === null) {
throw new Zend_Filter_Exception("Must instantiate this class and pass a valid parameter");
}
$range = Zend_Controller_Front::getInstance()->getRequest()->getParam($this->_param, false);
$start = strtotime(Zend_Controller_Front::getInstance()->getRequest()->getParam($this->_param . '_start', false));
$end = strtotime(Zend_Controller_Front::getInstance()->getRequest()->getParam($this->_param . '_end', false));

if (!$start || !$end) {
return false;
}
return array(
'start' => date('Y-m-d', $start),
'end' => date('Y-m-d', $end),
);

}

/**
* Converts a known relative date range string to an array of dates, returns false on failure
*
* @param string $datestring
* @return array|bool
*/
static public function stringToRange($datestring) {
switch ($datestring) {
case 'yesterday':
$daterange = array(
'start' => date('Y-m-d', strtotime('yesterday')),
'end' => date('Y-m-d'),
);
break;
case 'weektodate':
$daterange = array(
'start' => date('Y-m-d', strtotime('sunday this week')),
'end' => date('Y-m-d'),
);
break;
case 'lastweek':
$daterange = array(
'start' => date('Y-m-d', strtotime('sunday last week')),
'end' => date('Y-m-d', strtotime('saturday -2 weeks')),
);
break;
case 'monthtodate':
$daterange = array(
'start' => date('Y-m-d', strtotime('first day of this month')),
'end' => date('Y-m-d'),
);
break;
case 'lastmonth':
$daterange = array(
'start' => date('Y-m-d', strtotime('first day of last month')),
'end' => date('Y-m-d', strtotime('last day of last month')),
);
break;
case 'yeartodate':
$daterange = array(
'start' => date('Y-m-d', strtotime('first day of this year')),
'end' => date('Y-m-d'),
);
break;
case 'lastyear':
$daterange = array(
'start' => date('Y-m-d', strtotime('first day of last year')),
'end' => date('Y-m-d', strtotime('last day of last year')),
);
break;
case 'all':
$daterange = array(
'start' => date('Y-m-d', strtotime('1970-01-01')),
'end' => date('Y-m-d'),
);
break;
default:
return false;

}
return $daterange;
}

}

/application/views/scripts/misc/form/element/daterange.phtml

<?php
/**
* Output a daterange element
*
*/

/**
* @var Zend_Form_Element
*/
$element = $this->element;
$opts = $element->getDecorator('ViewScript')->getOptions();

ob_start();
?>
<script>
$(document).ready(function() {
var dates = $(<?=json_encode('#' . $element->getName() . '_start, #' . $element->getName() . '_end')?>).datepicker({
changeMonth : true
, onSelect : function( selectedDate ) {
// don't let end date be after start date
var option = this.id == <?=json_encode($element->getName() . '_start')?> ? "minDate" : "maxDate",
instance = $(this).data("datepicker"),
date = $.datepicker.parseDate(
instance.settings.dateFormat || $.datepicker._defaults.dateFormat
, selectedDate
, instance.settings
);
dates.not(this).datepicker("option", option, date);
}

});

$(<?=json_encode('#' . $element->getName())?>).change(function() {
if ($(this).val() == 'custom') {
$(<?=json_encode('#_custom_daterange_' . $element->getName() . '_start')?>).show();
$(<?=json_encode('#_custom_daterange_' . $element->getName() . '_end')?>).show();
} else {
$(<?=json_encode('#_custom_daterange_' . $element->getName() . '_start')?>).hide();
$(<?=json_encode('#_custom_daterange_' . $element->getName() . '_end')?>).hide();
}
});
});
</script>
<?php
$this->layout()->headJavascript .= ob_get_clean();

?>
<table id="_custom_daterange_<?=htmlentities($element->getName())?>">
<tr>
<td><?=$element->getLabel()?></td>
<td><?php
echo $this->{$element->helper}(
$element->getName(),
$element->getValue(),
$element->getAttribs(),
$element->getMultiOptions()
);?>
</td>
</tr>
<tr style="display:none" id="_custom_daterange_<?=htmlentities($element->getName())?>_start">
<td><label for="<?=htmlentities($element->getName())?>_start">Start: </label></td>
<td><input id="<?=htmlentities($element->getName())?>_start" name="<?=htmlentities($element->getName())?>_start" type="text"></td>
</tr>
<tr style="display:none" id="_custom_daterange_<?=htmlentities($element->getName())?>_end">
<td><label for="<?=htmlentities($element->getName())?>_end">End: </label></td>
<td><input id="<?=htmlentities($element->getName())?>_end" name="<?=htmlentities($element->getName())?>_end" type="text"></td>
</tr>
</table>
<div class="error bold"><?=$this->formErrors($element->getMessages())?></div>
<div class="hint"><?=$element->getDescription()?></div>