Date/Time Range Selector Widget for Symfony
I needed to create a date/time widget for use in an sfForm in a symfony project I was working on, and unfortunately there's not currently a widget that I could find to allow this. In this post I show how I solved this problem, creating a widget for selecting a date range, using the jquery date selector from sfFormExtraPlugin.
The end result looks like this:
The reason to create a widget for this is to allow for it to be reused across the site, and of course in other projects too. A similar widget for selecting date ranges already exists (sfWidgetFormDateRange) however it unfortunately doesn't support having time widgets too. My sfWidgetFormDateTimeRange widget is based off this widget. Here's the code, with comments removed:
<?php class sfWidgetFormDateTimeRange extends sfWidgetForm { protected function configure($options = array(), $attributes = array()) { $this->addRequiredOption('from_date'); $this->addRequiredOption('from_time'); $this->addRequiredOption('to_date'); $this->addRequiredOption('to_time'); $this->addOption('template', '<div class="datetime-range"> <span class="from">%from_date% %from_time%</span> <span><strong> - </strong></span> <span class="to">%to_date% %to_time%</span></div>'); } public function render($name, $value = null, $attributes = array(), $errors = array()) { $values = array_merge(array('from_date' => '', 'to_date' => '', 'from_time' => '', 'to_time' => ''), is_array($value) ? $value : array()); return strtr($this->translate($this->getOption('template')), array( '%from_date%' => $this->getOption('from_date')->render($name.'[from_date]', $value['from_date']), '%from_time%' => $this->getOption('from_time')->render($name.'[from_time]', $value['from_time']), '%to_date%' => $this->getOption('to_date')->render($name.'[to_date]', $value['to_date']), '%to_time%' => $this->getOption('to_time')->render($name.'[to_time]', $value['to_time']), )); } public function getStylesheets() { return array_unique(array_merge( $this->getOption('from_date')->getStylesheets(), $this->getOption('from_time')->getStylesheets(), $this->getOption('to_date')->getStylesheets(), $this->getOption('to_time')->getStylesheets() )); } public function getJavaScripts() { return array_unique(array_merge( $this->getOption('from_date')->getJavaScripts(), $this->getOption('from_time')->getJavaScripts(), $this->getOption('to_date')->getJavaScripts(), $this->getOption('to_time')->getJavaScripts() )); } }
As you can probably see from the code above, the concept is simple - our range widget is essentially a container around 4 other widgets which are provided as options. Allowing the definition of the enclosed widgets allows for extra flexibility, as the exact widgets used for the date and time selections can be defined when the widget is constructed.
Using the widget in a sfForm
The code below shows a simple example of how this widget can be used. I've added a little extra 'fluff' around it - to limit the minute drop down to 5 minute intervals, and defined a calendar icon for the jQuery pop-up activator.
<?php class myForm extends sfForm { public function __construct() { $minutes = array(); for($i = 0; $i < 60; $i = $i + 5) { $minutes[$i] = sprintf('%02d', $i); } $this->widgetSchema['span'] = new sfWidgetFormDateTimeRange(array( 'from_date' => new sfWidgetFormJQueryDate(array('image' => '/images/icons/calendar.gif')), 'from_time' => new sfWidgetFormTime(array('minutes' => $minutes)), 'to_date' => new sfWidgetFormJQueryDate(array('image' => '/images/icons/calendar.gif')), 'to_time' => new sfWidgetFormTime(array('minutes' => $minutes)), )); $this->validatorSchema['span'] = new sfValidatorDateTimeRange(array( 'from_date' => new sfValidatorDate(), 'from_time' => new sfValidatorTime(), 'to_date' => new sfValidatorDate(), 'to_time' => new sfValidatorTime(), )); } }
Using the widget with the admin generator
In your model, it's likely you'll have a 'startdate' and 'enddate' field or similar - and as this single widget is for setting both of these fields, a little bit of extra work is needed in your form class. This code below shows using
sfForm::updateObject
and
sfFormObject::updateDefaultsFromObject
to correctly setup the form from an object instance and save the values back to the model.
<?php class EventForm extends BaseEventForm { ... // setup the widget in configure() ... public function updateDefaultsFromObject() { parent::updateDefaultsFromObject(); $this->setDefault('span', array( 'from_date' => $this->getObject()->getDateTimeObject('startdate')->format('Y-m-d'), 'from_time' => $this->getObject()->getDateTimeObject('startdate')->format('H:i'), 'to_date' => $this->getObject()->getDateTimeObject('enddate')->format('Y-m-d'), 'to_time' => $this->getObject()->getDateTimeObject('enddate')->format('H:i'), )); } public function updateObject($values = null) { if (null === $values) { $values = $this->values; } $obj = parent::updateObject($values); $obj->setStartdate(sprintf('%s %d:%d', $values['span']['from_date'], $values['span']['from_time']['hour'], $values['span']['from_time']['minute'] )); $obj->setEnddate(sprintf('%s %d:%d', $values['span']['to_date'], $values['span']['to_time']['hour'], $values['span']['to_time']['minute'] )); return $obj; }
That is all that's needed! The admin generator will call the methods correctly for you and editing should 'just work'! The above is Doctrine specific, however modifying it for Propel or anything else should be reasonably simple.
Conclusion
I chose to write this post to demonstrate the flexibility of the sfForm system, and to show an example of a reasonably rich widget. This will be released as part of a plugin at some point.
When writing forms for your project, don't be afraid to write widget classes that are domain specific. For example, for our internal systems at work, I created a TimicoWidgetFormAccountId widget, which originally was just providing a simple text input, with a specific class attribute for styling. Later, I simply needed to modify this widget to project AJAX style lookups of customers to help people fill in a customers Account ID if they didn't know it. If I didn't create this widget to start with, even though to start with it served little purpose, I'd have had to go back though many form classes and make alterations.
March 11th, 2010 - 21:03
though I think your screenshot is a bit of an understatement this is a very useful post! thanks for sharing
March 11th, 2010 - 21:08
Thanks for your comment.
Yeah – my screenshot is pretty awful… I really do suck at design when it comes to UI’s, I’ll be the first to admit that, and my boss a close second
The way the output rendered is easily stylable, and it’s templated too – and it looks more interesting when you use it because you of course get the nice jQuery calender widget pop up.
March 12th, 2010 - 06:14
well some have their talents
March 30th, 2010 - 07:05
Hey this is great! Do you have an example of the validator class (sfValidatorDateTimeRange)?