Using the Unobtrusive Date Picker Widget in CakePHP

By Marcel Manning (mmanning)
The default CakePHP date selection boxes are rather a pain in the rear. Wouldn't it be nice if you could use a Date-Picker in your app's? Well you can, and it's rather easy to implement.

First things first

  • Download and extract the Unobtrusive Date-Picker Widget here: http://www.frequency-decoder.com/2006/10/02/unobtrusive-date-picker-widgit-update
  • Copy the 'css/datepicker.css' file to 'app/webroot/css'
  • Copy the 'js/datepicker.js' file to 'app/webroot/js'
  • Copy the contents of the media folder to 'app/webroot/img/datepicker'
  • Open the datepicker.css file in your 'app/webroot/css' folder. Using the Find/Replace functionality of your editor replace all occurences of '../media' with '../img/datepicker'

Edit the default view

Note: If you do not have a default.thtml file then you will need to create one, for simplicity you can just copy Cake's default one from 'cake/libs/view/templates/layouts' to 'app/views/layouts' and edit the latter.

Put the following before the closing head tag

Download code
<?php echo $html->css('datepicker')."\n"?>
<?php 
echo $javascript->link('datepicker.js')."\n"?>

Note: This may give you an error when you try to load your page, that is because you are trying to use the javascript helper but you have not defined it in your controller. Since it will be used site-wide the easiest thing is to define it in 'app_controller.php'.

Create 'app/app_controller.php' and enter the following:

Download code
<?php
class AppController extends Controller
{
    var 
$helpers = array('Javascript');       

}
?>

Create afterFind and beforeSave methods

So in order to separate the date from the time when using the dateTime format in MySQL you could use a custom query string, but this was not recommended to me so instead I decided to run an after find to create a pseudo-field.

You will need to recombine the fields before it is written back to the database otherwise the information will not be properly saved, this is where the beforeSave method comes into play.

Edit your model and enter the following methods:

Download code
<?php

/*
 * The validation below is optional, just to give you an idea
 * of how to validate these fields.
 */

var $validate = array(
  
'dateOnly' => '/[0-9]{2}[\-\/\.][0-9]{2}[\-\/\.][0-9]{2,4}$/i',
  
'headline' => VALID_NOT_EMPTY,
  
'detail' => VALID_NOT_EMPTY
);

// Extra form validation, since VALID_NOT_EMPTY does not work on
// these fields.
function validates() {
  
$event $this->data['Event'];
  if(empty(
$event['date_hour']) || empty($event['date_meridian']) || empty($event['date_meridian']))
     
$this->invalidate('date');

  
$errors $this->invalidFields();
  return 
count($errors) == 0;
}

/*
 * The validation above is optional, just to give you an idea
 * of how to validate these fields.
 */

function afterFind($results) {
   
// Create a dateOnly pseudofield using date field.
       
foreach ($results as $key => $val) {
           if (isset(
$val['Event']['date']))
               
$results[$key]['Event']['dateOnly'] = date('m-d-Y',strtotime($val['Event']['date']));  
       }
   return 
$results;
}

function 
beforeSave()
{
  
// Convert 12 hour to 24 hour
  
if($this->data['Event']['date_meridian'] == 'pm')
     
$hour $this->data['Event']['date_hour'] + 12;
  else
     
$hour $this->data['Event']['date_hour'];

  
// Get month day and year from date string
  
$timestamp strtotime(str_replace('-','/',$this->data['Event']['dateOnly']));
  
$month date('m',$timestamp);
  
$day date('d',$timestamp);
  
$year date('Y',$timestamp);
  
  
$this->data['Event']['date'] = date('Y-m-d H:i:s'mktime(
                  
$hour,
                  
$this->data['Event']['date_min'],
                  
null,
                  
$month,
                  
$day,
                  
$year));
  return 
true;
}

?>

Note: Be sure to substitue ['Event'] with your App name and ['date'] with the field in the database that contains the dateTime value.


Edit your view

You will need to modify you view to display the new format, below is what I used. Feel free to make any necessary changes for your application.

Example view:

Download code
<table cellpadding="0" cellspacing="0" class="view">
    <tr>
        <td><span class="title"><?php echo $form->labelTag('Event/dateOnly''Date');?></span></td>        
        <td>
            <?php echo $html->input('Event/dateOnly', array('size' => '15''class' => 'w8em format-m-d-y divider-dash highlight-days-12 no-transparency'));?>
        </td>
        <td><?php echo $html->tagErrorMsg('Event/dateOnly''Please select the Date.');?></td>
    </tr>
    <tr>
        <td><span class="title"><?php echo $form->labelTag('Event/date''Time');?></span></td>        
        <td>
            <?php echo $html->hourOptionTag('Event/date'); ?>
            <?php echo $html->minuteOptionTag('Event/date'); ?>
            <?php echo $html->meridianOptionTag('Event/date'); ?>
            <?php echo $html->checkbox('Event/allday'null, array());?>
            <?php echo $form->labelTag('Event/allday''Allday');?>            
        </td>
        <td><?php echo $html->tagErrorMsg('Event/date''Please select the Time.');?></td>    
    </tr>
</table>

You should now have a fully working date picker. If you are having problems, please post your comments and I will try to help resolve them.

Screenshots:

http://www.nexgentec.com/bakery/date-picker.jpg http://www.nexgentec.com/bakery/date-picker2.jpg

 

Comments 642

CakePHP Team Comments Author Comments
 

Question

1 Need help with date picker view...

My add view looks like this and I am not sure what to change to make things look or work right. As of this moment the datetime field looks the same as it did before. I'm also using the Anno Domini calendar so the events model code doesn't need to be edited from what you gave above, I think...
If you could look at the code and make a suggestion or anyone for that matter I would really appreciate the help.

create('Event',array('name'=>'eventAddForm'));?>
input('user_id',array('label'=>'User ', 'options' => $this->requestAction('/users/findUsersOptions')));?>
input('headline',array('label'=>'Headline '));?>
input('date',array('label'=>'Date and Time '));?>
input('location',array('label'=>'Location '));?>
input('detail',array('type'=>'textarea','rows'=>'5','cols'=>'40','label'=>'Details '));?>
button('Add Event',array('form'=>'eventAddForm'));?>
P.S.
If you want to email me that would work too, valendesigns@gmail.com
Posted May 27, 2008 by Derek Herman
 

Comment

2 All working now

I got this to work but there were more changes involved other than just the view.

So if you're using cake 1.2 then things have changed and so when you make a form the regular way of $form->create() then the right way to do the view parts would be:

input('Event/dateOnly', array('size' => '15', 'class' => 'w8em format-m-d-y divider-dash highlight-days-12 no-transparency'));?>
hour(); ?> minute(); ?> meridian(); ?>
and in the model class you need to change the [date_hour] [date_min] and [date_meridian] to [hour] [min] and [meridian] and things should work properly now. Hope this helps anyone who couldn't get this to work.
Posted May 27, 2008 by Derek Herman
 

Question

3 did you ever try

the other great stuff on that side?
like the paginator/sorting-table:
http://www.frequency-decoder.com/demo/table-sort-revisited/paginate/
i don't know how easy this one is to get along with cakePHP
Posted Jun 30, 2008 by Mark
 

Comment

4 This does not appear to work for 1.2

I'm struggling to see how to get this to work. It appears to me that there are many missing elements that cause errors.

First, I had to add some code to handle the case that the date is null in the afterFind method of the model.

Then I'm completely clueless as to how the date_hour and date_meridian that are referenced in the validates method are created. Either they're missing, or I'm missing somthing else.

So by including the code:

<?=$form->input('dateOnly', array('size' => '15', 'class' => 'w8em format-m-d-y divider-dash highlight-days-12 no-transparency'));?>

I get HTML like:

<div class="input text">
<label for="DateOnly">Date Only</label>
<input name="data[Job][dateOnly]" type="text" size="15" class="w8em format-m-d-y divider-dash highlight-days-12 no-transparency" value="11-24-2008" id="JobDateOnly" />
</div>

Which I'm guessing means the class is getting added to the wrong HTML tag.

Any suggestions?
Posted Oct 12, 2008 by Rob Weaver
 

Comment

5 OK found my problem

Found my problem - the JS file got placed in the wrong directory when I pulled it out of the zip file (saved it to js/js instead of js).

Thank goodness for Firebug - I was able to browse the DOM and see the Cake error there. Since it couldn't find the JS file, it was replacing the JS with HTML that was intended to show me the error.

Unfortunately, the browser just swallows that error, since it is embedded between script tags.
Posted Oct 12, 2008 by Rob Weaver
 

Comment

6 Excellent documentation

this is really a perfect working code and its so well documented!!
it really helped me a lot!!!!thanx Marcel Manning (mmanning) for this code and documentation.
Posted Jan 2, 2009 by Tahmeed Alam
 

Comment

7 RE: the other great stuff on that side?

the other great stuff on that side?
like the paginator/sorting-table:
http://www.frequency-decoder.com/demo/table-sort-revisited/paginate/
i don't know how easy this one is to get along with cakePHP

It looks nice, but I would not recommend using it with CakePHP, since you can easily have the same functionality with AJAX.

The author states: "the script was developed for “worst case scenarios” and if at all possible, you should use an Ajax or simple server-side pagination solution instead." which I agree with.
Posted Jan 24, 2009 by Marcel Manning
 

Comment

8 RE: Excellent documentation

this is really a perfect working code and its so well documented!!
it really helped me a lot!!!!thanx Marcel Manning (mmanning) for this code and documentation.

Thank You Tahmeed Alam, seems as though this is still the only Date Picker article in the Bakery.
Posted Jan 24, 2009 by Marcel Manning
 

Comment

9 Date fields the Cake 1.2 way!

Just added this widget to a site I am currently developing and it worked a dream!

I wasn't keen on having to add the afterFind/beforeSave methods to all my models that used this so did some digging and I have it working a bit more Cake-like .. although I'm sure it could be better.

Drop the following into your views and one date picker will update separate day-month-year select lists.

echo $form->day('dateField', date('d'), array('id' => 'FieldId-dd'), false)." - ";
echo $form->month('dateField', date('m'), array('id' => 'FieldId-mm'), false)." - ";
echo $form->year('dateField', '1980', date('Y'), null, array('id' => 'FieldId', 'class' => 'w8em split-date divider-dash highlight-days-12 no-transparency'), false);

Substitute 'dateField' for your date field and 'FieldId' for your field's id (tends to follow ModelFieldName format) and hey presto!

No need to do any model trickery as the date fields are now formatted as Cake likes them.

This works by specifying the 'split-date' class which breaks the date across 3 text fields or select lists and the widget does the hard work for you as long as you only add the class information to the year field and give your fields the correct ids:

  • day: FieldId-dd
  • month: FieldId-mm
  • year: FieldId (no additional)

Unfortunately you can't use Cake's automagic $form->input as it doesn't allow you to specify 3 different ids (it takes the fields id and adds Day, Month, Year) and adds the class declaration to each part of the date (resulting in 3 date pickers).

This results in a bit more code in the view, especially when you have to add in labels and divs etc. but I can handle that in favour of not having to add the extra methods into the model to create a false date field to work on.

Hope this proves useful,
Posted Feb 8, 2009 by Paul Gardner
 

Comment

10 Using additional date pickers

This is true Paul >>>Just added this widget to a site I am currently developing and it worked a dream!

Yours seems to be the easier choice rather than adding the afterFind/beforeSave methods in 1.2, I tried it and it worked. Just to add additional info to those who will be trying this with more than one date picker.

The solution is easy this field (FieldId) will give one date picker:

* day: FieldId-dd
* month: FieldId-mm
* year: FieldId (no additional)

to add additional date picker just append 0-9 etc. to the FieldId:

* day: FieldId0-dd
* month: FieldId0-mm
* year: FieldId0 (no additional)

will give you additional instance of the date picker.



Posted May 12, 2009 by Ephraim Gariguez
 

Question

11 Adding time and future years?

Just added this widget to a site I am currently developing and it worked a dream!

I wasn't keen on having to add the afterFind/beforeSave methods to all my models that used this so did some digging and I have it working a bit more Cake-like .. although I'm sure it could be better.

Drop the following into your views and one date picker will update separate day-month-year select lists.

echo $form->day('dateField', date('d'), array('id' => 'FieldId-dd'), false)." - ";
echo $form->month('dateField', date('m'), array('id' => 'FieldId-mm'), false)." - ";
echo $form->year('dateField', '1980', date('Y'), null, array('id' => 'FieldId', 'class' => 'w8em split-date divider-dash highlight-days-12 no-transparency'), false);

Substitute 'dateField' for your date field and 'FieldId' for your field's id (tends to follow ModelFieldName format) and hey presto!

No need to do any model trickery as the date fields are now formatted as Cake likes them.

This works by specifying the 'split-date' class which breaks the date across 3 text fields or select lists and the widget does the hard work for you as long as you only add the class information to the year field and give your fields the correct ids:

  • day: FieldId-dd
  • month: FieldId-mm
  • year: FieldId (no additional)

Unfortunately you can't use Cake's automagic $form->input as it doesn't allow you to specify 3 different ids (it takes the fields id and adds Day, Month, Year) and adds the class declaration to each part of the date (resulting in 3 date pickers).

This results in a bit more code in the view, especially when you have to add in labels and divs etc. but I can handle that in favour of not having to add the extra methods into the model to create a false date field to work on.

Hope this proves useful,

This works great. Appreciate the guidance.

I was wondering if you could provide an example of add/incorporating the time selection for your Cake-like date picker and allowing for years in the future?

Thanks for any help and your time!
Posted May 13, 2009 by Todd Cornett
 

Comment

12 Jumped the gun...

Found the answer to my own question:


echo $form->hour('dateField', true, date('H') ) . " : ";
echo $form->minute('dateField', date('i') );

Thanks for humoring me!
Posted May 13, 2009 by Todd Cornett
 

Comment

13 Blank email notifications

Found the answer to my own question:...
Glad you found a solution :)

I would have replied, but the article comment email notifications from the site are showing blank and I did not know which article someone had commented on?!?
Posted May 14, 2009 by Paul Gardner
 

Comment

14 Error

FireBug show this error:

uncaught exception: Index or size is negative or greater than the allowed amount (NS_ERROR_DOM_INDEX_SIZE_ERR)
[Break on this error] var datePickerController=(function dateP...;script.setAttribute("charset","utf-8");

Any idea?
Posted Jun 5, 2009 by Celso Fontes