We're going to step things up a notch with this one, so hang on for the ride, because for those of you not already well versed in symfony and jQuery, there will be a lot of new information, and it will go pretty quickly, but I will try to make sure to keep everyone caught up. This is another post I hinted towards back when I first wrote about symfony widgets. Right, so, on to the fun!
First things first, let's talk about requirements. My only real requirement is that it must be seamless. We must be able to only implement the widget, and have everything else work exactly as intended. For a date widget, this means when the user clicks that submit button, an array with 'month', 'day', and 'year' must be sent back. Not a string, not JSON, just that basic array.
It's for the above reason that we will need to make a widget on top of the basic jQuery UI Datepicker Widget. The Datepicker alone would just return a string when we submitted the form, which would then require at least a custom validator, if not much more. So to simplify things later, we'll put in more effort now.
There are four things we will be doing in this tutorial to make this happen. Here's a quick overview:
- Set up majaxJqueryPlugin to give us our jQuery/jQuery UI base
- Set up a new module for us to use with the widget tutorials
- Create our widget class
- Create our jQuery UI widget that goes along with our widget class
1. Setup majaxJqueryPlugin
We're using my plugin for two reasons:
- It's my blog, so of course, I'm using my plugin
- We need jQuery UI, both the JavaScript and the CSS, and majaxJqueryPlugin provides that right away
Setting it up is easy:
cd plugins
wget -O majaxJqueryPlugin.zip http://jmather.com/wp-content/plugins/download-monitor/download.php?id=4 |
cd plugins
wget -O majaxJqueryPlugin.zip http://jmather.com/wp-content/plugins/download-monitor/download.php?id=4
Now we add the plugin to our Project Configuration. Open up your config/ProjectConfiguration.class.php file, and place this line somewhere within your setup() function:
$this->enablePlugins('majaxJqueryPlugin'); |
$this->enablePlugins('majaxJqueryPlugin');
And one last step, we need to publish the assets:
./symfony plugin:publish-assets |
./symfony plugin:publish-assets
Alright, now we're ready to move on to the good stuff.
2. Setting up our plugin
Now we need to make a new plugin, and set up a few directories for future use. It's pretty self-explanatory so we'll cut right to the code, and come back afterwards to talk about some of the extras.
./symfony generate:plugin majaxWidgetPlugin
mkdir -p plugins/majaxWidgetPlugin/web/js
mkdir -p plugins/majaxWidgetPlugin/lib/widget |
./symfony generate:plugin majaxWidgetPlugin
mkdir -p plugins/majaxWidgetPlugin/web/js
mkdir -p plugins/majaxWidgetPlugin/lib/widget
We made our plugin, then we made the directory our jQuery plugins will go in, and the directory where we will store our widgets. The -p flag means "no error if existing, make parent directories as needed" which saved us a few mkdir commands.
3. Making our symfony widget
Now open up plugins/majaxWidgetPlugin/lib/widget/majaxWidgetFormDate.class.php and here's our code:
<?php
class majaxWidgetFormDate extends sfWidgetFormDate
{
public function render($name, $value = null, $attributes = array(), $errors = array())
{
$sfContext = sfContext::getInstance();
$resp = $sfContext->getResponse();
$resp->addJavascript('/majaxWidgetsPlugin/js/jquery.majax.dateselector.js');
$id = $this->generateId($name);
$txt = ($this->getOption('can_be_empty') == true) ? 'true' : 'false';
$display = '<div id="'.$id.'"></div>';
$display .= '<div id="'.$id.'_ctrls">';
$display .= parent::render($name, $value, $attributes, $errors);
$display .= '</div>';
$display .= '
<script type="text/javascript">
$(function() {
$(\'#'.$id.'\').majaxdateselector({can_be_empty: '.$txt.'});
});
</script>
';
return $display;
}
} |
<?php
class majaxWidgetFormDate extends sfWidgetFormDate
{
public function render($name, $value = null, $attributes = array(), $errors = array())
{
$sfContext = sfContext::getInstance();
$resp = $sfContext->getResponse();
$resp->addJavascript('/majaxWidgetsPlugin/js/jquery.majax.dateselector.js');
$id = $this->generateId($name);
$txt = ($this->getOption('can_be_empty') == true) ? 'true' : 'false';
$display = '<div id="'.$id.'"></div>';
$display .= '<div id="'.$id.'_ctrls">';
$display .= parent::render($name, $value, $attributes, $errors);
$display .= '</div>';
$display .= '
<script type="text/javascript">
$(function() {
$(\'#'.$id.'\').majaxdateselector({can_be_empty: '.$txt.'});
});
</script>
';
return $display;
}
}
So, to start out, we extend sfWidgetFormDate, as that will make the controls we need to manage a date submission. The only change we need to do, is to override the render function, which will let us add some custom code around the standard output, to allow us to more easily control the display.
The first thing we do in the render function is to include the required JavaScript. Some will say the widget shouldn't know about the JavaScript, but I say, if the widget shouldn't know... who should? We want to make this as transparent as possible, so why should we have to also remember to add a JavaScript include to use this widget?
Next, we grab our generated ID, and figure out our options (i.e. can the date be null) that we can pass along to Datepicker. After that we wrap the whole deal in a containing div, and then wrap the native controls in another specific div. You will see why we did this in a minute. Lastly, we write the JavaScript to activate the widget.
4. Making our jQuery Plugin
As is the standard, I'll show you the code, then highlight portions that are interesting. This file goes in plugins/majaxWidgetPlugin/web/js/jquery.majax.dateselector.js
(function($) {
$.widget('ui.majaxdateselector', {
version: '1.0.0',
eventPrefix: 'majax.dateselector',
options: {
can_be_empty: false,
datepicker_opts: {
}
},
_create: function() {
this.options['id'] = $(this.element).attr('id');
this._hide_real_ctrls();
this._build_facade();
return this;
},
_build_facade: function() {
$(this.element).html('<input size="10" type="text" id="'+this.options['id']+'_display" />');
var tfDisplayUpdate = function(widget) {
return function() {
widget._update_ctrls(this.value);
}
}
$('#'+this.options['id']+'_display').change(tfDisplayUpdate(this));
var m, d, y;
m = $('#'+this.options['id']+'_month').val();
d = $('#'+this.options['id']+'_day').val();
y = $('#'+this.options['id']+'_year').val();
if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
{
$('#'+this.options['id']+'_display').val(this._zero_pad(m, 2)+'/'+this._zero_pad(d, 2)+'/'+y);
}
$('#'+this.options['id']+'_display').datepicker(this.options['datepicker_opts']);
if (this.options['can_be_empty'])
{
$('#'+this.options['id']).append(' <input type="button" id="'+this.options['id']+'_empty" value="Clear" />');
$('#'+this.options['id']+'_empty').button();
var tfClear = function(widget) {
return function() {
widget._clear_display();
return false;
}
}
$('#'+this.options['id']+'_empty').click(tfClear(this));
}
},
_zero_pad: function(num,count)
{
var numZeropad = num + '';
while(numZeropad.length < count) {
numZeropad = "0" + numZeropad;
}
return numZeropad;
},
_clear_display: function() {
$('#'+this.options['id']+'_display').val('');
$('#'+this.options['id']+'_month').val('');
$('#'+this.options['id']+'_day').val('');
$('#'+this.options['id']+'_year').val('');
},
_update_ctrls: function(val) {
var vals = val.split('/');
if ((val == '' || vals.length != 3) && this.options['can_be_empty'])
{
$('#'+this.options['id']+'_month').val('');
$('#'+this.options['id']+'_day').val('');
$('#'+this.options['id']+'_year').val('');
}
var m, d, y;
m = vals[0];
d = vals[1];
y = vals[2];
if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
{
$('#'+this.options['id']+'_month').val(parseInt(m));
$('#'+this.options['id']+'_day').val(parseInt(d));
$('#'+this.options['id']+'_year').val(parseInt(y));
}
},
_hide_real_ctrls: function() {
$('#'+this.options['id']+'_ctrls').css('display', 'none');
},
_show_real_ctrls: function() {
$('#'+this.options['id']+'_ctrls').css('display', null);
},
destroy: function() {
this._show_real_ctrls();
('#'+this.options['id']).html('');
$.Widget.prototype.destroy.call(this);
return this;
}
});
})(jQuery); |
(function($) {
$.widget('ui.majaxdateselector', {
version: '1.0.0',
eventPrefix: 'majax.dateselector',
options: {
can_be_empty: false,
datepicker_opts: {
}
},
_create: function() {
this.options['id'] = $(this.element).attr('id');
this._hide_real_ctrls();
this._build_facade();
return this;
},
_build_facade: function() {
$(this.element).html('<input size="10" type="text" id="'+this.options['id']+'_display" />');
var tfDisplayUpdate = function(widget) {
return function() {
widget._update_ctrls(this.value);
}
}
$('#'+this.options['id']+'_display').change(tfDisplayUpdate(this));
var m, d, y;
m = $('#'+this.options['id']+'_month').val();
d = $('#'+this.options['id']+'_day').val();
y = $('#'+this.options['id']+'_year').val();
if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
{
$('#'+this.options['id']+'_display').val(this._zero_pad(m, 2)+'/'+this._zero_pad(d, 2)+'/'+y);
}
$('#'+this.options['id']+'_display').datepicker(this.options['datepicker_opts']);
if (this.options['can_be_empty'])
{
$('#'+this.options['id']).append(' <input type="button" id="'+this.options['id']+'_empty" value="Clear" />');
$('#'+this.options['id']+'_empty').button();
var tfClear = function(widget) {
return function() {
widget._clear_display();
return false;
}
}
$('#'+this.options['id']+'_empty').click(tfClear(this));
}
},
_zero_pad: function(num,count)
{
var numZeropad = num + '';
while(numZeropad.length < count) {
numZeropad = "0" + numZeropad;
}
return numZeropad;
},
_clear_display: function() {
$('#'+this.options['id']+'_display').val('');
$('#'+this.options['id']+'_month').val('');
$('#'+this.options['id']+'_day').val('');
$('#'+this.options['id']+'_year').val('');
},
_update_ctrls: function(val) {
var vals = val.split('/');
if ((val == '' || vals.length != 3) && this.options['can_be_empty'])
{
$('#'+this.options['id']+'_month').val('');
$('#'+this.options['id']+'_day').val('');
$('#'+this.options['id']+'_year').val('');
}
var m, d, y;
m = vals[0];
d = vals[1];
y = vals[2];
if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
{
$('#'+this.options['id']+'_month').val(parseInt(m));
$('#'+this.options['id']+'_day').val(parseInt(d));
$('#'+this.options['id']+'_year').val(parseInt(y));
}
},
_hide_real_ctrls: function() {
$('#'+this.options['id']+'_ctrls').css('display', 'none');
},
_show_real_ctrls: function() {
$('#'+this.options['id']+'_ctrls').css('display', null);
},
destroy: function() {
this._show_real_ctrls();
('#'+this.options['id']).html('');
$.Widget.prototype.destroy.call(this);
return this;
}
});
})(jQuery);
You know, looking over, it's pretty clear, I feel, what most parts do. The functions "_hide_real_ctrls", and "_show_real_ctrls" hide and show that wrapping div we built around the original controls, so we can keep them in play, but not have to worry about controlling them. The "_build_facade" function builds our fake interactive Datepicker object, and optionally our 'Clear' button, if our date is allowed to be empty. Functions "_clear_display" and "_update_ctrls" to exactly as you would expect. Cure functions "_create" and "destroy" are from the jQuery UI Widget framework, and are called ... can you guess when? đŸ™‚
We're done!
Once you're done, you can replace any sfWidgetFormDate instance with a majaxWidgetFormDate instance, and everything else is handled. A little bit of effort up front, and many rewards down the road!