Forms Validation

Required Controls

Controls are marked as required with the method setRequired(), whose argument is the text of the error message that will be displayed if the user does not fill it. If no argument is given, the default error message is used.

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

Rules

We add validation rules to controls with 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::MIN_LENGTH, 'Password must be at least %d characters', 8);

Nette comes with a number of built-in rules whose names are constants of the Nette\Forms\Form class:

We can use the following rules for all controls:

constant description arguments
REQUIRED alias of setRequired()
FILLED alias of setRequired()
BLANK must not be filled
EQUAL value is equal to parameter mixed
NOT_EQUAL value is not be equal to parameter mixed
IS_IN value is equal to some element in the array array
IS_NOT_IN value does not equal any element in the array array
VALID input passes validation (for conditions)

For controls addText(), addPassword(), addTextArea(), addEmail(), addInteger() the following rules can also be used:

MIN_LENGTH minimal string length int
MAX_LENGTH maximal string length int
LENGTH length in range or exact length pair [int, int] or int
EMAIL valid email address
URL valid URL
PATTERN matches regular pattern string
PATTERN_ICASE like PATTERN, but case-insensitive string
INTEGER integer
NUMERIC alias of INTEGER
FLOAT integer or floating point number
MIN minimum of the integer value int|float
MAX maximum of the integer value int|float
RANGE value in the range pair [int|float, int|float]

The INTEGER, NUMERIC a FLOAT rules automatically convert the value to integer (or float respectively). Furthermore, the URL rule also accepts an address without a schema (eg nette.org) and completes the schema (https://nette.org). The expressions in PATTERN and PATTERN_ICASE must be valid for the whole value, ie as if it were wrapped in the characters ^ and $.

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

MAX_FILE_SIZE maximal file size int
MIME_TYPE MIME type, accepts wildcards ('video/*') string|string[]
IMAGE uploaded file is JPEG, PNG, GIF, WebP
PATTERN file name matches regular expression string
PATTERN_ICASE like PATTERN, but case-insensitive string

The MIME_TYPE and IMAGE require PHP extension fileinfo. Whether a file or image is of the required type is detected by its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to load it.

For controls addMultiUpload(), addCheckboxList(), addMultiSelect() the following rules can also be used to limit the number of selected items, respectively uploaded files:

MIN_LENGTH minimal count int
MAX_LENGTH maximal count int
LENGTH count in range or exact count pair [int, int] or int

Error Messages

All predefined rules except PATTERN and PATTERN_ICASE have a default error message, so they it be omitted. However, by passing and formulating all customized messages, you will make the form more user-friendly.

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

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

%d gradually replaces the rules after the arguments
%n$d replaces with the nth rule argument
%label replaces with field label (without colon)
%name replaces with field name (eg name)
%value replaces with 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 no more than %d', [5, 10]);

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

Conditions

Besides validation rules, conditions can be set. They are set much like rules, yet we use addRule() instead of addCondition() and of course, we leave it without an error message (the condition just asks):

$form->addPassword('password', 'Password:')
	// if password is not longer than 8 characters ...
	->addCondition($form::MAX_LENGTH, 8)
		// ... then it must contain a number
		->addRule($form::PATTERN, 'Must contain number', '.*[0-9].*');

Condition can be linked to a different element than the current one using addConditionOn(). The first parameter is a reference to the field. In the following case, the email will only be required if the checkbox is checked (ie. its value is true):

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

$form->addEmail('email', 'Email:')
	// if checkbox is checked ...
	->addConditionOn($form['newsletters'], $form::EQUAL, true)
		// ... require email
		->setRequired('Fill your email address');

Conditions can be grouped into complex structures with elseCondition() and endCondition() methods.

$form->addText(/* ... */)
	->addCondition(/* ... */) // if the first condition is met
		->addConditionOn(/* ... */) // and the second condition on another element too
			->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 not of a condition on the JavaScript side using the toggle() method, see Dynamic JavaScript.

References Between Controls

The rule or condition argument can be a reference to another element. For example, you can dynamically validate that the text has as many characters as the value of the length field is:

$form->addInteger('length');
$form->addText('text')
	->addRule($form::LENGTH, null, $form['length']);

Custom Rules and Conditions

Sometimes we get into a situation where the built-in validation rules in Nette are not enough and we need to validate the data from the user in our own way. In Nette this is very easy!

You can pass any callback as the first parameter to the addRule() or addCondition() methods. The callback accepts the element 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 passed, and these are then passed as the second parameter.

The 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)
	{
		// additional validators
	}
}

The usage is then very simple:

$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 only requirement is that the rule must be a static method. Its name for the JavaScript validator is created by concatenating the class name without backslashes \, the underscore _, and the method name. For example, write App\MyValidators::validateDivisibility as AppMyValidators_validateDivisibility and add it 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 by checking the individual rules added by addRule() and then calling event onValidate. Its handler can be used for additional validation, typically to verify the correct combination of values in multiple form elements.

If an error is detected, it is passed to the form using the addError() method. This can be called either on a specific element 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 discover an error when we are processing a valid form, e.g. when we write a new entry to the database and encounter a duplicate key. In this case, we pass the error back to the form using the addError() method. This can be called either on a specific item or directly on the form:

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

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

If possible, we recommend adding the error directly to the form element, as it will appear 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 element. You get them with getErrors().

Note that $form->getErrors() returns a summary of all error messages, even those passed directly to individual elements, not just directly to the form. Error messages passed only to the form are 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 zip code:

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

The filter is included between the validation rules and conditions and therefore depends on the order of the methods, ie the filter and the rule are called in the same order as is the order of the addFilter() and addRule() methods.

JavaScript Validation

The language of validation rules and conditions is powerful. Even though all constructions work both server-side and client-side, in JavaScript. Rules are transferred in HTML attributes data-nette-rules as JSON. The validation itself is handled by another script, which hooks all form's submit events, iterates over all inputs and runs respective validations.

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

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

<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script>

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

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

Or install via npm:

npm install nette-forms

And then load and run:

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

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

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

Dynamic JavaScript

Do you only want to show the address fields only if the user chooses to send the goods by post? No problem. The key is a pair of methods addCondition() & toggle():

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

This code says that when the condition is met, that is, when the checkbox is checked, the HTML element #address-container will be visible. And vice versa. So, we place the form elements with the recipient's address in a container with that ID, and when the checkbox is clicked, they are hidden or shown. 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 with no other special characters is treated as an element ID, the same as if it were preceded by the # character. The second optional parameter allows us to reverse the behavior, i.e. if we used toggle('#address-container', false), the element would be displayed only if the checkbox was unchecked.

The default JavaScript implementation changes the hidden property for 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

In certain cases, you need to disable validation. If a submit button isn't supposed to run validation after submitting (for example Cancel or Preview button), you can disable the validation by calling $submit->setValidationScope([]). You can also validate the form partially by specifying items to 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 'name' field
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Validates only 'age' field
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Validates 'details' container

Event onValidate on the form is always invoked and is not affected by the setValidationScope. onValidate event on the container is invoked only when this container is specified for partial validation.