Forms Validation

Required Controls

Controls are marked as required using the setRequired() method. Its argument is the text of the error message that will be displayed if the user does not fill in the control. If no argument is provided, the default error message is used.

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

Rules

We add validation rules to controls using the addRule() method. The first parameter is the rule, the second is the error message, and the third is the validation rule argument.

$form->addPassword('password', 'Password:')
	->addRule($form::MinLength, 'Password must be at least %d characters long', 8);

Validation rules are checked only if the user has filled in the control.

Nette comes with several predefined rules whose names are constants of the Nette\Forms\Form class. We can apply these rules to all controls:

constant description argument type
Required required control, alias for setRequired()
Filled required control, alias for setRequired()
Blank control must not be filled
Equal value must be equal to the parameter mixed
NotEqual value must not be equal to the parameter mixed
IsIn value must be one of the items in the array array
IsNotIn value must not be any of the items in the array array
Valid is the control filled correctly? (for Conditions)

Text inputs

For controls addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat(), some of the following rules can also be applied:

MinLength minimum text length int
MaxLength maximum text length int
Length length in range or exact length pair [int, int] or int
Email valid email address
URL absolute URL
Pattern matches regular expression string
PatternInsensitive like Pattern, but case-insensitive string
Integer integer value
Numeric alias for Integer
Float number
Min minimum value of a numeric control int|float
Max maximum value of a numeric control int|float
Range value in range pair [int|float, int|float]

The validation rules Integer, Numeric, and Float automatically convert the value to an integer or float, respectively. Furthermore, the URL rule also accepts an address without a scheme (e.g., nette.org) and completes the scheme (https://nette.org). The expression in Pattern and PatternInsensitive must be valid for the entire value, i.e., as if it were wrapped in ^ and $ characters.

Number of Items

For controls addMultiUpload(), addCheckboxList(), addMultiSelect(), you can also use the following rules to limit the number of selected items or uploaded files:

MinLength minimum count int
MaxLength maximum count int
Length count in range or exact count pair [int, int] or int

File Upload

For controls addUpload(), addMultiUpload(), the following rules can also be used:

MaxFileSize maximum file size in bytes int
MimeType MIME type, wildcards allowed ('video/*') string|string[]
Image JPEG, PNG, GIF, WebP, AVIF image
Pattern file name matches regular expression string
PatternInsensitive like Pattern, but case-insensitive string

MimeType and Image require the PHP extension fileinfo. Whether a file or image is of the required type is detected based on its signature, and the integrity of the entire file is not checked. You can determine if an image is corrupted, for example, by trying to load it.

Error Messages

All predefined rules except Pattern and PatternInsensitive have a default error message, so they can be omitted. However, by providing and formulating all custom messages tailored to your needs, you will make the form more user-friendly.

You can change the default messages in the configuration, by modifying the texts in the Nette\Forms\Validator::$messages array, or by using a translator.

The following placeholder strings can be used in the text of error messages:

%d replaced sequentially by rule arguments
%n$d replaced by the n-th rule argument
%label replaced by the control label (without the colon)
%name replaced by the control name (e.g., name)
%value replaced by the value entered by the user
$form->addText('name', 'Name:')
	->setRequired('Please fill in %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'at least %d and at most %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'at most %2$d and at least %1$d', [5, 10]);

Conditions

In addition to rules, conditions can also be added. They are written similarly to rules, but instead of addRule(), we use the addCondition() method, and naturally, we don't provide an error message (the condition only asks):

$form->addPassword('password', 'Password:')
	// if the password length is not greater than 8
	->addCondition($form::MaxLength, 8)
		// then it must contain a digit
		->addRule($form::Pattern, 'Must contain a digit', '.*[0-9].*');

The condition can be linked to a control other than the current one using addConditionOn(). The first parameter is a reference to the control. In this example, the email will be required only if the checkbox is checked (i.e., its value is true):

$form->addCheckbox('newsletters', 'Send me newsletters');

$form->addEmail('email', 'Email:')
	// if the checkbox is checked
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// then require the email
		->setRequired('Enter your email address');

Conditions can be formed into complex structures using elseCondition() and endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // if the first condition is met
		->addConditionOn(/* ... */) // and the second condition on another control is also met
			->addRule(/* ... */) // require this rule
		->elseCondition() // if the second condition is not met
			->addRule(/* ... */) // require these rules
			->addRule(/* ... */)
		->endCondition() // we return to the first condition
		->addRule(/* ... */);

In Nette, it is very easy to react to the fulfillment or non-fulfillment of a condition on the JavaScript side using the toggle() method, see Dynamic JavaScript.

Reference to Another Control

You can also pass another form control as an argument to a rule or condition. The rule will then use the value entered later by the user in the browser. This can be used, for example, to dynamically validate that the password control contains the same string as the password_confirm control:

$form->addPassword('password', 'Password');
$form->addPassword('password_confirm', 'Confirm Password')
    ->addRule($form::Equal, 'The passwords do not match', $form['password']);

Custom Rules and Conditions

Sometimes we encounter situations where the built-in validation rules in Nette are insufficient, and we need to validate user data in our own way. In Nette, this is very simple!

You can pass any callback as the first parameter to the addRule() or addCondition() methods. The callback accepts the control itself as the first parameter and returns a boolean value indicating whether the validation was successful. When adding a rule using addRule(), additional arguments can be provided, which are then passed as the second parameter.

A custom set of validators can thus be created as a class with static methods:

class MyValidators
{
	// tests whether the value is divisible by the argument
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// other validators
	}
}

Usage is then very straightforward:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'The value must be a multiple of %d',
		8,
	);

Custom validation rules can also be added to JavaScript. The condition is that the rule must be a static method. Its name for the JavaScript validator is formed by concatenating the class name without backslashes \, an underscore _, and the method name. For example, App\MyValidators::validateDivisibility is written as AppMyValidators_validateDivisibility and added to the Nette.validators object:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Event onValidate

After the form is submitted, validation is performed, checking the individual rules added using addRule(), and subsequently, the onValidate event is triggered. Its handler can be used for additional validation, typically verifying the correct combination of values in multiple form controls.

If an error is detected, it is passed to the form using the addError() method. This can be called either on a specific control or directly on the form.

protected function createComponentSignInForm(): Form
{
	$form = new Form;
	// ...
	$form->onValidate[] = [$this, 'validateSignInForm'];
	return $form;
}

public function validateSignInForm(Form $form, \stdClass $data): void
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('This combination is not possible.');
	}
}

Processing Errors

In many cases, we only discover an error when processing a valid form, for example, when writing a new entry to the database and encountering a duplicate key. In such a case, we again pass the error back to the form using the addError() method. This can be called either on a specific control or directly on the form:

try {
	$data = $form->getValues();
	$this->user->login($data->username, $data->password);
	$this->redirect('Home:');

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) {
		$form->addError('Invalid password.');
	}
}

If possible, we recommend adding the error directly to the form control, as it will be displayed next to it when using the default renderer.

$form['date']->addError('Sorry, this date is already taken.');

You can call addError() repeatedly to pass multiple error messages to a form or control. You can retrieve them using getErrors().

Note that $form->getErrors() returns a summary of all error messages, including those passed directly to individual controls, not just directly to the form. Error messages passed only to the form can be retrieved via $form->getOwnErrors().

Modifying Input Values

Using the addFilter() method, we can modify the value entered by the user. In this example, we will tolerate and remove spaces in the postal code:

$form->addText('zip', 'Postal Code:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // remove spaces from the postal code
	})
	->addRule($form::Pattern, 'Postal code is not five digits', '\d{5}');

The filter is integrated among validation rules and conditions, and thus the order of methods matters, i.e., the filter and rule are called in the same order as the addFilter() and addRule() methods are listed.

JavaScript Validation

The language for formulating conditions and rules is very powerful. All constructs work both on the server side and on the client side in JavaScript. They are transferred in HTML attributes data-nette-rules as JSON. The validation itself is handled by a script that intercepts the form's submit event, iterates through the individual controls, and performs the corresponding validation.

This script is netteForms.js, and it is available from several possible sources:

You can embed the script directly into the HTML page from a CDN:

<script src="https://unpkg.com/nette-forms@3"></script>

Or copy it locally to the public folder of your project (e.g., from vendor/nette/forms/src/assets/netteForms.min.js):

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

Or install it via npm:

npm install nette-forms

And then load and run it:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternatively, you can load it directly from the vendor folder:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

Dynamic JavaScript

Want to display the address fields only if the user chooses to have the goods sent by post? No problem. The key is the pair of methods addCondition() & toggle():

$form->addCheckbox('send_it')
	->addCondition($form::Equal, true)
		->toggle('#address-container');

This code states that when the condition is met (i.e., when the checkbox is checked), the HTML element #address-container will be visible, and vice versa. So, we place the form controls with the recipient's address in a container with this ID, and they will hide or show when the checkbox is clicked. This is handled by the netteForms.js script.

Any selector can be passed as an argument to the toggle() method. For historical reasons, an alphanumeric string without other special characters is treated as an element ID, just as if it were preceded by the # character. The second optional parameter allows reversing the behavior; for instance, if we used toggle('#address-container', false), the element would be displayed only if the checkbox was not checked.

The default JavaScript implementation changes the hidden property of the elements. However, we can easily change the behavior, for example, by adding an animation. Just override the Nette.toggle method in JavaScript with a custom solution:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// hide or show 'el' according to the value of 'visible'
	});
};

Disabling Validation

Sometimes it might be useful to disable validation. If pressing a submit button should not perform validation (suitable for Cancel or Preview buttons), we disable it using the $submit->setValidationScope([]) method. If it should perform only partial validation, we can specify which fields or form containers should be validated.

$form->addText('name')
	->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')
	->setRequired('age');
$details->addInteger('age2')
	->setRequired('age2');

$form->addSubmit('send1'); // Validates the whole form
$form->addSubmit('send2')
	->setValidationScope([]); // Validates nothing
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // Validates only the 'name' control
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Validates only the 'age' control
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Validates the 'details' container

setValidationScope does not affect the Event onValidate on the form, which will always be called. The onValidate event on a container will only be triggered if that container is marked for partial validation.

version: 4.0 3.x 2.x