Forms Rendering
Forms' appearances can differ greatly. In fact, there are two extremes. One side is the need to render a set of very similar forms all over again, with little to none effort. Usually administrations and back-ends.
The other side is tiny sweet forms, each one being a piece of art. Their layout can best be written in HTML. Of course, besides those extremes, there are many forms just in between.
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.
{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}
Code Proposal {formPrint}
You can generate a similar Latte code for a form using the {formPrint}
tag. If you place it in a template, you
will see the draft code instead of the normal rendering. Then just select it and copy it into your project.
{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>
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}
<span class=error n:ifcontent>{inputError $input}</span>
</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}
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" 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" 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" value=""></dd>
<dt><label class="required" for="frm-age">Age:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" 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()
.
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);
};