12.24.07

7 Days of Symfony 1.1 - Forms, Widgets and Validators (Day7)

Posted by ryan in symfony


Note: Many things here may be out of date. This article is up to date only through early 2008 (which was a while ago).

After 6 days, we've made several forms, handled their processing, and investigated nearly all the widgets and validators. Today we turn our focus to a silent, but important, aspect of the forms: the decorator.

The job of the decorator is simple: To format the labels and form elements. Let's imagine our form without them. Imagine we have 3 input text fields: first_name, last_name and address. Imagine that we echo our form exactly how we have been throughout this seven day tutorial:

<?php echo $form; ?>

When this is called, our form iterates through its widgets and renders their html. Without decorators, the output would like something like this:

<label for="first_name">First Name</label><input type="text" name="first_name" id="first_name" />
<label for="last_name">Last Name</label><input type="text" name="last_name" id="last_name" />
<label for="address">Address</label><input type="text" name="address" id="address" />

The truth is, I'm being too kind, I included line breaks after every input element, which wouldn't necessarily even be there without decorators. Now, there's nothing wrong with the above output. With the proper wrapping div and css, we could make this work quite nicely. Of course our job would become much more difficult when you picture field-specific error messages sandwiched inside the code. Thus, the need to "decorate" our forms is born.

Symfony's new form system comes standard with 2 decorators (known as form formatters inside Symfony) out of the box. The first, and default decorator, is called 'table' and (you guessed it) centers around the use of tables. The second, called list, uses lists to present the table. The former is the sfWidgetFormSchemaFormatterTable class and the latter the sfWidgetFormSchemaFormatterList class. Each of these inherit from the base "form formatter" class called sfWidgetFormSchemaFormatter. Let's take a look at how we would change our existing forms to use either the table (already used by default) or the list decorators.

AuthorForm.class.php

public function configure()
  {  	
  	$this->widgetSchema->setFormFormatterName('list');
  	//or
  	$this->widgetSchema->setOption('form_formatter', 'list');
  }

Only one of the two above bottom lines are necessary. I simply added the second line to illustrate that the decorator is simply an option (remember widget options?) on the widget schema (which inherits from the sfFormWidget class).

So, probably like many of you, I have a slightly different idea of how forms should be formatted. In particular, there are 4 parts of each widget that we need to decorate:

  1. the label
  2. the form element itself
  3. the validation error (when it shows)
  4. the optional helper text (which, to this point, I've neglected to mention)

The "creation" of a decorator is as simple as defining the following variables (printed here with the default values for the table decorator":

    $rowFormat                 = "<tr>
  <th>%label%</th>
  <td>%error%%field%%help%%hidden_fields%</td>
</tr>
",
    $helpFormat                = '<br />%help%',
    $errorRowFormat            = "<tr><td colspan=\"2\">
%errors%</td></tr>
",
    $errorListFormatInARow     = "  <ul class=\"error_list\">
%errors%  </ul>
",
    $errorRowFormatInARow      = "    <li>%error%</li>
",
    $namedErrorRowFormatInARow = "    <li>%name%: %error%</li>
",
    $decoratorFormat           = "<table>
  %content%</table>";

Now, there are two true ways to create a custom formatter. The first would be to use setter methods in the following way.

inside a Form model class

$decorator = new myWidgetFormSchemaFormatterCustom($this->getWidgetSchema());
$decorator->setDecoratorFormat($val);
$decorator->setsetErrorListFormatInARow($val2);
...

So on and so forth. Each of the above properties have a setter with which you can modify your decorator right alongside your form modifications. However, since we want to define a new type of decorator, and we don't want to have to remodify it for each form we create, we're going to a new decorator class that we can easily apply to any form. Now, since I haven't seen anyone else do this yet, I'm going to have to guess at exactly where Fabien intends this class to be located. First, let's suppose I have a field called "first_name." Let's define what we'll want the ending form row to look like:

<div class="form-row">
    <ul class="error_list">
    	</li>Required</li>
  </ul>
  <label for="author_first_name">First name</label><input type="text" name="author[first_name]" id="author_first_name" />
</div>

You get the idea. Next, let's set everything up. To start, let's simply mirror the table form formatter as our custom decorator, and get our author form to being using it. Since I haven't seen a custom validator created yet, I don't know exactly where Fabien intends for the custom class to be located. I'll follow the Symfony library example, but it may not be perfect. Create a new directory in your project lib/widget and new file in that directory called myWidgetFormSchemaFormatterCustom.class.php. In that file:

class myWidgetFormSchemaFormatterCustom extends sfWidgetFormSchemaFormatter
{

  protected
    $rowFormat       = "<tr>
  <th>%label%</th>
  <td> custom %error%%field%%help%%hidden_fields%</td>
</tr>
",
    $helpFormat      = '<br />%help%',
    $errorRowFormat  = "<tr><td colspan=\"2\">
%errors%</td></tr>
",
    $errorListFormatInARow     = "  <ul class=\"error_list\">
%errors%  </ul>
",
    $errorRowFormatInARow      = "    <li>%error%</li>
",
    $namedErrorRowFormatInARow = "    <li>%name%: %error%</li>
",
    $decoratorFormat = "<table>
  %content%</table>";
}

Let's next hook up our author form from previous days to use our new decorator. Notice above that I've placed the word "custom" inside the of the $rowFormat variable so that we'll be able to initially distinguish the different between our custom decorator and that default table decorator. Inside the AuthorForm class:

public function configure()
  {
  	...
  	// Let's add a helper to one of our fields so that we can see it in our decorator
  	$this->widgetSchema->setHelp('email', 'Your contact email address');
  	...
  	$decorator = new myWidgetFormSchemaFormatterCustom($this->getWidgetSchema());
  	$this->widgetSchema->addFormFormatter('custom', $decorator);
  	$this->widgetSchema->setFormFormatterName('custom');
  }

Make sure you've cleared your cache, or our new form formatter class won't be autoloaded. What you should see is the same form, except with the word "custom" starting every row. That's proof that our custom decorator is being used.

All decorators are essentially the same thing - simple overrides of the above 7 protected fields that define how a row of a form is printed. Using our custom decorator is just as simple. First, we construct our new $decorator object by calling the constructor of our new decorator class. This takes no arguments, and serves no purpose. Next, we use the addFormFormatter function of our widget_schema to make the widget_schema notice our new decorator. This method's first argument is a name for the decorator, so that we can refer to it later. The final step is to use the setFormFormatterName method to set the name of the decorator to use to the name we just gave our custom form formatter. In this example, the name is "custom". The placement and naming of our class is unimportant - I just tried to stay consistent with the Symfony library example.

The only thing left to do is actually configure our class properties so that our output matches our target output listed above.

  protected
  	// this says to surround our element with a div of class "form-row".
  	// then print out the error, label, form field, help text, and finally the hidden fields
  	// new lines (
) are included for readable html
    $rowFormat			 = "<div class=\"form-row\">
  %error%
  %label%%field%
  %help%
%hidden_fields%
</div>
",
    // this defines the exact format of the above %help%. This says to wrap it in a div of class form_helper.
    // The %help% in this case defines that actual help text (if any) that's given
    $helpFormat      = '<div class="form_helper">%help%</div>',
    // Certain errors do not pertain to any specific widget. In these cases, the errors are printed on their own row
    // This defines the format of how to set up that row
    $errorRowFormat  = "<div>
%errors%</div>
",
    // this is the wrapper always uses around errors.
    $errorListFormatInARow     = "  <ul class=\"error_list\">
%errors%  </ul>
",
    // This defines how to print the actual error in normal situations
    $errorRowFormatInARow      = "    <li>%error%</li>
",
    // This defines how to print the actual error in situations where the error does not belong to a specific widget - is printed at the top of the form
    $namedErrorRowFormatInARow = "    <li>%name%: %error%</li>
",
    // We didn't cover it, but you can embed forms inside other forms.
    // When you do, the following is used to "wrap" your form. In the table example, this is a <table> </table> value.
    $decoratorFormat = "<div>
  %content%</div>";

Hopefully the above descriptions helped out. The truly important fields are $rowFormat, $helpFormat, $errorListFormInARow, and $errorRowFormatInARow. The others are important too, but don't always come in to play (they're not used in our examples). If you don't really need to override a value, simply omit it to use the parent's value. In our example, we could've omitted $errorListFormatInARow, $errorRowFormatInARow, and $namedErrorRowFormatInARow, as the above values are default.

Now, to pretty up our form a bit, let's add in some css:

.form-row
{
  clear: both;
  padding: 10px;
  border-bottom: 1px solid #bbf;
  background-color: #fff;
}
label
{
  display: block;
  padding: 0 1em 3px 0;
  float: left;
  text-align: left;
  width: 10em;
  color: #666;
  font-weight: normal !important;
}
div.form_helper {
    clear: both;
    color: #333;
    font-style: italic;
}
.error_list {
    color: #ff0000;
}

That's it! Of course, none of the above css is required - but it gives you an idea of what I was going for. You can now take your custom decorator and modify the format to fit the form formatting to which you're accustomed.

Well, that's all I have for you. I hope the previous seven days have been informative. I tried to cover everything important, without weighing down with things that are more advanced (and will come later!). Two things I DID neglect to cover in a timely fashion included the csrf token (which prevents cross-site request forgery attacks) and the helper text that you can apply to each element. These are both covered throughout the tutorial, but were given less emphasis than I originally intended.

So what other advanced things will we be doing in the future. These things might include the aforementioned embedding of forms or the re-ordering of widgets inside our widget schema (so that they display in a different order). There are also a few schema validators which I chose not to cover. We'll also probably see many different attempts at sufficiently replacing the form_tag helper so that the form tag is once again a function (and not hand-written). This will take advantage of the form's isMultipart() function and vary greatly depending on the need of ajax and the current javascript library. By the way, that's another big change for Symfony - no more javascript helpers. They've been purposely dropped by Symfony so that Symfony is no longer prototype/scriptaculous - dependent. No worries though, where's there is a void, the Symfony community will fill it. I imagine that several groups - Prototype, jQuery etc - will be creating a library (widgets?) to handle the job that javascript helpers once held.

Thanks for reading!

Thanks for the shares!
  • StumbleUpon
  • Sphinn
  • del.icio.us
  • Facebook
  • TwitThis
  • Google
  • Reddit
  • Digg
  • MisterWong
Posted by Volker on 2009-02-27
I tried your approach to modify my output and create my own formatter with symfony v1.2.4, but it doesn't seem to work anymore. I found several other websites where similar examples where posted, but none of them worked either. For instance, changing the $rowFormat shows no effect at all, even completely unsetting it ($rowFormat = '').
Posted by Ryan on 2009-02-27
Hmm, I just checked out the code here and everything still seems to work for me (also using 1.2.4). I did have an error with the constructor of my decorator (missing the first argument), but this should have thrown an exception for you. Other than that (fixed now), my custom decorator is working as it should. Perhaps you're not using the rendering method renderRow()? If you're using render() instead, then I don't think that the rowFormat will be used.

-Ryan
Posted by Marvin on 2009-06-30
how do i go about emailing the content of the forms?
Posted by Ryan on 2009-07-01
@Marvin-

Emailing is an entirely different issue and there are several ways to solve it in Symfony. I'll point you to one method highlighted in the Jobeet tutorial:

http://www.symfony-project.org/jobeet/1_2/Doctrine/en/16#chapter_16_sending_emails
Posted by Rodrigo Gregorio on 2009-08-04
IMPORTANT!!!

VERSION 1.2 SEE http://www.symfony-project.org/api/1_2/sfWidgetFormSchema#method_setdefaultformformattername
Posted by Hardeep Khehra on 2009-09-22
Where would I be without your blog Ryan.

To confirm. This works with symfony 1.2.8 without a problem. Just need to pass the first argument as mentioned in one of your replies.
Posted by awesemo on 2010-01-26
Is there anyway we can override the %Label% format.
I just wanted to format it as My Label
Posted by myclicker.info on 2010-09-26
Everything works with Symfony 1.4.7

Just added
$decorator = new myWidgetFormSchemaFormatter($this->getWidgetSchema());
$this->widgetSchema->addFormFormatter('custom', $decorator);
$this->widgetSchema->setFormFormatterName('custom');
//sfWidgetFormSchema::setDefaultFormFormatterName('list');

to the bottom of
/lib/form/doctrine/JobeetJobForm.class.php :: configure()
Posted by Sebastian on 2011-02-16
me a servido mucho : )