Prestação de formulários
A aparência das formas pode ser muito diversificada. Na prática, podemos encontrar dois extremos. Por um lado, há a
necessidade de renderizar uma série de formulários em uma aplicação que são visualmente semelhantes entre si, e apreciamos a
fácil renderização sem um modelo usando $form->render()
. Este é geralmente o caso das interfaces
administrativas.
Por outro lado, existem várias formas onde cada uma é única. Sua aparência é melhor descrita usando a linguagem HTML no modelo. E é claro que, além dos dois extremos mencionados, vamos encontrar muitos formulários que se enquadram em algum lugar no meio.
Renderização com Latte
O sistema de modelos Latte facilita fundamentalmente a renderização de formulários e seus elementos. Primeiro, mostraremos como renderizar formulários manualmente, elemento por elemento, para obter controle total sobre o código. Mais tarde, mostraremos como automatizar tal renderização.
Você pode ter a proposta de um modelo Latte para o formulário gerado usando o método
Nette\Forms\Blueprint::latte($form)
, que o enviará para a página do navegador. Em seguida, basta selecionar
o código com um clique e copiá-lo em seu projeto.
{control}
A maneira mais fácil de renderizar um formulário é escrever em um modelo:
{control signInForm}
A aparência da forma renderizada pode ser alterada configurando o Renderer e os controles individuais.
n:name
É extremamente fácil ligar a definição do formulário em código PHP com o código HTML. Basta adicionar os atributos
n:name
. É assim que é 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>
A aparência do código HTML resultante está inteiramente em suas mãos. Se você usar o atributo n:name
com
<select>
, <button>
ou <textarea>
elementos, seu conteúdo interno é
automaticamente preenchido. Além disso, o <form n:name>
cria uma variável local $form
com
o objeto do formulário desenhado e o fechamento </form>
desenha todos os elementos ocultos não desenhados
(o mesmo se aplica a {form} ... {/form}
).
No entanto, não devemos esquecer de entregar possíveis mensagens de erro. Tanto as que foram adicionadas a elementos
individuais pelo método addError()
(usando {inputError}
) como as que foram adicionadas diretamente ao
formulário (devolvidas 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>
Elementos de formulário mais complexos, tais como RadioList ou CheckboxList, podem ser renderizados item por item:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Você não quer pensar para cada elemento que elemento HTML usar para ele no template, se <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 utilizar um tradutor, o texto dentro das tags {label}
será traduzido.
Novamente, elementos mais complexos da forma, tais 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 tornar o <input>
no próprio item Checkbox, use {input myCheckbox:}
. Os atributos HTML
devem ser separados por uma vírgula {input myCheckbox:, class: required}
.
{inputError}
Imprime uma mensagem de erro para o elemento do formulário, se ele tiver um. A mensagem é geralmente envolvida em um
elemento HTML para estilização. Evitar renderizar um elemento vazio, se não houver mensagem, pode ser feito elegantemente com
n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Podemos detectar a presença de um erro usando o método hasErrors()
e definir a classe do elemento pai de
acordo:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Etiquetas {form signInForm}...{/form}
são uma alternativa para
<form n:name="signInForm">...</form>
.
Renderização automática
Com as etiquetas {input}
e {label}
, podemos criar facilmente um modelo genérico para qualquer
formulário. Ele irá iterar e renderizar todos os seus elementos sequencialmente, exceto os elementos ocultos, que são
renderizados automaticamente quando o formulário é encerrado com o </form>
tag. Esperará o nome da forma
renderizada 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 etiquetas de par auto-encerramento utilizadas {label .../}
mostram as etiquetas provenientes da definição do
formulário no código PHP.
Você pode salvar este modelo genérico no arquivo basic-form.latte
e para renderizar o formulário, basta
incluí-lo e passar o nome do formulário (ou instância) para o parâmetro $form
:
{include basic-form.latte, form: signInForm}
Se você gostaria de influenciar a aparência de uma determinada forma e desenhar um elemento de maneira diferente, então a maneira mais fácil é preparar blocos no modelo que podem ser sobregravados posteriormente. Os blocos também podem ter nomes dinâmicos, de modo que você pode inserir o nome do elemento a ser desenhado neles. Por exemplo, o nome do elemento a ser desenhado:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Para o elemento, por exemplo username
, isto cria o bloco input-username
, que pode ser facilmente
anulado usando a etiqueta {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternativamente, todo o conteúdo do modelo 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}
Isto tornará o uso um pouco mais fácil:
{embed basic-form, signInForm}
...
{/embed}
Você só precisa importar o bloco em um lugar, no início do modelo de layout:
{import basic-form.latte}
Casos especiais
Se você precisar renderizar somente a parte interna do formulário sem tags HTML <form>
por exemplo, ao
enviar snippets, oculte-os 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 etiqueta formContainer
ajuda na renderização de entradas dentro de um contêiner de formulário.
<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}
Renderização sem Latte
A maneira mais fácil de entregar um formulário é telefonar:
$form->render();
A aparência da forma renderizada pode ser alterada configurando o Renderer e os controles individuais.
Renderização manual
Cada elemento do formulário tem métodos que geram o código HTML para o campo e etiqueta do formulário. Eles podem devolvê-lo como uma string ou como um objeto Nette\UtilsHtml:
getControl(): Html|string
retorna o código HTML do elementogetLabel($caption = null): Html|string|null
retorna o código HTML da etiqueta, se houver
Isto permite que a forma seja renderizada elemento por elemento:
<?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 elementos getControl()
retorna um único elemento HTML (por exemplo
<input>
, <select>
etc.), para outros retorna um código HTML inteiro (CheckboxList,
RadioList). Neste caso, pode-se usar métodos que geram entradas e etiquetas individuais, para cada item separadamente:
getControlPart($key = null): ?Html
retorna o código HTML de um único itemgetLabelPart($key = null): ?Html
retorna o código HTML para a etiqueta de um único item
Estes métodos são prefixados com get
por razões históricas, mas generate
seria
melhor, pois cria e devolve um novo elemento Html
em cada chamada.
Renderizador
É um objeto que proporciona a renderização da forma. Ele pode ser definido pelo método $form->setRenderer
.
Ele é passado no controle quando o método $form->render()
é chamado.
Se não definirmos um renderizador personalizado, será usado o renderizador padrão Nette\Forms\Rendering\DefaultFormRenderer. Isto renderizará os elementos do formulário como uma tabela HTML. A saída é parecida com esta:
<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>
...
Depende de você, se usar uma mesa ou não, e muitos web designers preferem marcações diferentes, por exemplo, uma lista.
Podemos configurar DefaultFormRenderer
para que ela não se torne uma mesa. Só temos que definir os embrulhos adequados.
O primeiro índice sempre representa uma área e o segundo é seu elemento. Todas as áreas respectivas são mostradas na
figura:
Por padrão, um grupo de controls
está envolto em <table>
e cada pair
é uma linha
de tabela <tr>
contendo um par de label
e control
(células <th>
e
<td>
). Vamos mudar todos esses elementos do invólucro. Vamos embrulhar controls
em
<dl>
deixe pair
por si só, coloque label
em <dt>
e embrulhe
control
em <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();
Resultados no seguinte trecho:
<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>
Os embaladores podem afetar muitos atributos. Por exemplo:
- adicionar classes especiais de CSS a cada entrada de formulário
- distinguir entre linhas ímpares e pares
- fazer o sorteio obrigatório e opcional de forma diferente
- definido, se as mensagens de erro são mostradas acima do formulário ou perto de cada elemento
Opções
O comportamento do Renderer também pode ser controlado ajustando opções em elementos individuais da forma. Desta forma, é possível definir a ponta da ferramenta que é exibida ao lado do campo de entrada:
$form->addText('phone', 'Number:')
->setOption('description', 'This number will remain hidden');
Se quisermos colocar conteúdo HTML dentro dele, usamos a classe Html.
use Nette\Utils\Html;
$form->addText('phone', 'Phone:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Terms of service.</a>')
);
O elemento Html também pode ser usado no lugar do rótulo:
$form->addCheckbox('conditions', $label)
.
Agrupamento de entradas
O Renderer permite agrupar elementos em grupos visuais (conjuntos de campos):
$form->addGroup('Personal data');
A criação de um novo grupo o ativa – todos os elementos adicionados são acrescentados a este grupo. Você pode construir uma forma como esta:
$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);
O renderizador desenha primeiro os grupos e depois os elementos que não pertencem a nenhum grupo.
Suporte de Bootstrap
Você pode encontrar exemplos de configuração do Renderer para Twitter Bootstrap 2, Bootstrap 3 e Bootstrap 4
Atributos HTML
Para definir atributos HTML arbitrários para elementos de formulário, use o método
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('número', 'Número:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Order by:', ['price', 'name'])
->setHtmlAttribute('onchange', 'submit()'); // chama a função JS submit() on change
// Para definir os atributos do próprio <form>
$form->setHtmlAttribute('id', 'myForm');
Especificando o tipo de elemento:
$form->addText('tel', 'Your telephone:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Please, fill in your telephone');
A definição do tipo e de outros atributos serve apenas para fins visuais. A verificação da exatidão da entrada deve ocorrer no servidor, o que pode ser garantido pela escolha de um controle de formulário apropriado e pela especificação de regras de validação.
Para itens individuais em listas de rádio ou caixas de seleção, podemos definir um atributo HTML com valores diferentes para
cada um deles. Observe os dois pontos após style:
, o que garante que o valor seja selecionado com base
na chave:
$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('style:', $styles);
Renderizadores:
<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>
Para definir atributos booleanos, como readonly
, podemos usar a notação com um ponto de interrogação:
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // use array para múltiplas chaves, por exemplo ['r', 'g'].
Renderizadores:
<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>
Para as caixas de seleção, o método setHtmlAttribute()
define os atributos do <select>
elemento. Se quisermos definir os atributos para cada <option>
usaremos o método
setOptionAttribute()
. Além disso, o cólon e o ponto de interrogação utilizados acima funcionam:
$form->addSelect('colors', 'Colors:', $colors)
->setOptionAttribute('style:', $styles);
Renderizadores:
<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>
Protótipos
Uma forma alternativa de definir atributos HTML é modificar o modelo a partir do qual o elemento HTML é gerado. O template
é um objeto Html
e é retornado pelo método getControlPrototype()
:
$input = $form->addInteger('número');
$html = $input->getControlPrototype(); // <input>
$html->classe('big-number'); // <input class="big-number">
O modelo de etiqueta devolvido por getLabelPrototype()
também pode ser modificado desta forma:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
Para os itens Checkbox, CheckboxList e RadioList você pode influenciar o modelo do elemento que envolve o item. Ele é
devolvido por getContainerPrototype()
. Por padrão é um elemento „vazio“, portanto nada é renderizado, mas ao
dar-lhe um nome, ele será renderizado:
$input = $form->addCheckbox('enviar');
$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 da CheckboxList e RadioList também é possível influenciar o padrão separador de itens devolvido pelo método
getSeparatorPrototype()
. Por padrão, ele é um elemento <br>
. Se você o mudar para um elemento
de par, ele envolverá os itens individuais em vez de separá-los. Também é possível influenciar o modelo de elemento HTML das
etiquetas dos itens, que retorna getItemLabelPrototype()
.
Traduzindo
Se você estiver programando uma aplicação multilíngüe, provavelmente precisará renderizar o formulário em diferentes idiomas. O Nette Framework define uma interface de tradução para este fim Nette\Localization\Translator. Não há uma implementação padrão em Nette, você pode escolher de acordo com suas necessidades entre várias soluções prontas que você pode encontrar na Componette. Sua documentação lhe diz como configurar o tradutor.
O formulário suporta a saída de texto através do tradutor. Nós o passamos usando o método
setTranslator()
:
$form->setTranslator($translator);
De agora em diante, não apenas todas as etiquetas, mas também todas as mensagens de erro ou entradas de caixa de seleção serão traduzidas para outro idioma.
É possível definir um tradutor diferente para elementos individuais do formulário ou desativar completamente a tradução
com null
:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
Para regras de validação, parâmetros específicos também são passados para o tradutor, por exemplo, para regra:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
o tradutor é chamado com os seguintes parâmetros:
$translator->translate('Password has to be at least %d characters long', 8);
e assim pode escolher a forma plural correta para a palavra characters
por contagem.
Evento onRender
Pouco antes de o formulário ser entregue, podemos ter nosso código invocado. Isto pode, por exemplo, adicionar classes HTML
aos elementos do formulário para uma exibição adequada. Acrescentamos o código à matriz onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};