When creating HTML forms, we often find that we are writing a lot of repetitive view code which is difficult to be reused in a different project. For example, for every input field, we need to associate it with a text label and display possible validation errors. To improve the reusability of these code, we can use the form builder feature.
The Yii form builder uses a CForm object to represent the specifications needed to describe an HTML form, including which data models are associated with the form, what kind of input fields there are in the form, and how to render the whole form. Developers mainly need to create and configure this CForm object, and then call its rendering method to display the form.
Form input specifications are organized in terms of a form element hierarchy. At the root of the hierarchy, it is the CForm object. The root form object maintains its children in two collections: CForm::buttons and CForm::elements. The former contains the button elements (such as submit buttons, reset buttons), while the latter contains the input elements, static text and sub-forms. A sub-form is a CForm object contained in the CForm::elements collection of another form. It can have its own data model, CForm::buttons and CForm::elements collections.
When users submit a form, the data entered into the input fields of the whole form hierarchy are submitted, including those input fields that belong to the sub-forms. CForm provides convenient methods that can automatically assign the input data to the corresponding model attributes and perform data validation.
In the following, we show how to use the form builder to create a login form.
First, we write the login action code:
public function actionLogin()
{
$model = new LoginForm;
$form = new CForm('application.views.site.loginForm', $model);
if($form->submitted('login') && $form->validate())
$this->redirect(array('site/index'));
else
$this->render('login', array('form'=>$form));
}
In the above code, we create a CForm object using the specifications pointed to
by the path alias application.views.site.loginForm
(to be explained shortly).
The CForm object is associated with the LoginForm
model as described in
Creating Model.
As the code reads, if the form is submitted and all inputs are validated without
any error, we would redirect the user browser to the site/index
page. Otherwise,
we render the login
view with the form.
The path alias application.views.site.loginForm
actually refers to the PHP file
protected/views/site/loginForm.php
. The file should return a PHP array representing
the configuration needed by CForm, as shown in the following:
return array(
'title'=>'Please provide your login credential',
'elements'=>array(
'username'=>array(
'type'=>'text',
'maxlength'=>32,
),
'password'=>array(
'type'=>'password',
'maxlength'=>32,
),
'rememberMe'=>array(
'type'=>'checkbox',
)
),
'buttons'=>array(
'login'=>array(
'type'=>'submit',
'label'=>'Login',
),
),
);
The configuration is an associative array consisting of name-value pairs that are used to initialize the corresponding properties of CForm. The most important properties to configure, as we aformentioned, are CForm::elements and CForm::buttons. Each of them takes an array specifying a list of form elements. We will give more details on how to configure form elements in the next sub-section.
Finally, we write the login
view script, which can be as simple as follows,
<h1>Login</h1> <div class="form"> echo $form; </div>
Tip: The above code
echo $form;
is equivalent toecho $form->render();
. This is because CForm implements__toString
magic method which callsrender()
and returns its result as the string representation of the form object.
Using the form builder, the majority of our effort is shifted from writing view script code to specifying the form elements. In this sub-section, we describe how to specify the CForm::elements property. We are not going to describe CForm::buttons because its configuration is nearly the same as CForm::elements.
The CForm::elements property accepts an array as its value. Each array element specifies a single form element which can be an input element, a static text string or a sub-form.
An input element mainly consists of a label, an input field, a hint text and an error display. It must be associated with a model attribute. The specification for an input element is represented as a CFormInputElement instance. The following code in the CForm::elements array specifies a single input element:
'username'=>array(
'type'=>'text',
'maxlength'=>32,
),
It states that the model attribute is named as username
, and the input field type is text
whose
maxlength
attribute is 32.
Any writable property of CFormInputElement can be configured like above. For example, we may specify
the hint option in order to display a hint text, or we may specify the
items option if the input field is a list box, a drop-down list, a check-box list
or a radio-button list. If an option name is not a property of CFormInputElement, it will be treated
the attribute of the corresponding HTML input element. For example, because maxlength
in the above is not
a property of CFormInputElement, it will be rendered as the maxlength
attribute of the HTML text input field.
The type option deserves additional attention. It specifies the type of the input
field to be rendered. For example, the text
type means a normal text input field should be rendered;
the password
type means a password input field should be rendered. CFormInputElement recognizes the following
built-in types:
Among the above built-in types, we would like to describe a bit more about the usage of those "list" types,
which include dropdownlist
, checkboxlist
and radiolist
. These types require setting the items
property of the corresponding input element. One can do so like the following:
'gender'=>array(
'type'=>'dropdownlist',
'items'=>User::model()->getGenderOptions(),
'prompt'=>'Please select:',
),
...
class User extends CActiveRecord
{
public function getGenderOptions()
{
return array(
0 => 'Male',
1 => 'Female',
);
}
}
The above code will generate a drop-down list selector with prompt text "please select:". The selector options
include "Male" and "Female", which are returned by the getGenderOptions
method in the User
model class.
Besides these built-in types, the type option can also take a widget class name or the path alias to it. The widget class must extend from CInputWidget or CJuiInputWidget. When rendering the input element, an instance of the specified widget class will be created and rendered. The widget will be configured using the specification as given for the input element.
In many cases, a form may contain some decorational HTML code besides the input fields. For example, a horizontal line may be needed to separate different portions of the form; an image may be needed at certain places to enhance the visual appearance of the form. We may specify these HTML code as static text in the CForm::elements collection. To do so, we simply specify a static text string as an array element in the appropriate position in CForm::elements. For example,
return array(
'elements'=>array(
......
'password'=>array(
'type'=>'password',
'maxlength'=>32,
),
'<hr />',
'rememberMe'=>array(
'type'=>'checkbox',
)
),
......
);
In the above, we insert a horizontal line between the password
input and the rememberMe
input.
Static text is best used when the text content and their position are irregular. If each input element in a form needs to be decorated similarly, we should customize the form rendering approach, as to be explained shortly in this section.
Sub-forms are used to divide a lengthy form into several logically connected portions. For example, we may divide user registration form into two sub-forms: login information and profile information. Each sub-form may or may not be associated with a data model. In the user registration form example, if we store user login information and profile information in two separate database tables (and thus two data models), then each sub-form would be associated with a corresponding data model. If we store everything in a single database table, then neither sub-form has a data model because they share the same model with the root form.
A sub-form is also represented as a CForm object. In order to specify a sub-form, we should configure
the CForm::elements property with an element whose type is form
:
return array(
'elements'=>array(
......
'user'=>array(
'type'=>'form',
'title'=>'Login Credential',
'elements'=>array(
'username'=>array(
'type'=>'text',
),
'password'=>array(
'type'=>'password',
),
'email'=>array(
'type'=>'text',
),
),
),
'profile'=>array(
'type'=>'form',
......
),
......
),
......
);
Like configuring a root form, we mainly need to specify the CForm::elements property for a sub-form. If a sub-form needs to be associated with a data model, we can configure its CForm::model property as well.
Sometimes, we may want to represent a form using a class other than the default CForm. For example,
as will show shortly in this section, we may extend CForm to customize the form rendering logic.
By specifying the input element type to be form
, a sub-form will automatically be represented as an object
whose class is the same as its parent form. If we specify the input element type to be something like
XyzForm
(a string terminated with Form
), then the sub-form will be represented as a XyzForm
object.
Accessing form elements is as simple as accessing array elements. The CForm::elements property returns
a CFormElementCollection object, which extends from CMap and allows accessing its elements like a normal
array. For example, in order to access the username
element in the login form example, we can use the following
code:
$username = $form->elements['username'];
And to access the email
element in the user registration form example, we can use
$email = $form->elements['user']->elements['email'];
Because CForm implements array access for its CForm::elements property, the above code can be further simplified as:
$username = $form['username'];
$email = $form['user']['email'];
We already described sub-forms. We call a form with sub-forms a nested form. In this section,
we use the user registration form as an example to show how to create a nested form associated
with multiple data models. We assume the user credential information is stored as a User
model,
while the user profile information is stored as a Profile
model.
We first create the register
action as follows:
public function actionRegister()
{
$form = new CForm('application.views.user.registerForm');
$form['user']->model = new User;
$form['profile']->model = new Profile;
if($form->submitted('register') && $form->validate())
{
$user = $form['user']->model;
$profile = $form['profile']->model;
if($user->save(false))
{
$profile->userID = $user->id;
$profile->save(false);
$this->redirect(array('site/index'));
}
}
$this->render('register', array('form'=>$form));
}
In the above, we create the form using the configuration specified by application.views.user.registerForm
.
After the form is submitted and validated successfully, we attempt to save the user and profile models.
We retrieve the user and profile models by accessing the model
property of the corresponding sub-form objects.
Because the input validation is already done, we call $user->save(false)
to skip the validation. We do
this similarly for the profile model.
Next, we write the form configuration file protected/views/user/registerForm.php
:
return array(
'elements'=>array(
'user'=>array(
'type'=>'form',
'title'=>'Login information',
'elements'=>array(
'username'=>array(
'type'=>'text',
),
'password'=>array(
'type'=>'password',
),
'email'=>array(
'type'=>'text',
)
),
),
'profile'=>array(
'type'=>'form',
'title'=>'Profile information',
'elements'=>array(
'firstName'=>array(
'type'=>'text',
),
'lastName'=>array(
'type'=>'text',
),
),
),
),
'buttons'=>array(
'register'=>array(
'type'=>'submit',
'label'=>'Register',
),
),
);
In the above, when specifying each sub-form, we also specify its CForm::title property. The default form rendering logic will enclose each sub-form in a field-set which uses this property as its title.
Finally, we write the simple register
view script:
<h1>Register</h1> <div class="form"> echo $form; </div>
The main benefit of using form builder is the separation of logic (form configuration stored in a separate file) and presentation (CForm::render method). As a result, we can customize the form display by either overriding CForm::render or providing a partial view to render the form. Both approaches can keep the form configuration intact and can be reused easily.
When overriding CForm::render, one mainly needs to traverse through the CForm::elements and CForm::buttons collections and call the CFormElement::render method of each form element. For example,
class MyForm extends CForm
{
public function render()
{
$output = $this->renderBegin();
foreach($this->getElements() as $element)
$output .= $element->render();
$output .= $this->renderEnd();
return $output;
}
}
We may also write a view script _form
to render a form:
echo $form->renderBegin(); foreach($form->getElements() as $element) echo $element->render(); echo $form->renderEnd();
To use this view script, we can simply call:
<div class="form"> $this->renderPartial('_form', array('form'=>$form)); </div>
If a generic form rendering does not work for a particular form (for example, the form needs some irregular decorations for certain elements), we can do like the following in a view script:
some complex UI elements here echo $form['username']; some complex UI elements here echo $form['password']; some complex UI elements here
In the last approach, the form builder seems not to bring us much benefit, as we still need to write similar amount of form code. It is still beneficial, however, that the form is specified using a separate configuration file as it helps developers to better focus on the logic.
Found a typo or you think this page needs improvement?
Edit it on github !
data from the model in the dropdown
It becomes tricky when you want to use data from the model in dropdown when using CForm. Sure, you can create config array in the Controller, but the cleaner way is to do it in view as shown this article. I used the following method for this:
--registerForm.php--
<?php $data = CHtml::listData(User::model()->findAll(), 'userID', 'username')); return array( .... 'users' => array( 'type'=>'dropdownlist', 'items'=>$data//here it goes ), ... )
How to generate multi select elements (dropdownlists, etc)
It took me a while to figure this out and some educated guesses as there seems to be no documentation on it. The key is the items array gets turned into a options/values. I hope this helps someone!
$form = new CForm( array( 'title' =>'form title', 'showErrorSummary' => true, 'elements' => array( 'numCols' => array( 'type' => 'dropdownlist', 'items' => array(1=>1,2=>2,3=>3,4=>4), ), 'numRows' => array( 'type' => 'dropdownlist', 'items' => array(2=>2,3=>3,4=>4,5=>5), ), 'defaultStatus' => array( 'type' => 'text', 'maxlength' => 24, ), ), 'buttons'=>array( 'submit'=>array( 'type'=>'submit', 'label'=>'Save', ), ), ), $model );
Avoid "echo $form;"
Using "echo $form" will give you a hard time tracing errors in your form configuration, since an exception thrown in CForm::__toString() doesn't get caught this way.
Use $form->render() instead to have the benefit of a complete stack trace.
Attributes must be safe
In Yii 1.1.6, and probably earlier versions as well, the attribute name that the element key defines must be a "safe" attribute in the model. If it is not considered safe, it will not be rendered. At least when you use a widget like this in the 'elements' array:
'attributeName'=>array(
'type'=>'MyWidget',
),
When implementing a custom widget that didn't map to a particular attribute (i.e. I had no use of specifying an attribute name, but had to since a key is required), I noticed that unless the virtual attribute name I used didn't have a rule that made it "safe" in the model, the widget wouldnt be rendered at all.
Actually it seems that there being a rule for the attribute is the only requirement for its element to be rendered; I didn't have to define the attribute even virtually.
See Securing Attribute Assignments in the Yii Guide for more information on "safe" attributes.
how to render these complex UI elements
If you want to do a custom rendering of an element then you could take each of its properties and render it.
I mean use the
$form['attribute']->label
for label along with CHtml static methods and so on.
The best approach would be to create a widget which has the CInputWidget as its parent class.
Now if you are too lazy to implement a new widget you could try this:
Use the "layout" attribute
By default:
layout = '{label} {input} {hint} {error}'
So you can create,for example, a custom input, get its returned html code
and redefine the layout as such:
$input = some_render_input_method_or_function(); layout = '{label} '. $input .' {hint} {error}';
That's it, now you have the Ugly, The Beautiful and the Lazy to choose on how to render ;)
Re: #472
I just discovered, and it makes sense, but in the form config file you're in the context of the CForm object. This enables you to use $this in the file, such as:
return array( 'elements' => array( 'attribute' => array( 'type' => 'dropdownlist', 'items' => $this->model->getAttributeListData(), ), ), );
Stateful form
If you want to create a stateful form, change the config file as below:
return array( 'activeForm' => array( 'stateful' => true, ), ... );
Signup or Login in order to comment.