Forms

Nette Forms greatly facilitates creating and processing web forms. What it can really do?

  • validate sent data both client-side (JavaScript) and server-side
  • provide high level of security
  • multiple render modes
  • translations, i18n

Nette Framework puts a great effort to be safe and since forms are the most common user input, Nette forms are as good as impenetrable. All is maintained dynamically and transparently, nothing has to be set manually. Well-known vulnerabilities such as Cross Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) are filtered, as well as special control characters. All inputs are checked for UTF-8 validity. Every multiple-choice, select boxes and similar are checked for forged values upon validating. Sounds good? Let's try it out.

With Nette Forms, you can diminish routine tasks like double validation on both server and client side. You can also avoid common mistakes and security issues.

First Form

Let's create a simple registration form for our app. Forms are added into presenters using component factories:

use Nette\Application\UI;

class HomepagePresenter extends UI\Presenter
{
    // ...

    protected function createComponentRegistrationForm()
    {
        $form = new UI\Form;
        $form->addText('name', 'Name:');
        $form->addPassword('password', 'Password:');
        $form->addSubmit('login', 'Sign up');
        $form->onSuccess[] = [$this, 'registrationFormSucceeded'];
        return $form;
    }

    // called after form is successfully submitted
    public function registrationFormSucceeded(UI\Form $form, $values)
    {
        // ...
        $this->flashMessage('You have successfully signed up.');
        $this->redirect('Homepage:');
    }
}

render in template is done using control macro:

{control registrationForm}

and the result should look like this:

We have created a simple form with name and password form fields, which will call registrationFormSucceeded() after being submitted and successfully validated. The form itself is passed as the first parameter. The submitted values are passed as the second parameter contained in a ArrayHash object. If you prefer a simple array, you can typehint the second parameter array $values. You can also use $values = $form->getValues() to retrieve the submitted values.

In the context of presenter lifecycle the form is processed on the same level as the processing of signals (handle* methods), that is right after the action* method and before the render* method.

The rendered form follows basic web accessibility guidelines. All labels are generated as <label> elements and are associated with their inputs, clicking on the label moves cursor on the input.

Data stored in $values do not contain values of form buttons, so they're ready for more operations (such as inserting into database). It is noteworthy that whitespace at both left and right side of the input is removed. Just try it out: put your name into the form field, add few spaces and submit it: the name will be trimmed.

Although we mentioned validation, our form has none. Let's fix it. In order to require user's name, call setRequired() method on the form item. You can pass an error message as optional argument and it will be displayed if user does not fill his name in:

$form->addText('name', 'Name:')
    ->setRequired('Please fill your name.');

Try submitting a form without the name – the message is displayed unless you meet the validation rules. The form is validated on both the client and server side.

If you don't use nette/sandbox, you need to link netteForms.js in order to enable JavaScript validation. You can find the file in src/assets folder.

<script src="/path/to/netteForms.js"></script>

Nette Framework adds required class to all mandatory elements. Adding the following style will turn label of name input to red.

<style>
.required label { color: maroon }
</style>

Even though setting classes might be handy, we need solid data, don't we? We will add yet another validation rule, this time with addRule() method. First argument sets what should the value look like, second one is the error message again, shown if the validation is not passed. Preset validation rules will do this time, but you will learn how to create your own in no time.

Form will get another input age with condition, that it is optional, it has to be a number (Form::INTEGER) and in certain boundaries (Form::RANGE). This time we will utilize a third argument of addRule(), the range itself:

$form->addText('age', 'Age:')
    ->setRequired(false)
    ->addRule(Form::INTEGER, 'Your age must be an integer.')
    ->addRule(Form::RANGE, 'You must be older 18 years and be under 120.', [18, 120]);

Obviously, room for a small refactoring is available. In the error message and in the third parameter, the numbers are listed in duplicate, which is not ideal. If we were creating a multilingual forms and the message containing numbers would have to be translated into multiple languages, it would make it more difficult to change values. For this reason, substitute characters can be used in this format:

->addRule(Form::RANGE, 'You must be older %d years and be under %d.', [18, 120]);

Nette Framework supports HTML5, including new form elements. Thanks to that we can set the age input as numeric:

$form->addText('age', 'Age:')
    ->setHtmlType('number')
    ...

In the most advance browsers, namely Google Chrome, Safari and Opera, tiny arrows are rendered next to the input. Safari for iPhone shows an optimized keyboard with numbers.

Let's return to the password field, make it required, and verify the minimum password length (Form::MIN_LENGTH), again using the substitute characters in the message:

$form->addPassword('password', 'Password:')
    ->setRequired('Pick a password')
    ->addRule(Form::MIN_LENGTH, 'Your password has to be at least %d long', 3);

We will add one more input, passwordVerify, where user will be prompted to enter his password once more, to check for typo. Using validation rules, we will check if both fields contain the same value (Form::EQUAL). Notice the dynamic third argument, which is in fact the password control itself:

$form->addPassword('passwordVerify', 'Password again:')
    ->setRequired('Fill your password again to check for typo')
    ->addRule(Form::EQUAL, 'Password mismatch', $form['password']);

If the form would not be used for registration, but rather for editing records, it would be handy to set default values.

That's a complete, fully working registration form with both client-side and server-side validation. Automatically treats magic quotes, checks for invalid UTF-8 strings etc. Everything is ready and without a slightest effort on our side – Nette has taken care of it.

Examples are available to download. Try adding some more form fields. Many inspiring forms are also in /examples/Forms of the distribution package.

Form Fields

Besides wide range of built-in form fields you can add custom fields to the form as follows:

$form = new Form;
$form['date'] = new DateInput('Date:');

Use the extension method to add a new method to object form:

Nette\Forms\Container::extensionMethod('addZip', function ($form, $name, $label = null) {
    return $form->addText($name, $label)
        ->setRequired(false)
        ->addRule($form::PATTERN, 'At least 5 numbers', '[0-9]{5}');
});

$form = new Form;
$form->addZip('zip', 'ZIP code:');

Fields are removed using unset:

unset($form['zip');

Default Values

There are two ways to set default values. Method setDefaults() on a form or a container:

$form->addText('name', 'Name:');
$form->addInteger('age', 'Age:');

$form->setDefaults([
    'name' => 'John',
    'age' => '33'
]);

or method setDefaultValue() on a single input:

$form->addEmail('email', 'Email:')
    ->setDefaultValue('user@example.com');

Default values of select-boxes and radio lists are set with the key from passed array of values:

$form->addSelect('country', 'Country', [
    'cz' => 'Czech republic',
    'sk' => 'Slovakia',
]);
$form['country']->setDefaultValue('sk'); // country defaults to Slovakia

For CheckBox:

$form->addCheckbox('agree', 'Agree with conditions')
    ->setDefaultValue(true);

Another useful option is the “emptyValue”. If value of the form field after form submit is same as its “emptyValue” the field behaves as not filled.

$form->addText('phone', 'Phone:')
    ->setEmptyValue('+42');

Disabling Inputs

In order to disable an input, you can call $control->setDisabled(true).

$form->addEmail('email', 'E-mail:')->setDisabled(true);

The input cannot be written to and its value won't be returned by getValues().

If we need a read-only input (the input will be rendered as disabled but its value will be shown), we disable the input first and set the value afterwards. The reason is that setDisabled() method resets the value of the input.

$form->addText('readonly', 'Readonly:')->setDisabled()->setValue('readonly value');

If you only need to remove input's value but not disabling it, you can use setOmitted(true). This option is useful for omitting antispam inputs for example.

$form->addText('antispam', 'Antispam:')->setOmitted(true);

Low-level Forms

To add an item to the form, you don't have to call $form->addXyz(). Form items can be introduced exclusively in templates instead. This is useful if you, for example, need to generate dynamic items:

{foreach $items as $item}
    <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p>
{/foreach}

After submission, you can retrieve the values:

$values = $form->getHttpData($form::DATA_TEXT, 'sel[]');
$values = $form->getHttpData($form::DATA_TEXT | $form::DATA_KEYS, 'sel[]');

In the first parameter, you specify element type (DATA_FILE for type=file, DATA_LINE for one-line inputs like text, password or email and DATA_TEXT for the rest). The second parameter matches HTML attribute name. If you need to preserve keys, you can combine the first parameter with DATA_KEYS. This is useful for select, radioList or checkboxList.

getHttpData() returns sanitized input. In this case, it will always be array of valid UTF-8 strings, no matter what is sent by the form. It's an alternative to working with $_POST or $_GET directly if you want to receive safe data.

Cross-Site Request Forgery (CSRF) Protection

Nette Framework protects your applications against Cross-Site Request Forgery (CSRF) attacks. An attacker lures a victim on a webpage, which quietly performs a request to server the victim is logged into. The server would not recognize whether the user send the request willingly.

The protection is pretty simple:

$form->addProtection('Security token has expired, please submit the form again');

Generating and validating authentication token can prevent this attack. It has a limited expiration time: a session lifespan. Thanks to that, it does not prevent using the application in multiple windows (as long as it is the same session). The first argument is the error message shown, if the token has expired.

This protection should be added to all form which change sensitive data.

Forms in Presenters

When using forms in presenters, we use class Nette\Application\UI\Form which is an extension of Nette\Forms\Form.

Using One Form in Multiple Presenters

In a case when we need to use one form in multiple presenters, we have two options:

  1. either putting it into the presenters' hierarchy into a common ancestor and define a factory there
  2. or to define it in a separate factory class and create its instance in the presenters' factories.

Appropriate place for such class is e.g. app/forms/SignInFormFactory.php. Our factory class will look like this:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Name:');
        // ...
        $form->addSubmit('login', 'Log in');

        return $form;
    }
}

Now in each presenters' factory, which use our form, we create a form instance using our form factory class using create() method:

protected function createComponentSignInForm()
{
    $form = (new SignInFormFactory)->create();
    $form['login']->caption = 'Continue'; // we can also modify our form

    $form->onSuccess[] = [$this, 'signInFormSubmitted']; // and add event handlers

    return $form;
}

We could also process our submitted form on one place. It's as simple as moving our event handler to our factory class, renaming it from signInFormSubmitted to e.g. submitted. Alternatively, we can use an anonymous function as a handler:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Name:');
        ...
        $form->addSubmit('login', 'Log in');

        $form->onSuccess[] = function (Form $form, \stdClass $values) {
            // we process our submitted form here
        };

        return $form;
    }
}

Submitting a Form

If a form contains multiple submit buttons we need to distinguish, it is useful to bind the handlers to onClick event of the button, which is invoked immediately before onSuccess.

$form->addSubmit('login', 'Log in')
    ->onClick[] = [$this, 'signInFormSubmitted'];

If a form is submitted by pressing enter key, the first submit button is invoked.

Handlers of onSuccess and onClick events are invoked only if the submitted values pass validation. You don't need to validate the input inside the handler functions. The form also has onSubmit event, which is invoked irrespectively of the validation.

Sometimes can be useful to reset the form to its state before submitting. It can be done by invoking reset() (available since nette/forms version 2.4.6) method on the form:

$form->isSubmitted(); // true
$form->reset(); // the form is now in the state as it was never submitted
$form->isSubmitted(); // false

Standalone Forms

Nette Forms can be used without the whole framework as a standalone package. The form is created easily like this:

use Nette\Forms\Form;

$form = new Form;

$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');

echo $form; // renders the form

Such form is submitted through POST method to the same page. You can alter that easily:

$form = new Form;
$form->setAction('/submit.php');
$form->setMethod('GET');
...

We can find out whether the form was submitted and passed validation by calling $form->isSuccess(). If so, let's print out the data.

if ($form->isSuccess()) {
    echo 'Form was filled and submitted successfully';

    $values = $form->getValues();
    dump($values);
}

You can access form items like you access array. In our case, you can find the first text input on index $form['name'].

It's recommended to redirect to other page after you have processed the data. You can avoid duplicate form submission this way.

Rendering the Form

Each element has getLabel() and getControl() methods which return HTML code of label and the element itself. Nette provides getter and setter property access as if you are accessing the attribute itself.

<?php $form->render('begin') ?>
<?php $form->render('errors') ?>

<table>
<tr class="required">
    <th><?php echo $form['name']->label // Calls getLabel() ?></th>
    <td><?php echo $form['name']->control // Calls getControl()  ?></td>
</tr>

<tr class="required">
    <th><?php echo $form['age']->label ?></th>
    <td><?php echo $form['age']->control ?></td>
</tr>

...

</table>

<?php $form->render('end') ?>