Forms Used Standalone
Nette Forms dramatically simplify the creation and processing of web forms. You can use them in your applications completely standalone, without the rest of the framework, as demonstrated in this chapter.
However, if you use Nette Application and presenters, there is a dedicated guide for you: forms in presenters.
First Form
Let's try writing a simple registration form. Its code will be as follows (full code):
use Nette\Forms\Form;
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
And let's render it very easily:
$form->render();
The result in the browser should look like this:

The form is an object of the Nette\Forms\Form
class (the Nette\Application\UI\Form
class is used in
presenters). We added controls named ‘name’, ‘password’, and a submit button to it.
Now let's bring the form to life. By querying $form->isSuccess()
, we find out if the form was submitted and if
it was filled in validly. If so, we will output the data. After the form definition, add:
if ($form->isSuccess()) {
echo 'Form was filled and submitted successfully';
$data = $form->getValues();
// $data->name contains the name
// $data->password contains the password
var_dump($data);
}
The getValues()
method returns the submitted data as an ArrayHash object. We will show how to change this later. The $data
object contains the keys name
and
password
with the data entered by the user.
Usually, we send the data directly for further processing, such as inserting it into a database. However, an error might occur
during processing, for example, if the username is already taken. In this case, we pass the error back to the form using
addError()
and let it be rendered again, along with the error message.
$form->addError('Sorry, this username is already taken.');
After processing the form, we redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the refresh or back buttons, or by navigating through browser history.
By default, the form is sent using the POST method to the same page. Both can be changed:
$form->setAction('/submit.php');
$form->setMethod('GET');
And that's basically it :-) We have a functional and perfectly secured form.
Try adding other form controls as well.
Access to Controls
The form and its individual controls are called components. They form a component tree, where the form is the root. You can access individual form controls like this:
$input = $form->getComponent('name');
// alternative syntax: $input = $form['name'];
$button = $form->getComponent('send');
// alternative syntax: $button = $form['send'];
Controls are removed using unset
:
unset($form['name']);
Validation Rules
The word valid was mentioned, but the form doesn't have any validation rules yet. Let's fix that.
The name will be required, so we mark it with the setRequired()
method. Its argument is the text of the error
message displayed if the user doesn't fill in the name. If no argument is provided, the default error message is used.
$form->addText('name', 'Name:')
->setRequired('Please enter a name.');
Try submitting the form without filling in the name, and you'll see an error message appear. The browser or server will reject it until you fill in the field.
At the same time, you can't cheat the system by typing only spaces into the input. No way. Nette automatically trims leading and trailing whitespace. Try it. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to trick the form by sending a multi-line string as the name. Even here, Nette won't be fooled, and line breaks will be converted to spaces.)
The form is always validated on the server side, but JavaScript validation is also generated. This runs instantly, and the user
learns about errors immediately, without needing to submit the form to the server. This is handled by the
netteForms.js
script. Insert it into the page:
<script src="https://unpkg.com/nette-forms@3"></script>
If you look at the source code of the page with the form, you might notice that Nette inserts required controls into elements
with the CSS class required
. Try adding the following stylesheet to the template, and the “Name” label will turn
red. This elegantly highlights required controls for users:
<style>
.required label { color: maroon }
</style>
We add other validation rules using the addRule()
method. The first parameter is the rule, the second is again the
text of the error message, and an optional validation rule argument can follow. What does that mean?
Let's extend the form with a new optional field “age”, which must be an integer (addInteger()
) and within an
allowed range ($form::Range
). Here we will use the third parameter of the addRule()
method to pass the
required range to the validator as a pair [min, max]
:
$form->addInteger('age', 'Age:')
->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]);
If the user does not fill in the field, the validation rules will not be checked, as the control is optional.
This creates room for a small refactoring. The numbers are duplicated in the error message and the third parameter, which isn't
ideal. If we were creating multilingual forms and the
message containing numbers were translated into multiple languages, changing the values would become difficult. For this reason,
placeholders %d
can be used, and Nette will fill in the values:
->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]);
Let's return to the password
control, make it required as well, and also verify the minimum password length
($form::MinLength
), again using a placeholder in the message:
$form->addPassword('password', 'Password:')
->setRequired('Choose a password')
->addRule($form::MinLength, 'Password must be at least %d characters long', 8);
Let's add another field passwordVerify
to the form, where the user enters the password again for verification.
Using validation rules, we check if both passwords are the same ($form::Equal
). As the parameter, we provide a
reference to the first password using square brackets:
$form->addPassword('passwordVerify', 'Password again:')
->setRequired('Please enter the password again for verification')
->addRule($form::Equal, 'Passwords do not match', $form['password'])
->setOmitted();
Using setOmitted()
, we marked a control whose value we don't really care about and which exists only for
validation purposes. Its value is not passed to $data
.
With this, we have a fully functional form with validation in both PHP and JavaScript. Nette's validation capabilities are much broader; you can create conditions, show and hide parts of the page based on them, etc. You will learn everything in the chapter on form validation.
Default Values
We often set default values for form controls:
$form->addEmail('email', 'Email')
->setDefaultValue($lastUsedEmail);
It's often useful to set default values for all controls simultaneously, for example, when the form is used for editing records. We read the record from the database and set its values as defaults:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Call setDefaults()
after defining the controls.
Rendering the Form
By default, the form is rendered as a table. Individual controls adhere to basic accessibility guidelines – all labels are
generated as <label>
elements and associated with their respective form controls. Clicking on a label
automatically places the cursor in the form field.
We can set arbitrary HTML attributes for each control. For example, add a placeholder:
$form->addInteger('age', 'Age:')
->setHtmlAttribute('placeholder', 'Please fill in the age');
There are many ways to render a form, so a separate chapter is dedicated to rendering.
Mapping to Classes
Let's return to processing form data. The getValues()
method returned the submitted data as an
ArrayHash
object. Since this is a generic class, like stdClass
, we lack certain conveniences when
working with it, such as property autocompletion in editors or static code analysis. This could be solved by having a specific
class for each form, whose properties represent the individual controls. E.g.:
class RegistrationFormData
{
public string $name;
public ?int $age;
public string $password;
}
Alternatively, you can use the constructor:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Properties of the data class can also be enums, and they will be automatically mapped.
How do we tell Nette to return data as objects of this class? Easier than you might think. All you need to do is specify the class name or the object to hydrate as a parameter:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
You can also specify 'array'
as the parameter, and the data will be returned as an array.
If the forms consist of a multi-level structure composed of containers, create a separate class for each one:
$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */
class PersonFormData
{
public string $firstName;
public string $lastName;
}
class RegistrationFormData
{
public PersonFormData $person;
public ?int $age;
public string $password;
}
The mapping then knows from the $person
property type that it should map the container to the
PersonFormData
class. If the property were to contain an array of containers, specify the array
type and
pass the class to be mapped directly to the container:
$person->setMappedType(PersonFormData::class);
You can have a proposal for the form's data class generated using the
Nette\Forms\Blueprint::dataClass($form)
method, which will print it to the browser page. You can then simply click to
select and copy the code into your project.
Multiple Submit Buttons
If the form has more than one button, we usually need to distinguish which one was pressed. The button's
isSubmittedBy()
method returns this information:
$form->addSubmit('save', 'Save');
$form->addSubmit('delete', 'Delete');
if ($form->isSuccess()) {
if ($form['save']->isSubmittedBy()) {
// ...
}
if ($form['delete']->isSubmittedBy()) {
// ...
}
}
Do not omit the $form->isSuccess()
check; it verifies the validity of the data.
When a form is submitted by pressing the Enter key, it is treated as if it were submitted by the first button.
Vulnerability Protection
Nette Framework places a strong emphasis on security and therefore meticulously ensures the proper security of forms.
In addition to protecting forms against well-known vulnerabilities like Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF), it performs many small security measures that you no longer need to think about.
For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered options and that no forgery occurred. We've already mentioned that for single-line text inputs, it removes end-of-line characters that an attacker might send. For multi-line inputs, it normalizes end-of-line characters. And so on.
Nette handles security risks for you that many programmers are unaware even exist.
The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is currently logged in. The server believes that the request was made by the victim voluntarily. Therefore, Nette prevents the form from being submitted via POST from a different domain. If, for some reason, you want to disable protection and allow the form to be submitted from another domain, use:
$form->allowCrossOrigin(); // WARNING! Disables protection!
This protection uses a SameSite cookie named _nss
. Therefore, create the form object before sending the first
output so that the cookie can be sent.
SameSite cookie protection may not be 100% reliable, so it's advisable to also enable token protection:
$form->addProtection();
We strongly recommend applying this protection to forms in the administrative part of your application that modify sensitive
data. The framework defends against CSRF attacks by generating and validating an authorization token stored in the session.
Therefore, it is necessary to have a session started before displaying the form. In the administrative part of the website, the
session is usually already started due to user login. Otherwise, start the session using the
Nette\Http\Session::start()
method.
So, we've had a quick introduction to forms in Nette. Try looking in the examples directory in the distribution for more inspiration.