jquery

How to build a better date widget in symfony with jQuery

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:

  1. Set up majaxJqueryPlugin to give us our jQuery/jQuery UI base
  2. Set up a new module for us to use with the widget tutorials
  3. Create our widget class
  4. Create our jQuery UI widget that goes along with our widget class

1. Setup majaxJqueryPlugin

We’re using my plugin for two reasons:

  1. It’s my blog, so of course, I’m using my plugin
  2. 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

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');

And one last step, we need to publish the 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

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;
  }
}

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);

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!

How to add jQuery to your Symfony project, part 2.

In part 1 we explored why exactly we may want to build our own jQuery plugin, so now in part two, we will cover exactly how to do so.

Note: these examples are through a bash command line, as I am fairly certain that is the most common way one interacts with Symfony. If you’re using Windows, you’ll get the gist of what to do, however.

Let’s start with initializing the plugin:

./symfony generate:plugin majaxJqueryPlugin

Now we will switch to our plugin directory:

cd plugins/majaxJqueryPlugin

We’re going to be providing web resources, so we will want to make web, js, and css directories. We will also make a temp directory to download files to:

mkdir web
mkdir web/js
mkdir web/css
mkdir temp

Prepare a place to download what we need:

cd temp

Here is our download and copy code. I have decided to host it from here, as jQuery UI doesn’t seem to provide direct download links.

wget "http://jmather.com/wp-content/plugins/download-monitor/download.php?id=3"
unzip jquery-ui-1.8.2.custom.zip
cp js/jquery-* ../web/js/
cp -a css/smoothness/ ../web/css/

Let’s clean up after ourselves…

cd ..
rm -rf temp

Now for the fun part, to make it work. Since I like things to ‘just work’, here’s how I will do it. I will take a page from jQuery Reloaded and use a helper to actually load the files into the response, but instead of making people manually add it where they want it, we will just automatically shove it in if the plugin is enabled!

It’s time to make our helper directory:

mkdir lib/helper

Here is the contents of lib/helper/MajaxjQueryHelper.php:

1
2
3
4
5
6
7
<?php
$jq = '/majaxJqueryPlugin/js/jquery-1.4.2.min.js';
$jqui = '/majaxJqueryPlugin/js/jquery-ui-1.8.2.custom.min.js';
$jquicss = '/majaxJqueryPlugin/css/smoothness/jquery-ui-1.8.2.custom.css';
sfContext::getInstance()->getResponse()->addJavascript($jq, 'first');
sfContext::getInstance()->getResponse()->addJavascript($jqui, 'first');
sfContext::getInstance()->getResponse()->addStylesheet($jquicss, 'first');

For the last little trick before we enable the plugin, open up config/majaxJqueryPluginConfiguration.class.php and add the following code to the initialize() function:

   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);

Your completed majaxJqueryPluginConfiguration.class.php file should look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
/**
 * majaxJqueryPlugin configuration.
 *
 * @package     majaxJqueryPlugin
 * @subpackage  config
 * @author	Jacob Mather
 * @version     SVN: $Id: PluginConfiguration.class.php 17207 2009-04-10 15:36:26Z Kris.Wallsmith $
 */
class majaxJqueryPluginConfiguration extends sfPluginConfiguration
{
  const VERSION = '1.0.0-DEV';
  /**
   * @see sfPluginConfiguration
   */
  public function initialize()
  {
   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);
  }
}

You’re done! Now you just have to go back to your project root, and edit your project configuration to add the plugin, and publish it’s assets.

Here is the line to add to your ProjectConfiguration’s setup() function, just in case you need it:

    $this->enablePlugins('majaxJqueryPlugin');

To publish it’s assets:

./symfony plugin:publish

And now you’re ready to use jQuery throughout your Symfony project!

To download a copy of the majaxJqueryPlugin I have made (and save yourself some work!), use the link below, and you just have to follow the last step to enable the plugin in your project:

Download majaxJqueryPlugin v1.0.0 (109.06 kB zip)

How to add jQuery to your Symfony project, part 1.

The simplest way, hands down, is to simply include the sfJqueryReloadedPlugin in your project. This also enables you to include the sfAdminDashPlugin which makes for easy navigation and a nice login screen for your back-end systems.

However, the problem with jQuery Reloaded, is that it is both old, and for our purposes, incomplete.

Since the last update to jQuery Reloaded, jQuery has progressed to version 1.4.2 (from 1.3.2), and more importantly jQuery UI has upgraded to 1.8.2 (from 1.7.3) and added some very nice and easy pieces we can use in our quest to simplify some of the widgets users are commonly presented.

The other problem we will run into with jQuery Reloaded, is that it doesn’t provide the CSS half of the jQuery UI library, meaning we would not be able to see any widgets we used properly.

So, with all this in mind, you now understand why in Part 2, I will be showing you how to roll your own jQuery plugin for Symfony.

Improved Symfony Widgets

One of the biggest problems with any user interface is ensuring the forms and controls are simple and straight forward to use. While Symfony comes with a great many of widgets that you can use in your forms, some of the most common ones are not as user friendly as one would hope.

Over the next week or two I will be releasing (and posting about) a number of symfony widgets that I’ve powered up with a little (or in some cases, a lot) of javascript. The first step, though, will be a plugin that adds jQuery and jQuery UI into your Symfony stack.

twitter