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(): UI\Form
{
$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, \stdClass $values): void
{
// ...
$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 $form, string $name, string $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:
- either putting it into the presenters' hierarchy into a common ancestor and define a factory there
- 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
{
public function create(): Form
{
$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
{
$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
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
...
$form->addSubmit('login', 'Log in');
$form->onSuccess[] = function (Form $form, \stdClass $values): void {
// 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') ?>