Forms in Presenters

Nette Forms significantly simplify the creation and processing of web forms. In this chapter, you will learn how to use forms inside presenters.

If you are interested in using them completely standalone without the rest of the framework, there is a guide for standalone usage.

First Form

Let's try writing a simple registration form. Its code will be as follows:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];

and in the browser, it will be displayed like this:

A form in a presenter is an object of the Nette\Application\UI\Form class; its predecessor Nette\Forms\Form is intended for standalone use. We added controls named name, password, and a submit button. Finally, the line $form->onSuccess[] = [$this, 'formSucceeded']; states that after submission and successful validation, the method $this->formSucceeded() should be called.

From the presenter's perspective, the form is a regular component. Therefore, it is treated as a component and integrated into the presenter using a factory method. It will look like this:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// here we will process the data sent by the form
		// $data->name contains name
		// $data->password contains password
		$this->flashMessage('You have successfully signed up.');
		$this->redirect('Home:');
	}
}

And in the template, the form is rendered using the {control} tag:

<h1>Registration</h1>

{control registrationForm}

And that's basically everything :-) We have a functional and perfectly secured form.

Now you're probably thinking it was too fast, wondering how it's possible that the formSucceeded() method is called and what parameters it receives. Yes, you're right, this deserves an explanation.

Nette introduces a refreshing mechanism called Hollywood style. Instead of you, as a developer, having to constantly ask if something happened (‘was the form submitted?’, ‘was it submitted validly?’, and ‘was it not forged?’), you tell the framework ‘when the form is validly filled, call this method’ and leave the subsequent work to it. If you program in JavaScript, you are intimately familiar with this style of programming. You write functions that are called when a certain event occurs. And the language passes the appropriate arguments to them.

This is precisely how the presenter code above is constructed. The $form->onSuccess array represents a list of PHP callbacks that Nette calls the moment the form is submitted and correctly filled (i.e., it is valid). Within the presenter life cycle, this is a so-called signal, so they are called after the action* method and before the render* method. And to each callback, it passes the form itself as the first parameter and the submitted data as an ArrayHash object (or stdClass, or a custom class) as the second. You can omit the first parameter if you don't need the form object. The second parameter can be smarter, but more on that later.

The $data object contains the name and password properties with the data entered by the user. Usually, we send the data directly for further processing, which might be, for example, insertion into a database. However, an error might occur during processing, for instance, the username is already taken. In such a 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, username is already in use.');

Besides onSuccess, there is also onSubmit: callbacks are called whenever the form is submitted, even if it is not filled correctly. And also onError: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in onSuccess or onSubmit using addError().

After processing the form, we redirect to another page. This prevents the unwanted resubmission of the form by using the refresh, back button, or navigating through browser history.

Try adding other form controls.

Access to Controls

The form is a component of the presenter, in our case named registrationForm (after the factory method name createComponentRegistrationForm), so anywhere in the presenter, you can access the form using:

$form = $this->getComponent('registrationForm');
// alternative syntax: $form = $this['registrationForm'];

Individual form controls are also components, so you can access them in the same way:

$input = $form->getComponent('name'); // or $input = $form['name'];
$button = $form->getComponent('send'); // or $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 mandatory, so we mark it using the setRequired() method. Its argument is the text of the error message displayed if the user doesn't fill in the name. If the argument is omitted, a default error message is used.

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

Try submitting the form without filling in the name, and you'll see an error message displayed, and the browser or server will reject it until you fill in the field.

At the same time, you can't cheat the system by entering only spaces in the field, for example. No way. Nette automatically trims leading and trailing whitespace. Try it out. 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 fool the form and send a multi-line string as the name. Even here, Nette won't be tricked, and line breaks will be converted to spaces.)

The form is always validated on the server side, but JavaScript validation is also generated, which runs instantly, and the user learns about the error immediately, without needing to submit the form to the server. This is handled by the netteForms.js script. Include it in your layout template:

<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 wraps required controls in elements with the CSS class required. Try adding the following stylesheet to your template, and the ‘Name’ label will be red. This elegantly highlights required fields for users:

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

We add further validation rules using the addRule() method. The first parameter is the rule, the second is again the text of the error message, and an argument for the validation rule may follow. What does that mean?

Let's extend the form with a new optional field ‘age’, which must be an integer (addInteger()) and also 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 doesn't fill in the field, the validation rules will not be checked, as the element is optional.

This creates room for a small refactoring. In the error message and the third parameter, the numbers are duplicated, which is not 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 substitute 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('Pick a password')
	->addRule($form::MinLength, 'Your 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 confirmation. Using validation rules, we check if both passwords are the same (Form::Equal). As the argument, we provide a reference to the first password using square brackets:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Passwords do not match.', $form['password'])
	->setOmitted();

Using setOmitted(), we marked a control whose value we don't actually 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; conditions can be created, parts of the page can be shown or hidden based on them, etc. You will learn everything in the chapter on form validation.

Default Values

We commonly 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 the default values:

//$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 web accessibility rules – all labels are written as <label> elements and associated with their respective form controls. Clicking on the label automatically focuses 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 truly many ways to render a form, so a separate chapter on rendering is dedicated to it.

Mapping to Classes

Let's return to the formSucceeded() method, which receives the submitted data in the second parameter $data as an ArrayHash object (or stdClass). Because it's a generic class, similar to 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 a 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. Simply specify the class as the type of the $data parameter in the handler method:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name is instance of RegistrationFormData
	$name = $data->name;
	// ...
}

You can also specify array as the type, and then the data will be passed as an array.

Similarly, you can use the getValues() method, passing the class name or an object to hydrate as a parameter:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

If the forms have a multi-level structure composed of containers, create a separate class for each:

$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 infers from the type of the $person property that it should map the container to the PersonFormData class. If the property were to contain an array of containers, specify the type array and pass the class to be mapped directly to the container:

$person->setMappedType(PersonFormData::class);

You can generate a proposal for the form's data class using the Nette\Forms\Blueprint::dataClass($form) method, which prints it to the browser page. 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. We can create a separate handler function for each button. Set it as a handler for the onClick event:

$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

These handlers are called only if the form is validly filled (unless validation is disabled for the button), just like the onSuccess event. The difference is that the first parameter passed can be the submit button object instead of the form, depending on the type hint you specify:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

When the form is submitted by pressing the Enter key, it is treated as if it were submitted by the first submit button.

Event onAnchor

When you build a form in a factory method (like createComponentRegistrationForm), it doesn't yet know if it has been submitted or with what data. However, there are cases where we need to know the submitted values, perhaps the form's appearance depends on them, or they are needed for dependent select boxes, etc.

Therefore, you can have the code that builds the form called only when it is ‘anchored,’ meaning it is already connected to the presenter and knows its submitted data. Place such code in the $onAnchor array:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// this function will be called when the form knows data it was submitted with
	// so you can use the getValue() method
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Vulnerability Protection

Nette Framework places great emphasis on security and therefore meticulously ensures the security of forms. It does this completely transparently and requires no manual setup.

Besides protecting forms against attacks 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 the data from the form is always clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered ones 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 aren't even aware 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 logged in. The server then believes the request was made by the victim willingly. Therefore, Nette prevents POST forms from being submitted from a different domain. If, for some reason, you want to disable this protection and allow form submission from another domain, use:

$form->allowCrossOrigin(); // WARNING! Disables protection!

This protection uses a SameSite cookie named _nss. SameSite cookie protection might not be 100% reliable, so it's advisable to also enable token protection:

$form->addProtection();

It is strongly recommended to apply this protection to forms in the administrative parts of your website that modify sensitive data. The framework defends against CSRF attacks by generating and verifying 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 a website, the session is usually already started due to user login. Otherwise, start the session using Nette\Http\Session::start().

Using One Form in Multiple Presenters

If you need to use the same form in multiple presenters, we recommend creating a factory for it, which you then inject into the presenters. A suitable location for such a class is, for example, the app/Forms directory.

The factory class might look like this:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}

We request the class to produce the form in the component factory method within the presenter:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// we can change the form, here for example we change the label on the button
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // and add handler
	return $form;
}

The form processing handler can also be provided by the factory itself:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		$form->onSuccess[] = function (Form $form, $data): void {
			// we process our submitted form here
		};
		return $form;
	}
}

So, we've covered a quick introduction to forms in Nette. Try looking in the examples directory in the distribution for more inspiration.

version: 4.0 3.x 2.x