Forms in Presenters
Nette Forms greatly facilitates the creation and processing of web forms in your applications. In this chapter, you will learn how to use forms inside presenters.
If you do not use presenters and Nette Application, there is a guide for you: standalone forms.
First Form
We will try to write a simple registration form. Its code will look like this:
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 the result should look like this:
The form in the presenter is an object of the class Nette\Application\UI\ orm
, its predecessor
Nette\Forms\Form
is intended for standalone use. We added the fields name, password and sending button to it.
Finally, the line with $form->onSuccess
says that after submission and successful validation, the
$this->formSucceeded()
method should be called.
From the presenter's point of view, the form is a common component. Therefore, it is treated as a component and incorporated into the presenter using factory method. It will look like this:
use Nette;
use Nette\Application\UI\Form;
class HomepagePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm()
{
$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)
{
// 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('Homepage:');
}
}
And render in template is done using {control}
tag:
<h1>Registration</h1>
{control registrationForm}
And that is all :-) We have a functional and perfectly secured form.
Now you're probably thinking that it was too fast, wondering how it's possible that the method formSucceeded()
is
called and what the parameters it gets. Sure, you're right, this deserves an explanation.
Nette comes up with a cool mechanism, which we call Hollywood style. Instead of having to constantly ask if something has happened (“was the form submitted?”, “was it validly submitted?” or “was it not forged?”), you say to the framework “when the form is validly completed, call this method” and leave further work on it. If you program in JavaScript, you are 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 how the above presenter code is built. Array $form->onSuccess
represents the list of PHP callbacks that
Nette will call when the form is submitted and filled in correctly. Within the presenter's life cycle it is a
so-called signal, so they are called after the action*
method and before the render*
method. And it
passes to each callback the form itself in the first parameter and the sent data as object ArrayHash in the second. If you prefer to get the data as an
array, add the type hint array
for parameter.
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 can be, for example, insertion into the database. However,
an error may occur during processing, for example, the username is already taken. In this case, we pass the error back to the form
using addError()
and let it redrawn, with an error message:
$form->addError('Sorry, username is already in use.');
In addition to onSuccess
, there is also onSubmit
: callbacks are always called after the form is
submitted, even if it is not filled in correctly. And finally 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 will redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the refresh, back button, or moving the browser history.
Try adding more form controls.
Access to Controls
The form is a component of the presenter, in our case named registrationForm
(after the name of the factory method
createComponentRegistrationForm
), so anywhere in the presenter you can get to the form using:
$form = $this->getComponent('registrationForm');
// alternative syntax: $form = $this['registrationForm'];
Also individual form controls are 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 used several times, but the form has no validation rules yet. Let's fix it.
The name will be mandatory, so we will mark it 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.');
Try to submit the form without the name filled in and you will see that an error message is displayed and the browser or server will reject it until you fill it.
At the same time, you will not be able cheat the system by typing only spaces in the input, for example. No way. Nette automatically trims left and right 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 fool the forms and send a multiline string as the name. Even here, Nette won't be fooled and the line breaks will change to spaces.)
The form is always validated on the server side, but JavaScript validation is also generated, which is quick and the user knows
of the error immediately, without having to send the form to the server. This is handled by the script netteForms.js
.
Insert it into the layout template:
<script src="https://nette.github.io/resources/js/2/netteForms.min.js"></script>
If you look in the source code of the page with form, you may notice that Nette inserts the required fields into elements with
a CSS class required
. Try adding the following style to the template, and the “Name” label will be red.
Elegantly, we mark the required fields for the users:
<style>
.required label { color: maroon }
</style>
Additional validation rules will be added by method addRule()
. The first parameter is rule, the second is again
the text of the error message, and the optional validation rule argument can follow. What does that mean?
The form will get another optional input age with the condition, that it has to be a number (addInteger()
)
and in certain boundaries ($form::RANGE
). And here we will use the third argument of addRule()
, the
range itself:
$form->addInteger('age', 'Age:')
->setRequired(false)
->addRule($form::RANGE, 'You must be older 18 years and be under 120.', [18, 120]);
If the user does not fill in the field, the validation rules will not be verified, because the field is optional.
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 form 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
%d
can be used:
->addRule($form::RANGE, 'You must be older %d years and be under %d.', [18, 120]);
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', 8);
We will add a field passwordVerify
to the form, where the user enters the password again, for checking. Using
validation rules, we check whether both passwords are the same ($form::EQUAL
). And as an argument we give 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, 'Password mismatch', $form['password'])
->setOmitted();
Using setOmitted()
, we marked an element whose value we don't really care about and which exists only for
validation. Its value is not passed to $data
.
We have a fully functional form with validation in PHP and JavaScript. Nette's validation capabilities are much broader, you can create conditions, display and hide parts of a page according to them, etc. You can find out everything in the chapter on form validation.
Default Values
We often set default values for form controls:
$form->addEmail('email', 'Email')
->setDefaultValue($lastUsedEmail);
It is often useful to set default values for all controls at once. For example, when the form is used to edit records. We read the record from the database and set it as 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. The individual controls follows basic web accessibility guidelines. All labels are
generated as <label>
elements and are associated with their inputs, clicking on the label moves the cursor on
the input.
We can set any HTML attributes for each element. For example, add a placeholder:
$form->addInteger('age', 'Age:')
->setHtmlAttribute('placeholder', 'Please fill in the age');
There are really a lot of ways to render a form, so it's dedicated chapter about rendering.
Multiple Submit Buttons
If the form has more than one button, we usually need to distinguish which one was pressed. We can create own 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 also called only in the case form is valid, as in the case of the onSuccess
event. The
difference is that the first parameter is not the form, but the submit button:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
When a form is submitted with the Enter key, it is treated as if it had been submitted with the first button.
Event onAnchor
When you build a form in a factory method (such as createComponentRegistrationForm
), it doesn't yet know if it has
been submitted or the data it was submitted with. But there are cases where we need to know the submitted values, perhaps
it's depend on them what the form will look like, or they are used for dependent selectboxes, etc.
Therefore, you can have the code that builds the form called when it is anchored, i.e. it is already linked to the presenter
and knows its submitted data. We will put such code into 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 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.
A CSRF attack is that an attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. It's strongly recommended to apply this protection to the forms in an administrative part of your application which changes sensitive data.
The protection is pretty simple:
$form->addProtection('Your session has expired. Please return to the home page and try again.');
The framework protects against a CSRF attack by generating and validating authentication token that is stored in a session (the
argument is the error message shown if the token has expired). That's why it is necessary to have an session started before
displaying the form. In the administration part of the website, the session is usually already started, due to the user's login.
Otherwise, start the session with the method Nette\Http\Session::start()
, in the presenter you can use
$this->getSession()->start()
. You can also enable a session for the entire application using autoStart configuration.
Using One Form in Multiple Presenters
If you need to use one form in more than one presenter, we recommend that you create a factory for it, which you then pass on
to the presenter. A suitable location for such a class is, for example, the directory app/Forms
.
The factory class might look like this:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create()
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Log in');
return $form;
}
}
We ask the class to produce the form in the factory method for components in the presenter:
public function __construct(SignInFormFactory $formFactory)
{
$this->formFactory = $formFactory;
}
protected function createComponentSignInForm()
{
$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 delivered from the factory:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create()
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Log in');
$form->onSuccess[] = function (Form $form, $data) {
// we process our submitted form here
};
return $form;
}
}
So, we have a quick introduction to forms in Nette. Try looking in the examples directory in the distribution for more inspiration.