Using Form Builder

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.

1. Basic Concepts

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.

2. Creating a Simple Form

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">
<?php echo $form; ?>
</div>

Tip: The above code echo $form; is equivalent to echo $form->render();. This is because CForm implements __toString magic method which calls render() and returns its result as the string representation of the form object.

3. Specifying Form Elements

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.

Specifying Input Element

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:

  • text
  • hidden
  • password
  • textarea
  • file
  • radio
  • checkbox
  • listbox
  • dropdownlist
  • checkboxlist
  • radiolist

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.

Specifying Static Text

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.

Specifying Sub-form

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.

4. Accessing Form Elements

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'];

5. Creating a Nested Form

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">
<?php echo $form; ?>
</div>

6. Customizing Form Display

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:

<?php
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">
<?php $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
 
<?php echo $form['username']; ?>
 
some complex UI elements here
 
<?php 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.

$Id$

Total 7 comments

#6092 report it
Farzan at 2011/12/12 12:30am
Stateful form

If you want to create a stateful form, change the config file as below:

return array(
    'activeForm' => array(
        'stateful' => true,
    ),
    ...
);
#5080 report it
Say_Ten at 2011/09/13 10:39am
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(),
        ),
    ),
);
#5004 report it
pligor at 2011/09/05 10:29am
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 ;)

#2752 report it
rAWTAZ at 2011/02/05 04:16pm
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.

#472 report it
KJedi at 2010/05/21 06:54am
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
),
...
)
#721 report it
ptoly at 2010/03/11 02:32pm
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
);
#741 report it
ciss at 2010/03/08 07:53am
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.

Leave a comment

Please to leave your comment.