gold stars

Bill Hunt Gold Star

Form Building: More Auto-Magic Than You Can Handle?

posted by: Nate :: Dec 21st 2006, 23:12

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.
Of course, you can always manually specify the type of element rendered using the '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

bottom

1. Tijs :: on Dec 23, 2006 Wow, those are some great changes. Now since the add and edit forms are now almost complete copies will it be possible to have one action and one view or two actions and one view for both add and edit in 1.2? i always found it a pain to copy paste the edit form, change is slightly and then keep both versions in sync for data model changes...
2. Nate :: on Dec 25, 2006 Well, it's always been possible to combine add and edit views with a couple of if blocks, but combining controller actions is a little bit trickier, and not something you'd really want to do as your application begins to scale, and you add concerns like authentication and access control.
3. Mariano Iglesias :: on Dec 25, 2006 Those are some cool features. Is there any idea on how big of an impact this change is for CakePHP 1.1.x based applications? I mean, I've seen that CakePHP 1.2 renders a deprecated debug message (which we can easily turn off naturally) on every $html->form(), $html->input(), etc. and from your code it doesn't seem that hard to update from old HtmlHelper based form tags to new FormHelper tags, but how big of an impact was for you to update your code?
4. Walker Hamilton :: on Dec 26, 2006 I discovered two things:

1. to end a form do end();?>
2. The password function has been deprecated. Do: input('password', array('label'=>'Password', 'type'=>'password'));?>
5. Nate :: on Dec 26, 2006 Mariano: No, it didn't take long, but I've been updating that app over time as 1.2 has changed. But most of the new structure invloves quite a bit less code, so I don't anticipate that the transition will take others long if they adopt the new conventions.

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( ).
6. Walker Hamilton :: on Dec 26, 2006 Nate, if the password function in FormHelper is used alone it does not have an option to output a label. If it's wrapped by $form->input, then it's fine.

$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.
7. Nate :: on Dec 29, 2006 Walker... Walker, slow down and think for a second: as I have said, the input( ) method is a *wrapper*, and as a wrapper, it generates some *extra* stuff.

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.
8. GreyCells :: on Jan 02, 2007 Hi Nate

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.
9. Claudio Poli :: on Jan 07, 2007 actually there are two main problems, when using the new validation rules error messages doesn't appear, but I suspect it's still a work in progress.
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 :)
10. Marcel :: on Jan 09, 2007 Hi Nate,

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)
11. Nate :: on Jan 14, 2007 Hi Marcel, I got your ticket on FormHelper::create(), and I'll look into it later today.

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.
12. Todd Pinel :: on Jan 19, 2007 Hi Nate, I was wondering if you could clear something up for me. I tried the the form helper in the 1.2 branch but I get in my weird results from form->create(); I get an action of /app/controllername/edit(or add)/id:2 when usually I would get /app/controllername/edit(or add)/2. The action is not grabbing the param of the URL on a submit. Am I doing something wrong or have something configured incorrectly?? Thanks in advance
13. Nate :: on Jan 21, 2007 Hi Todd, this is something I probably should have clarified. All the new 1.2 code uses the Router to generate URLs, and most URLs are created as arrays. This really deserves it's own post, but for now, just add this to your routes config:

Router::connect('/:controller/:action/:id', array(), array('id' => $ID));

The value of the ID will be available in the controller as $this->params['id'].
14. Marco :: on Jan 22, 2007 Hi Nate,

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
15. Nate :: on Jan 24, 2007 Hi 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".
16. Alexander Wegener :: on Feb 26, 2007 Hi Nate,

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
17. Rob :: on Mar 13, 2007 this is about the most helpful page on formhelpers ever.

add one

 
 
top