Improving the forms even more
April 14th, 2009 | by David |After reading Bernhard’s great article about how the symfony forms framework could be enhanced I sat back and thought: how would I like the forms to be? What could be improved to increase my efficiency? Well, after 2 days of brain storming I have found those areas:
- Field grouping
- Field formatters
- Required marks
- Separators
- Submit buttons
- Help texts
- I18n for everything
In my current symfony project at work I’m using at least 9 different forms, tendency rising. There are lots of things that would help making most of them easier to use.
Field grouping
The W3C HTML specification provides a clean solution to grouping form fields: fieldset and legend elements. Symfony doesn’t support them by now, but I think they are crucial, since lots of people use them and they are quite easy to define.
Looking at Bernhard’s code sample let’s suppose we want to group the username and email fields and then the password fields:
public function configure() { $this->addGroup('user_stuff', 'Personal information'); $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)); $this->addField('email') ->setValidator(new sfValidatorEmail()); $this->addGroup('password_stuff', 'Password information'); $this->addField('password', new PasswordField()); $this->addField('password_again') ->setWidget(new sfWidgetPassword()) ->setValidator(new sfValidatorPass()) ->setLabel('Password (again)'); }
In my imagination this should render like this:
<fieldset id="user_stuff"> <legend>Personal information</legend> <!-- The name and email form fields go here --> </fieldset> <fieldset id="password_stuff"> <legend>Password information</legend> <!-- The password form fields go here --> </fieldset>
The magic would be that “addGroup($name, $legendText)” would automatically close a previous group and add all further fields to the current group fieldset. To close a group manually and add the following fields to the global scope a closeGroup() method would be enough.
The grouping HTML code would be defined in the form’s formatter, which can be enhanced easily:
class sfWidgetFormSchemaFormatterList extends sfWidgetFormSchemaFormatter { protected // other stuff $groupFormat = "<fieldset>\n <legend>%legend%</legend>\n %content%</fieldset>"; }
And what if we add a form field later on that belongs to a previous group? Nothing easier than that!
$this->addFieldToGroup('surname', 'user_stuff');
At the end of the article you’ll see why I think it would be useful to have this in the form definition.
Field formatters
Besides the grouping my forms share one more thing: not all the fields are rendered the same way. Some of them require parameters to the label tag or just completely different row stylings. The solution to this at the moment: I have to generate the row myself. The drawback: I’m usually too lazy to include stuff like error rendering and if I change the formatter one day I have to manually alter all the custom form field rows. There’s only one summary for this: it sucks!
Why not enable custom formatters per row? There’s not even a need to define something new, we can re-use the existing formatter code!
public function configure() { // This is how formatters are defined now: // $this->widgetSchema->setFormFormatterName('List'); // Make it more intuitive in the first place: $this->setFormatterName('List'); $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)); $this->addField('email') ->setValidator(new sfValidatorEmail()); $this->addField('password', new PasswordField()) // here it comes! ->setFormatterName('customPasswordFormat'); $this->addField('password_again') ->setWidget(new sfWidgetPassword()) ->setValidator(new sfValidatorPass()) ->setLabel('Password (again)') // and here! ->setFormatterName('customPasswordFormat'); }
The only thing that changes here is that the 2 password fields would be formatted using another formatter. Everything else would stay the same. So simple and yet so powerful…
Required marks
There have been lots of discussions on the mailing lists how required fields should be displayed. My favourite solution would be to put this where all the other formatting stuff goes: to the formatter.
class sfWidgetFormSchemaFormatterList extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<li>\n %error%%label%%required%\n %field%%help%\n%hidden_fields%</li>\n", // other stuff $requiredFormat = "<span>*</span>"; }
Combined with the ability to define custom styles per field this would be a powerful solution.
Ah, there’s something else I would like to notice: defining the required property in the validator is fine from a techie-architect-point-of-view (as are so many other form related issues). But let’s once more see it from the user’s perspective:
public function configure() { // old way $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15, 'required' => true)); // what about this? $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)) ->setRequired(true);
See the difference? I’d personally like to have both options, but the second one would be much more intuitive from the user’s point of view.
Separators
Update 2009-04-16: I’m not sure anymore that separators like I described them here are a good idea. I’ll keep the content unchanged for anyone to understand the discussion that followed. Keep in mind that all this is a kind of brain storming which naturally results in more storm than brain
.
Separators are quite useful in templates when 2 form rows shouldn’t be too close to each other or separated visually using a horizontal line or whatever. In the template this is usually done like this:
<?php echo $form['name']->renderRow() ?> <div class="separator"></div> <?php echo $form['email']->renderRow() ?>
Let’s extend the formatter first (you know what I’m getting at, don’t you?):
class sfWidgetFormSchemaFormatterList extends sfWidgetFormSchemaFormatter { protected // other stuff $separatorFormat = '<div class="separator"></div>'; }
Now we can use it in the form code. And we can use different separators in the same form!
public function configure() { $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)); $this->addSeparator(); $this->addField('email') ->setValidator(new sfValidatorEmail()); $this->addSeparator() ->setFormatterName('anotherSeparatorFormat'); $this->addField('password', new PasswordField());
Submit buttons
Submit buttons belong to the form. They are even input widgets from a technical point of view. So why not add them to the form definition?
public function configure() { $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)); // all the other fields // solution 1 $this->addField('submit') ->setWidget(new sfWidgetSubmit('Submit the form')) ->setValidator(new sfValidatorPass()); // solution 2 $this->addSubmitButton('submit', 'Submit the form');
Together with the ability to use a specific formatter for the submit widget this would make every kind of submit button possible.
Help texts
So far the only possible way to add help texts to your form is passing them to the renderRow() method like this:
echo $form['name']->renderRow(array(), 'Label text', 'Help text');
Why is this separated from the form definition? Especially if the label is defined inside the form? Are there scenarios where the label stays the same but the help text changes? I don’t think so!
Let’s try an alternative approach:
public function configure() { $this->addField('name') ->setWidgetAttributes(array('max_length' => 15)) ->setValidatorOptions(array('max_length' => 15)) ->setLabel('Label text') ->setHelp('Help text');
You see that this is mixing content with structure. But since most of the forms stuff is mixing content with structure (otherwise the labels should never have been in the form definition in the first place) why not allow to do it properly? And this leads to my last point:
I18n for everything
Symfony is great. Symfony has many powerful features. Symfony supports internationalization (short i18n) for everything. Everything? Not so! The form classes strictly defy being i18ned (sic!), making you wrap labels and help texts in __() functions in the template and hoping that the users don’t mind english error messages. Is there a reason for this? Of course: removing the form’s dependency to use an i18n instance. Is this necessary? I don’t think so:
public function configure() { $this->enableI18n(sfContext::getInstance()->getI18n());
Inject the i18n instance into the form. After that, everything could be translated: labels, help texts, messages, errors, even formatters! If you don’t want to use it, don’t. If you need it, make sure you get the damn i18n instance into the form somehow (there are solutions…). I know this is not easy because it changes how most of the widgets are rendered. But I think it would be worth the price.
Why do I want all this stuff?
That’s lots of feature requests. That’s lots of extensions and changes to the forms framework. But why? I can tell you! Because if all of the above features were available, 80% of my form templates could look like this:
<?php echo $form->render() ?>
Wouldn’t that be worth it? Wouldn’t that help the core powers of symfony: KISS and DRY? I know that some projects have different people for generating form code and markup, but I think most work is done by the same developers. At least this would provide an option to do it differently.
Comments are welcome!
9 Responses to “Improving the forms even more”
By naholyr on Apr 14, 2009 | Reply
About grouping : Instead of relying on the order instructions are called, I’d be more explicit.
Or
Both could be supported by the way (even your method could be supported to, but it could be a bit confusing).
For all the other stuff, I’d say +10 to I18N injection, field formatters, submit buttons, +1 to help texts, submit buttons, and required marks, and finally a -1 to separators.
Everything is already a bit messed up in sfForm : we cannot clearly get what is view, what is controller, what is model. And with this we add a little more mess…
Though, there is clearly a flaw about this rendering thing. I’d think more about a template implementation, or a formRenderer with smart markers :
Well, that’s far from perfect but the information about how fields must be arranged in the view must be in a view, or at least in a “renderer”.
By David on Apr 14, 2009 | Reply
Thanks for your suggestions!
You’re right with the grouping, I thought so too. In fact we would even have to go one step further and define some kind of ordering of the fields allowing things like:
or
As for the separators: one could easily implement this using “dummy” label widgets with a custom formatter. If it becomes widely used in that way, there should be an easy accessor to it, because it will be used anyway but could be more comfortable for the programmer. But seen that way it is a minor issue anyway and not important for now, so perhaps it’s better to leave it out.
It’s quite difficult where to draw the line between form and template. Even the input widgets contain code that could be seen as template related, and it’s the same for the formatters. Perhaps the solution could be to replace the formatters with something like partials, but on the other hand this creates new ties between the form component and the templating framework.
By kjaer on Apr 14, 2009 | Reply
Would be my solution.
By David on Apr 14, 2009 | Reply
@kjaer: looks like a valid solution too, although the inGroup() line seems duplicate to me
By Bernhard on Apr 14, 2009 | Reply
I too think that we must take care not to mix up the MVC concepts. Once we add separators and stuff like that to the form, it will be very hard to know what belongs where.
Conceptually, the form is only a collection of fields, but not their presentation. The presentation is handled by the widgets, the formatters and (finally) the template. So any presentation logic belongs there, the form is just the glue to keep all the objects together.
I’ll come back at some more of your points in my next post.
By Andreas on Apr 16, 2009 | Reply
I agree with Bernhard on the separators belonging to the presentation layer, but I think field groups make sense (and are, in fact, needed). Whether a group is later rendered as a fieldset, div, or not at all should be left to the formatter, but groups of fields make sense from a form logic perspective as well.
By David on Apr 16, 2009 | Reply
Today Bernhard and I found a few minutes during the linux weeks fair to discuss the forms issues face 2 face and close in on a general consensus. Besides other things we decided that the separators where a bad idea in the way I added them. He will summarize our current point of view in the following days at webmozarts.com.
By Josh on May 4, 2009 | Reply
Have you implemented any of these suggestions, or these just suggested features for a future version of symfony?
By David on May 4, 2009 | Reply
All these articles at webmozarts.com and here have started as suggestions, but at the moment Bernhard is working on some proof-of-concept code that implements the desired changes and enhancements (in combination with the community feedback). Up to now all the examples he gives in his blog posts are working (so the screen shots are real).
Last Wednesday we met and he told me that his current codebase is a single file holding all the classes without much documentation or error handling, but he is continuously working on it and once it’s becoming mature enough he will publish his work. He is also still waiting for a more detailed response from Fabien Potencier since we want the new forms system to become a part of the framework and thus Fabien’s opinion is important.