Renderização de Formulários

A aparência dos formulários pode variar muito. Na prática, podemos encontrar dois extremos. Por um lado, há a necessidade de renderizar vários formulários na aplicação que são visualmente tão semelhantes quanto dois ovos, e apreciamos a renderização fácil sem um template usando $form->render(). Este é geralmente o caso das interfaces administrativas.

Por outro lado, existem formulários diversos onde a regra é: cada peça é um original. A sua forma é melhor descrita usando a linguagem HTML no template do formulário. E, claro, além dos dois extremos mencionados, encontraremos muitos formulários que se situam algures entre eles.

Renderização usando Latte

Sistema de templates Latte facilita fundamentalmente a renderização de formulários e dos seus controlos. Primeiro, mostraremos como renderizar formulários manualmente, controlo por controlo, obtendo assim controlo total sobre o código. Mais tarde, mostraremos como essa renderização pode ser automatizada.

Pode gerar o design do template Latte do formulário usando o método Nette\Forms\Blueprint::latte($form), que o imprime na página do navegador. Depois, basta clicar para selecionar o código e copiá-lo para o seu projeto.

{control}

A maneira mais simples de renderizar um formulário é escrever no template:

{control signInForm}

A aparência do formulário renderizado desta forma pode ser influenciada configurando o Renderer e os controlos individuais.

n:name

A definição do formulário no código PHP pode ser ligada de forma extremamente fácil ao código HTML. Basta adicionar atributos n:name. É tão fácil!

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>

Tem controlo total sobre a forma do código HTML resultante. Se usar o atributo n:name nos elementos <select>, <button> ou <textarea>, o seu conteúdo interno será preenchido automaticamente. Além disso, a tag <form n:name> cria uma variável local $form com o objeto do formulário a ser desenhado, e a tag de fecho </form> renderiza todos os controlos ocultos não renderizados (o mesmo se aplica a {form} ... {/form}).

No entanto, não devemos esquecer de renderizar possíveis mensagens de erro. Tanto aquelas que foram adicionadas aos controlos individuais pelo método addError() (usando {inputError}), como aquelas adicionadas diretamente ao formulário (retornadas por $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>

Controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item desta forma:

{foreach $form[gender]->getItems() as $key => $label}
	<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}

{label} {input}

Não quer pensar em que elemento HTML usar no template para cada controlo, seja <input>, <textarea>, etc.? A solução é a tag universal {input}:

<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>

Se o formulário usar um tradutor, o texto dentro das tags {label} será traduzido.

Mesmo neste caso, controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item:

{foreach $form[gender]->items as $key => $label}
	{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}

Para renderizar apenas o <input> no controlo Checkbox, use {input myCheckbox:}. Neste caso, separe sempre os atributos HTML com uma vírgula {input myCheckbox:, class: required}.

{inputError}

Exibe a mensagem de erro para um controlo de formulário, se houver alguma. A mensagem geralmente é envolvida num elemento HTML para estilização. Evitar a renderização de um elemento vazio, se não houver mensagem, pode ser feito elegantemente usando n:ifcontent:

<span class=error n:ifcontent>{inputError $input}</span>

A presença de um erro pode ser verificada com o método hasErrors() e, com base nisso, definir a classe do elemento pai:

<div n:class="$form[username]->hasErrors() ? 'error'">
	{input username}
	{inputError username}
</div>

{form}

As tags {form signInForm}...{/form} são uma alternativa a <form n:name="signInForm">...</form>.

Renderização automática

Graças às tags {input} e {label}, podemos facilmente criar um template genérico para qualquer formulário. Ele iterará e renderizará sequencialmente todos os seus controlos, exceto os controlos ocultos, que são renderizados automaticamente ao fechar o formulário com a tag </form>. O nome do formulário a ser renderizado será esperado na variável $form.

<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>

As tags de par auto-fechadas {label .../} usadas exibem os rótulos provenientes da definição do formulário no código PHP.

Guarde este template genérico, por exemplo, no ficheiro basic-form.latte e para renderizar o formulário, basta incluí-lo e passar o nome (ou instância) do formulário para o parâmetro $form:

{include basic-form.latte, form: signInForm}

Se quiser intervir na forma de um formulário específico durante a renderização e, por exemplo, renderizar um controlo de forma diferente, o caminho mais simples é preparar blocos no template que possam ser sobrescritos posteriormente. Os blocos também podem ter nomes dinâmicos, pelo que pode inserir o nome do controlo a ser renderizado neles. Por exemplo:

...
	{label $input /}
	{block "input-{$input->name}"}{input $input}{/block}
...

Para o controlo, por exemplo, username, será criado o bloco input-username, que pode ser facilmente sobrescrito usando a tag {embed}:

{embed basic-form.latte, form: signInForm}
	{block input-username}
		<span class=important>
			{include parent}
		</span>
	{/block}
{/embed}

Alternativamente, todo o conteúdo do template basic-form.latte pode ser definido como um bloco, incluindo o parâmetro $form:

{define basic-form, $form}
	<form n:name=$form class=form>
		...
	</form>
{/define}

Graças a isso, a sua chamada será ligeiramente mais simples:

{embed basic-form, signInForm}
	...
{/embed}

O bloco só precisa ser importado num único local, no início do template de layout:

{import basic-form.latte}

Casos especiais

Se precisar de renderizar apenas a parte interna do formulário sem as tags HTML <form>, por exemplo, ao enviar snippets, oculte-as usando o atributo n:tag-if:

<form n:name=signInForm n:tag-if=false>
	<div>
		<label n:name=username>Username: <input n:name=username></label>
		{inputError username}
	</div>
</form>

A tag {formContainer} ajuda na renderização de controlos dentro de um contêiner de formulário.

<p>Quais notícias deseja receber:</p>

{formContainer emailNews}
<ul>
	<li>{input sport} {label sport /}</li>
	<li>{input science} {label science /}</li>
</ul>
{/formContainer}

Renderização sem Latte

A maneira mais simples de renderizar um formulário é chamar:

$form->render();

A aparência do formulário renderizado desta forma pode ser influenciada configurando o Renderer e os controlos individuais.

Renderização manual

Cada controlo de formulário possui métodos que geram o código HTML do campo de formulário e do rótulo. Podem retorná-lo como uma string ou como um objeto Nette\Utils\Html:

  • getControl(): Html|string retorna o código HTML do controlo
  • getLabel($caption = null): Html|string|null retorna o código HTML do rótulo, se existir

O formulário pode, assim, ser renderizado controlo por controlo:

<?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') ?>

Enquanto para alguns controlos getControl() retorna um único elemento HTML (por exemplo, <input>, <select>, etc.), para outros retorna um pedaço inteiro de código HTML (CheckboxList, RadioList). Nesse caso, pode usar métodos que geram inputs e rótulos individuais, para cada item separadamente:

  • getControlPart($key = null): ?Html retorna o código HTML de um item
  • getLabelPart($key = null): ?Html retorna o código HTML do rótulo de um item

Estes métodos têm o prefixo get por razões históricas, mas generate seria melhor, pois a cada chamada criam e retornam um novo elemento Html.

Renderer

É um objeto que garante a renderização do formulário. Pode ser definido pelo método $form->setRenderer. O controlo é passado para ele quando o método $form->render() é chamado.

Se não definirmos o nosso próprio renderizador, será usado o renderizador padrão Nette\Forms\Rendering\DefaultFormRenderer. Ele renderiza os controlos do formulário na forma de uma tabela HTML. A saída parece-se com isto:

<table>
<tr class="required">
	<th><label class="required" for="frm-name">Nome:</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">Idade:</label></th>

	<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>

<tr>
	<th><label>Sexo:</label></th>
	...

Se usar ou não uma tabela para a estrutura do formulário é discutível, e muitos web designers preferem outra marcação. Por exemplo, uma lista de definição. Reconfiguraremos, portanto, o DefaultFormRenderer para que ele renderize o formulário na forma de uma lista. A configuração é feita editando o array $wrappers. O primeiro índice representa sempre a área e o segundo o seu atributo. As áreas individuais são mostradas na imagem:

Por padrão, o grupo de controlos controls é envolvido por uma tabela <table>, cada pair representa uma linha da tabela <tr> e o par label e control são células <th> e <td>. Agora mudaremos os elementos envolventes. Inseriremos a área controls num contêiner <dl>, deixaremos a área pair sem contêiner, inseriremos label em <dt> e, finalmente, envolveremos control com as tags <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();

O resultado é este código HTML:

<dl>
	<dt><label class="required" for="frm-name">Nome:</label></dt>

	<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>


	<dt><label class="required" for="frm-age">Idade:</label></dt>

	<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>


	<dt><label>Sexo:</label></dt>
	...
</dl>

No array wrappers, é possível influenciar toda uma gama de outros atributos:

  • adicionar classes CSS a tipos individuais de controlos de formulário
  • distinguir linhas pares e ímpares com classes CSS
  • distinguir visualmente itens obrigatórios e opcionais
  • determinar se as mensagens de erro serão exibidas diretamente nos controlos ou acima do formulário

Options

O comportamento do Renderer também pode ser controlado definindo options nos controlos de formulário individuais. Assim, pode-se definir um rótulo que será exibido ao lado do campo de entrada:

$form->addText('phone', 'Número:')
	->setOption('description', 'Este número permanecerá oculto');

Se quisermos colocar conteúdo HTML nele, usamos a classe Html

use Nette\Utils\Html;

$form->addText('phone', 'Número:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Termos de armazenamento do seu número</a>')
	);

O elemento Html também pode ser usado em vez de um rótulo: $form->addCheckbox('conditions', $label).

Agrupamento de controlos

O Renderer permite agrupar controlos em grupos visuais (fieldsets):

$form->addGroup('Dados Pessoais');

Após criar um novo grupo, ele torna-se ativo e cada controlo recém-adicionado também é adicionado a ele. Assim, o formulário pode ser construído desta forma:

$form = new Form;
$form->addGroup('Dados Pessoais');
$form->addText('name', 'Seu nome:');
$form->addInteger('age', 'Sua idade:');
$form->addEmail('email', 'Email:');

$form->addGroup('Endereço de entrega');
$form->addCheckbox('send', 'Enviar para o endereço');
$form->addText('street', 'Rua:');
$form->addText('city', 'Cidade:');
$form->addSelect('country', 'País:', $countries);

O Renderer primeiro renderiza os grupos e só depois os controlos que não pertencem a nenhum grupo.

Suporte para Bootstrap

Nos exemplos encontrará exemplos de como configurar o Renderer para Twitter Bootstrap 2, Bootstrap 3 e Bootstrap 4

Atributos HTML

Para definir quaisquer atributos HTML para controlos de formulário, usamos o método setHtmlAttribute(string $name, $value = true):

$form->addInteger('number', 'Número:')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', 'Ordenar por:', ['preço', 'nome'])
	->setHtmlAttribute('onchange', 'submit()'); // enviar ao alterar


// Para definir atributos do próprio <form>
$form->setHtmlAttribute('id', 'myForm');

Especificação do tipo de controlo:

$form->addText('tel', 'Seu telefone:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'escreva o telefone');

A definição do tipo e outros atributos servem apenas para fins visuais. A verificação da correção das entradas deve ocorrer no servidor, o que é garantido pela escolha do controlo de formulário apropriado e pela indicação das regras de validação.

Para itens individuais em listas de rádio ou checkbox, podemos definir um atributo HTML com valores diferentes para cada um deles. Observe os dois pontos após style:, que garantem a seleção do valor pela chave:

$colors = ['r' => 'vermelho', 'g' => 'verde', 'b' => 'azul'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Cores:', $colors)
	->setHtmlAttribute('style:', $styles);

Exibe:

<label><input type="checkbox" name="colors[]" style="background:red" value="r">vermelho</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">azul</label>

Para definir atributos lógicos, como readonly, podemos usar a notação com ponto de interrogação:

$form->addCheckboxList('colors', 'Cores:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // para mais chaves use um array, ex. ['r', 'g']

Exibe:

<label><input type="checkbox" name="colors[]" readonly value="r">vermelho</label>
<label><input type="checkbox" name="colors[]" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">azul</label>

No caso de selectboxes, o método setHtmlAttribute() define os atributos do elemento <select>. Se quisermos definir atributos para os <option> individuais, usamos o método setOptionAttribute(). As notações com dois pontos e ponto de interrogação mencionadas acima também funcionam:

$form->addSelect('colors', 'Cores:', $colors)
	->setOptionAttribute('style:', $styles);

Exibe:

<select name="colors">
	<option value="r" style="background:red">vermelho</option>
	<option value="g" style="background:green">verde</option>
	<option value="b">azul</option>
</select>

Protótipos

Uma maneira alternativa de definir atributos HTML consiste em modificar o modelo a partir do qual o elemento HTML é gerado. O modelo é um objeto Html e é retornado pelo método getControlPrototype():

$input = $form->addInteger('number', 'Número:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number');            // <input class="big-number">

Desta forma, também é possível modificar o modelo do rótulo, que é retornado por getLabelPrototype():

$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive');         // <label class="distinctive">

Para os controlos Checkbox, CheckboxList e RadioList, pode influenciar o modelo do elemento que envolve todo o controlo. Ele é retornado por getContainerPrototype(). No estado padrão, é um elemento “vazio”, então nada é renderizado, mas ao definir um nome para ele, ele será renderizado:

$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>

No caso de CheckboxList e RadioList, também é possível influenciar o modelo do separador dos itens individuais, que é retornado pelo método getSeparatorPrototype(). No estado padrão, é o elemento <br>. Se o alterar para um elemento de par, ele envolverá os itens individuais em vez de separá-los. E, além disso, é possível influenciar o modelo do elemento HTML do rótulo nos itens individuais, que é retornado por getItemLabelPrototype().

Tradução

Se está a programar uma aplicação multilíngue, provavelmente precisará renderizar o formulário em diferentes versões de idioma. O Nette Framework define uma interface para tradução para este propósito Nette\Localization\Translator. No Nette, não há implementação padrão, pode escolher de acordo com as suas necessidades entre várias soluções prontas que encontra no Componette. Na sua documentação, aprenderá como configurar o tradutor.

Os formulários suportam a exibição de textos através do tradutor. Passamos para eles usando o método setTranslator():

$form->setTranslator($translator);

A partir deste momento, não apenas todos os rótulos, mas também todas as mensagens de erro ou itens de caixas de seleção serão traduzidos para outro idioma.

Para controlos de formulário individuais, é possível definir um tradutor diferente ou desativar completamente a tradução com o valor null:

$form->addSelect('carModel', 'Modelo:', $cars)
	->setTranslator(null);

Para regras de validação, parâmetros específicos também são passados ao tradutor, por exemplo, para a regra:

$form->addPassword('password', 'Senha:')
	->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8);

o tradutor é chamado com estes parâmetros:

$translator->translate('A senha deve ter pelo menos %d caracteres', 8);

e, portanto, pode escolher a forma plural correta da palavra caracteres de acordo com o número.

Evento onRender

Pouco antes de o formulário ser renderizado, podemos deixar o nosso código ser chamado. Ele pode, por exemplo, adicionar classes HTML aos controlos do formulário para exibição correta. Adicionamos o código ao array onRender:

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
versão: 4.0