Forms Rendering
The appearance of forms can be very diverse. In practice, we can encounter two extremes. On one hand, there is a need to render
a series of forms in an application that are visually similar to each other, and we appreciate the easy rendering without a
template using $form->render()
. This is usually the case with administrative interfaces.
On the other hand, there are various forms where each one is unique. Their appearance is best described using HTML language in the template. And of course, in addition to both mentioned extremes, we will encounter many forms that fall somewhere in between.
Rendering With Latte
The Latte templating system fundamentally facilitates the rendering of forms and their elements. First, we'll show how to render forms manually, element by element, to gain full control over the code. Later we will show how to automate such rendering.
You can have the proposal of a Latte template for the form generated using the method
Nette\Forms\Blueprint::latte($form)
, which will output it to the browser page. Then, you simply need to select the
code with a click and copy it into your project.
{control}
The easiest way to render a form is to write in a template:
{control signInForm}
The look of the rendered form can be changed by configuring Renderer and individual controls.
n:name
It is extremely easy to link the form definition in PHP code with HTML code. Just add the n:name
attributes.
That's how easy it is!
protected function createComponentSignInForm(): Form
{
$form = new Form;
$form->addText('username')->setRequired();
$form->addPassword('password')->setRequired();
$form->addSubmit('send');
return $form;
}
<form n:name=signInForm class=form>
<div>
<label n:name=username>Username: <input n:name=username size=20 autofocus></label>
</div>
<div>
<label n:name=password>Password: <input n:name=password></label>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
The look of the resulting HTML code is entirely in your hands. If you use the n:name
attribute with
<select>
, <button>
or <textarea>
elements, their internal content is
automatically filled in. In addition, the <form n:name>
tag creates a local variable $form
with
the drawn form object and the closing </form>
draws all undrawn hidden elements (the same applies to
{form} ... {/form}
).
However, we must not forget to render possible error messages. Both those that were added to individual elements by the
addError()
method (using {inputError}
) and those added directly to the form (returned by
$form->getOwnErrors()
):
<form n:name=signInForm class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div>
<label n:name=username>Username: <input n:name=username size=20 autofocus></label>
<span class=error n:ifcontent>{inputError username}</span>
</div>
<div>
<label n:name=password>Password: <input n:name=password></label>
<span class=error n:ifcontent>{inputError password}</span>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
More complex form elements, such as RadioList or CheckboxList, can be rendered item by item:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Don't you want to think for each element what HTML element to use for it in the template, whether <input>
,
<textarea>
etc.? The solution is the universal {input}
tag:
<form n:name=signInForm class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div>
{label username}Username: {input username, size: 20, autofocus: true}{/label}
{inputError username}
</div>
<div>
{label password}Password: {input password}{/label}
{inputError password}
</div>
<div>
{input send, class: "btn btn-default"}
</div>
</form>
If the form uses a translator, the text inside the {label}
tags will be translated.
Again, more complex form elements, such as RadioList or CheckboxList, can be rendered item by item:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
To render the <input>
itself in the Checkbox item, use {input myCheckbox:}
. HTML attributes
must be separated by a comma {input myCheckbox:, class: required}
.
{inputError}
Prints an error message for the form element, if it has one. The message is usually wrapped in an HTML element for styling.
Avoiding rendering an empty element if there is no message can be elegantly done with n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
We can detect the presence of an error using the hasErrors()
method and set the class of the parent element
accordingly:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Tags {form signInForm}...{/form}
are an alternative to
<form n:name="signInForm">...</form>
.
Automatic Rendering
With the {input}
and {label}
tags, we can easily create a generic template for any form. It will
iterate and render all of its elements sequentially, except for hidden elements, which are rendered automatically when the form is
terminated with the </form>
tag. It will expect the name of the rendered form in the $form
variable.
<form n:name=$form class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div n:foreach="$form->getControls() as $input"
n:if="$input->getOption(type) !== hidden">
{label $input /}
{input $input}
{inputError $input}
</div>
</form>
The used self-closing pair tags {label .../}
show the labels coming from the form definition in the PHP code.
You can save this generic template in the basic-form.latte
file and to render the form, just include it and pass
the form name (or instance) to the $form
parameter:
{include basic-form.latte, form: signInForm}
If you would like to influence the appearance of one particular form and draw one element differently, then the easiest way is to prepare blocks in the template that can be overwritten later. Blocks can also have dynamic names, so you can insert the name of the element to be drawn into them. For example:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
For the element e.g. username
this creates the block input-username
, which can be easily overridden
by using the tag {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternatively, the entire contents of the basic-form.latte
template can be defined as a block, including the $form
parameter:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
This will make it slightly easier to use:
{embed basic-form, signInForm}
...
{/embed}
You only need to import the block in one place, at the beginning of the layout template:
{import basic-form.latte}
Special Cases
If you need to render only the inner part of the form without HTML tags <form>
, for example when sending
snippets, hide them using the n:tag-if
attribute:
<form n:name=signInForm n:tag-if=false>
<div>
<label n:name=username>Username: <input n:name=username></label>
{inputError username}
</div>
</form>
Tag formContainer
helps with rendering of inputs inside a form container.
<p>Which news you wish to receive:</p>
{formContainer emailNews}
<ul>
<li>{input sport} {label sport /}</li>
<li>{input science} {label science /}</li>
</ul>
{/formContainer}
Rendering Without Latte
The easiest way to render a form is to call:
$form->render();
The look of the rendered form can be changed by configuring Renderer and individual controls.
Manual Rendering
Each form element has methods that generate the HTML code for the form field and label. They can return it as either a string or a Nette\Utils\Html object:
getControl(): Html|string
returns the HTML code of the elementgetLabel($caption = null): Html|string|null
returns the HTML code of the label, if any
This allows the form to be rendered element by element:
<?php $form->render('begin') ?>
<?php $form->render('errors') ?>
<div>
<?= $form['name']->getLabel() ?>
<?= $form['name']->getControl() ?>
<span class=error><?= htmlspecialchars($form['name']->getError()) ?></span>
</div>
<div>
<?= $form['age']->getLabel() ?>
<?= $form['age']->getControl() ?>
<span class=error><?= htmlspecialchars($form['age']->getError()) ?></span>
</div>
// ...
<?php $form->render('end') ?>
While for some elements getControl()
returns a single HTML element (e.g. <input>
,
<select>
etc.), for others it returns a whole piece of HTML code (CheckboxList, RadioList). In this case, you
can use methods that generate individual inputs and labels, for each item separately:
getControlPart($key = null): ?Html
returns the HTML code of a single itemgetLabelPart($key = null): ?Html
returns the HTML code for the label of a single item
These methods are prefixed with get
for historical reasons, but generate
would be
better, as it creates and returns a new Html
element on each call.
Renderer
It is an object that provides rendering of the form. It can be set by the $form->setRenderer
method. It is
passed control when the $form->render()
method is called.
If we don't set a custom renderer, the default renderer Nette\Forms\Rendering\DefaultFormRenderer will be used. This will render the form elements as an HTML table. The output looks like this:
<table>
<tr class="required">
<th><label class="required" for="frm-name">Name:</label></th>
<td><input type="text" class="text" name="name" id="frm-name" required value=""></td>
</tr>
<tr class="required">
<th><label class="required" for="frm-age">Age:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Gender:</label></th>
...
It's up to you, whether to use a table or not, and many web designers prefer different markups, for example a list. We may
configure DefaultFormRenderer
so it would not render into a table at all. We just have to set proper $wrappers. The
first index always represents an area and the second one it's element. All respective areas are shown in the picture:
By default a group of controls
is wrapped in <table>
, and every pair
is a table
row <tr>
containing a pair of label
and control
(cells <th>
and
<td>
). Let's change all those wrapper elements. We will wrap controls
into
<dl>
, leave pair
by itself, put label
into <dt>
and wrap
control
into <dd>
:
$renderer = $form->getRenderer();
$renderer->wrappers['controls']['container'] = 'dl';
$renderer->wrappers['pair']['container'] = null;
$renderer->wrappers['label']['container'] = 'dt';
$renderer->wrappers['control']['container'] = 'dd';
$form->render();
Results into the following snippet:
<dl>
<dt><label class="required" for="frm-name">Name:</label></dt>
<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>
<dt><label class="required" for="frm-age">Age:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Gender:</label></dt>
...
</dl>
Wrappers can affect many attributes. For example:
- add special CSS classes to each form input
- distinguish between odd and even lines
- make required and optional draw differently
- set, whether error messages are shown above the form or close to each element
Options
The behavior of Renderer can also be controlled by setting options on individual form elements. This way you can set the tooltip that is displayed next to the input field:
$form->addText('phone', 'Number:')
->setOption('description', 'This number will remain hidden');
If we want to place HTML content into it, we use Html class.
use Nette\Utils\Html;
$form->addText('phone', 'Phone:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Terms of service.</a>')
);
Html element can be also used instead of label: $form->addCheckbox('conditions', $label)
.
Grouping Inputs
Renderer allows to group elements into visual groups (fieldsets):
$form->addGroup('Personal data');
Creating new group activates it – all elements added further are added to this group. You may build a form like this:
$form = new Form;
$form->addGroup('Personal data');
$form->addText('name', 'Your name:');
$form->addInteger('age', 'Your age:');
$form->addEmail('email', 'Email:');
$form->addGroup('Shipping address');
$form->addCheckbox('send', 'Ship to address');
$form->addText('street', 'Street:');
$form->addText('city', 'City:');
$form->addSelect('country', 'Country:', $countries);
The renderer draws groups first and then elements that do not belong to any group.
Bootstrap Support
You can find examples of configuration of Renderer for Twitter Bootstrap 2, Bootstrap 3 and Bootstrap 4
HTML Attributes
To set arbitrary HTML attributes for form elements, use the method
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('number', 'Number:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Order by:', ['price', 'name'])
->setHtmlAttribute('onchange', 'submit()'); // calls JS function submit() on change
// To set attributes of the <form> itself
$form->setHtmlAttribute('id', 'myForm');
Specifying the type of element:
$form->addText('tel', 'Your telephone:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Please, fill in your telephone');
Setting the type and other attributes serves only for visual purposes. Verification of input correctness must occur on the server, which you can ensure by choosing an appropriate form control and specifying validation rules.
For individual items in radio or checkbox lists, we can set an HTML attribute with different values for each of them. Notice
the colon after style:
, which ensures the value is selected based on the key:
$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('style:', $styles);
Renders:
<label><input type="checkbox" name="colors[]" style="background:red" value="r">red</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>
For setting boolean attributes, such as readonly
, we can use the notation with a question mark:
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // use array for multiple keys, e.g. ['r', 'g']
Renders:
<label><input type="checkbox" name="colors[]" readonly value="r">red</label>
<label><input type="checkbox" name="colors[]" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>
For selectboxes, the setHtmlAttribute()
method sets the attributes of the <select>
element. If
we want to set the attributes for each <option>
, we will use the method setOptionAttribute()
.
Also, the colon and question mark used above work:
$form->addSelect('colors', 'Colors:', $colors)
->setOptionAttribute('style:', $styles);
Renders:
<select name="colors">
<option value="r" style="background:red">red</option>
<option value="g" style="background:green">green</option>
<option value="b">blue</option>
</select>
Prototypes
An alternative way to set HTML attributes is to modify the template from which the HTML element is generated. The template is
an Html
object and is returned by the getControlPrototype()
method:
$input = $form->addInteger('number');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
The label template returned by getLabelPrototype()
can also be modified in this way:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
For Checkbox, CheckboxList and RadioList items you can influence the element template that wraps the item. It is returned by
getContainerPrototype()
. By default it is an “empty” element, so nothing is rendered, but by giving it a name it
will be rendered:
$input = $form->addCheckbox('send');
$html = $input->getContainerPrototype();
$html->setName('div'); // <div>
$html->class('check'); // <div class="check">
echo $input->getControl();
// <div class="check"><label><input type="checkbox" name="send"></label></div>
In the case of CheckboxList and RadioList it is also possible to influence the item separator pattern returned by the method
getSeparatorPrototype()
. By default, it is an element <br>
. If you change it to a pair element, it
will wrap the individual items instead of separating them. It is also possible to influence the HTML element template of the item
labels, which returns getItemLabelPrototype()
.
Translating
If you are programming a multilingual application, you will probably need to render the form in different languages. The Nette Framework defines a translation interface for this purpose Nette\Localization\Translator. There is no default implementation in Nette, you can choose according to your needs from several ready-made solutions you can find on Componette. Their documentation tells you how to configure the translator.
The form supports outputting text through the translator. We pass it using the setTranslator()
method:
$form->setTranslator($translator);
From now on, not only all labels, but also all error messages or select box entries will be translated into another language.
It is possible to set a different translator for individual form elements or to disable translation completely with
null
:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
For validation rules, specific parameters are also passed to the translator, for example for rule:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
the translator is called with the following parameters:
$translator->translate('Password has to be at least %d characters long', 8);
and thus can choose the correct plural form for the word characters
by count.
Event onRender
Just before the form is rendered, we can have our code invoked. This can, for example, add HTML classes to the form elements
for proper display. We add the code to the onRender
array:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};