gold stars
| Bill Hunt |
|
Form Building: More Auto-Magic Than You Can Handle?
One thing that's been getting some attention in Cake 1.2 is the building of forms. Recently, I was rewriting some old code from an application which I've been developing on and off for quite a while now, and the difference was remarkable. Consider the following:
(Old)
<form id="TaskEditForm" method="post" action="/tasks/edit/<?=$data['Task']['id']; ?>" onSubmit="return false;">
Here we see a hand-coded form tag with embedded PHP, echoing the ID of the current Task object. Now consider the following code, updated for 1.2, which produces effectively the same output:
(New)<?=$form->create(array('default' => false)); ?>Okay, let's start with what we don't see. You'll notice first that we don't see any reference to a URL; neither a controller nor an action; not even a model name. We also don't see a DOM ID, or any reference to the JavaScript event in the preceeding code. Let's start with the bit about the model. In Cake 1.2, we're transitioning to an approach to form building that is more directly model-oriented, and according to the API, the first parameter to FormHelper::create() is actually supposed to be the name of a model, i.e.:
<?=$form->create('Task', array('default' => false)); ?>However, if you don't provide one, it is assumed to be the default model for the controller (in this case TasksController).
So how does it even know whether this is an add or edit form? Simple: it checks $this->data for a value for the primary key for the given model. If it is set and not empty, it is assumed to be an edit form, otherwise it is an add form. According to convention, 'add' and 'edit' are the default names for form-related actions. Most of the rest of the attributes are pretty straightforward: as with form elements, DOM IDs are now auto-generated for forms themselves, based on the model and type (add/edit).
The last remaining bit is 'default' => false. This is new for both forms and links, and provides you a simple way to disable the default action without actually having to write any JavaScript.
We've also replaced most of the form-related methods in HtmlHelper with roughly equivalent ones in FormHelper. We've also added some wrapper methods to FormHelper, which you can provide with a few hints about how you want your form elements to render, and they take care of everything for you. Here's an example:
(Old)
// Controller:
$this->set('contacts', $this->Task->Contact->generateList());
// View:
<div class="input">
<label for="TaskContactId">Assigned to</label>
<?=$html->selectTag('Task/contact_id', $contacts, $this->data['Task']['contact_id'], null, null, false); ?>
</div>
(New)
// Controller:
$this->set('contacts', $this->Task->Contact->generateList());
// View:
<?=$form->input('contact_id', array('label' => 'Assigned to', 'empty' => false)); ?>
Again, both code listings produce effectively the same output.
FormHelper::input() takes various hints about what type of form element you want it to generate based not only on the information you provide it, but also on the field's model data, and on the view environment itself. Let's first compare the $form->input() part with the $html->selectTag() (you can ignore the controller code, it's the same in both). The first thing is the lack of the 'Task/' part in the field definition that is passed to the method. We're still using that syntax, but if you don't provide a model name, that's okay, because Cake already knows that we're creating a form for the Task model, so it is used automatically.
The next thing you'll notice is that $contacts, our list of options to actually use in the <select /> element, is not present in the view code at all. Since Cake knows that contact_id is a foreign key to another model, it checks the view for a variable called $contacts (the plural of the key name, with the '_id' part removed), and if it is found (and it is an array), Cake uses it as the option list.
The other factors that determine the type of form field that input() will generate are as follows:
- As in the above example, if the field is a foreign key, and the corresponding options variable is specified, a select menu will be rendered.
- If you specify an
'options'key in the second parameter, a select menu will be rendered. - If the field name is
'password'or'passwd', a password field will be rendered. - Text and varchar fields are rendered as textareas and text inputs, respectively.
- Booleans render as checkboxes (and the label is rendered to the right of the element).
- Date, time, and datetime fields all render with the corresponding group of select menus.
- If the field is the primary key of the model, it renders as a hidden field.
'type' key of the $options array, which can be set to any one of the following values: 'hidden', 'checkbox', 'text', 'password', 'file', 'select', 'time', 'date', 'datetime', or 'textarea'. If the 'type' key is unspecified, and the field type is not one of the above, it will render as a textarea.
In addition to rendering the input element itself, input() will also render a label, a wrapper <div /> and any associated validation messages, if present. This is all by default, but you can customize the behavior, as in the following:
<?=$form->input('terms', array('label' => array('text' => 'Terms of service', 'class' => 'title'))); ?>Here, the 'label' key has been defined as an array in order to specify custom rendering options for the <label /> tag. You can also define 'label' as a string (the text of the label itself), or false, to disable the rendering of the label.
The 'div' key works similarly for controlling the rendering of the wrapper div, except that assigning it a string sets the class name.
Now, at the risk of being too meta, there's also a wrapper method for the wrapper method: FormHelper::inputs(). This method takes a single array parameter, which can be indexed, associative, or mixed. The array is simply a list of fields to be rendered with FormHelper::input(). By default, all fields are rendered with the default options, but you can use the associative array syntax to specify options for individual fields, as in the following:
<?=$form->inputs(array('first_name', 'middle_initial' => array('label' => 'MI'))); ?>Here, the first_name field would be rendered with defaults, but the middle_initial field would be rendered with custom label text. Using this syntax it is possible (though not always recommended ;-)) to render an entire form in one line of code.
17 comments
1. to end a form do =$form->end();?>
2. The password function has been deprecated. Do: =$form->input('password', array('label'=>'Password', 'type'=>'password'));?>
Walker: Right now, $form->end is no better or worse than simply writing a closing form tag, but more features are planned for that. As far as deprecated methods, all form-related methods in HtmlHelper are being deprecated, but they'll all have FormHelper equivalents, i.e. FormHelper::password( ).
$form->input knows to put the label on there.
Is there a reason you don't have the input function just output the password field as type="password" rather passing it off:
00355 case 'password':
00356 $out .= $this->password($tagName, $options);
00357 break;
edited:
I also just noticed that when I use $form->input and set "type"=>"password" the type="password" attribute gets doubled.
The password( ) method exists independently of the input( ) method because it is still quite possible that you'd want to generate a password field by itself, without the fluff. And in those cases, it takes less typing to use password( ).
Also, I hope you understand why it would be a Very Bad Idea to try to pack the logic for all form inputs into a single method.
As for the issue you just noticed, I am fixing it, but in the future you can direct such issues to the Cake Trac site, in the form of a ticket.
This is looking good. I have a bunch of code that will render a select list as a group of checkboxes if appearance="full" and multiple="multiple" if you'd like it to merge in. Ala xforms minimal|compact|full. It's a little messy & I haven't done the radio bit yet (I was working with an early version of FormHelper and it wasn't happy with me), but in principal I've found it incredibly useful.
the second problem is with select widgets, when submitting a form and its content doesn't validates, the values for the select doesn't exists anymore.
the problem lies in validation.php, line 271, $varOptions after a validation is empty.
Anyway I've filed a ticket for both problems, I want to publish my brand new cake 1.2 project soon :)
ran into a problem with FormHelper::create() and I'm not sure if it's because I'm not using it right or if it's because it doesn't work like you described it.
(I'm using cake 1.2 fresh out of the trunk)
I use the same view for both add and edit actions. Consequently to create the form I just put
echo $form->create();
and rely on all that nice new automagic you guys have added. Since you said about the first parameter, the model name, "However, if you don't provide one, it is assumed to be the default model for the controller", I didn't bother :)
Problem is that the helper correctly identifies the model I'm working with but generates an add form instead of an edit form:
< form id="AddForm" method="post" action="/people/add">
Whereas if I provide the model for the create method:
echo $form->create('Person');
I get an edit form:
< form id="PersonEditForm" method="post" action="/people/edit/1">
I know it's not a lot of extra typing but every keystroke counts! :) It just had me stumped all evening so I thought I'd mention it.
thanks!
(p.s. could you make the comment box a little wider? it's tough to enter code)
And yes, I'll eventually make the comment box bigger; there are a few design elements of the site that I'd like to tweak, when I get around to it.
Router::connect('/:controller/:action/:id', array(), array('id' => $ID));
The value of the ID will be available in the controller as $this->params['id'].
looks great. But I have two problems.
First, if I want to have a form that is not directly connected to a model (eg., a search functionality with one input filed and a submit button), how can I do that? If I setup a page (handled by PagesController) and include this:
$form->create(null,array('action'=>'/sortiment/suche'));
$form->text('Search/query',array('id'=>"query",'name'=>"query"));
$form->submit('Suchen', array('div'=>false,'id'=>"button"));
(HTML stripped...), I get the following notice:
Notice: Undefined offset: 0 in /srv/www/htdocs/cake/cake/libs/view/helpers/form.php on line 82
I looked up that line and it tried to access the first model used by the controller (which is not set in PagesController). Is there a way to tell the FormHelper *not* to bind itself to a model but work without one?
Second problem is, if I have a HABTM relation and try to build a form, how do I specify the tagname in $form->input(...)? If I use generateFieldNames() in the controller and pass its output to $form->inputs() in the view, it correctly generates the select element and populates it with the available data from the related table, but the related value(s) are not selected (if I use scaffolding, it works).
Thanks in advance,
Marco
In the latest SVN version of 1.2, you can pass false as the first parameter to FormHelper::create(), and this will generate a model-less form.
You should be able to specify HABTM relationships in FormHelper::input the same way as before, i.e. "Tag/Tag".
Great work! It was a little tricky to find out how $form->create() works - maybe because I'm new to Cake and not familar with the webpages to check if there is a newly developed function. ;-)
My question is: I'm using advanced validation with multiple error messages and when I want to use $form->input(), I've to specify the error msg throug the $options['error'] param, right?
Is it planned to integrate multiple advanced validation in CakePHP?
And is it possible to insert an hidden data[Model][id] input field when editing an object? it is more comfortable to save an object without assigning the id-param of the action function to the $this->data variable.
Bye,
Alex

