フォームの描画

フォームの見た目はとても多様です。実務では 2 つの極端に出くわします。ひとつは、見た目のそろったたくさんのフォームをアプリケーションで描く必要がある場合で、そこでは $form->render() によるテンプレートなしの簡単な描画がありがたく思えます。管理画面がまさにその例です。

もうひとつは、ひとつひとつが違う多様なフォームです。その見た目は、フォームのテンプレートの HTML で書くのがいちばんです。そしてもちろん、この 2 つの極端のあいだのどこかに落ちるフォームにも数多く出会います。

Latte での描画

Latte のテンプレートシステムは、フォームとその要素の描画を大いに簡単にします。まず、コードを完全に思いどおりにするために、フォームを要素ごとに手で描く方法をお見せします。そのあとで、そうした描画を自動化する方法を見ます。

フォームの Latte のテンプレートは Nette\Forms\Blueprint::latte($form) メソッドで生成でき、ブラウザのページに出力されます。あとはクリックしてコードを選び、プロジェクトにコピーするだけです。

{control}

フォームを描くいちばん単純な方法は、テンプレートに次のように書くことです。

{control signInForm}

描かれるフォームの見た目は、Rendererの設定と個々の要素で変えられます。

n:name

PHP のコードのフォームの定義と HTML のコードを結び付けるのはきわめて簡単です。n:name の属性を足すだけです。それだけです。

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>ユーザー名: <input n:name=username size=20 autofocus></label>
	</div>
	<div>
		<label n:name=password>パスワード: <input n:name=password></label>
	</div>
	<div>
		<input n:name=send class="btn btn-default">
	</div>
</form>

できあがる HTML のコードの見た目を完全に思いどおりにできます。n:name 属性を <select><button><textarea> の要素で使うと、その中身は自動的に埋められます。さらに <form n:name> のタグは、描かれるフォームのオブジェクトが入った局所変数 $form を作り、閉じる </form> のタグは描かれていない隠しの要素を描きます({form} ... {/form} でも同じです)。

とはいえ、エラーのメッセージを描くのを忘れてはいけません。これには addError() メソッドで個々の要素に足されたエラー({inputError} で描かれます)と、フォームそのものに足されたエラー($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>ユーザー名: <input n:name=username size=20 autofocus></label>
		<span class=error n:ifcontent>{inputError username}</span>
	</div>
	<div>
		<label n:name=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>

RadioList や CheckboxList のような、もっと込み入ったフォームの要素は、次のように項目ごとに描けます。

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

{label} {input}

要素ごとにテンプレートでどの HTML 要素を使うか、<input> なのか <textarea> なのかを考えたくないですか。その解が万能の {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}ユーザー名: {input username, size: 20, autofocus: true}{/label}
		{inputError username}
	</div>
	<div>
		{label password}パスワード: {input password}{/label}
		{inputError password}
	</div>
	<div>
		{input send, class: "btn btn-default"}
	</div>
</form>

フォームが翻訳器を使っているなら、フォームの定義から描かれるラベル(たとえば {label username /})は翻訳されます。{label}{/label} のタグのあいだに直接書かれた文は翻訳されません。

ここでも、RadioList や CheckboxList のような、もっと込み入ったフォームの要素は項目ごとに描けます。

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

Checkbox の要素の <input> だけを描くには {input myCheckbox:} を使います。この場合、HTML の属性は必ずコンマで区切ってください。{input myCheckbox:, class: required} のようにです。

{inputError}

フォームの要素のエラーのメッセージがあれば表示します。このメッセージは、体裁を整えるためにふつう HTML の要素で包みます。メッセージがないときに空の要素が描かれるのを防ぐには、n:ifcontent を使うと優雅です。

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

エラーがあるかどうかは hasErrors() メソッドで確かめられ、それに応じて親の要素のクラスを設定できます。

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

{form}

{form signInForm}...{/form} のタグは <form n:name="signInForm">...</form> の代わりになります。引数があれば名前とコンマで区切ってください。{form signInForm, class: foo} のようにです。

名前の前に置く scope の語は、フォームをスタックに積むだけで(つまり {input}{label} などがそれに結び付きます)、<form> のタグは描きません。フォームの一部を、たとえばスニペットの中で描くのに便利です。すでにフォームが有効なら、名前はそれを基準に解決されるので、{form scope}{formContainer} の代わりにもなります。

{form scope signInForm}
	{input username}
{/form}

detached の語は空の <form></form> を描き、すべての要素を HTML の form 属性でそれに結び付けます。おかげで、HTML がふつうは禁じているフォームの中のフォームを置けます。切り離されたフォームには HTML の id が要りますが、名前を与えれば自動的に生成されます(下の outerForm のようにです)。

{form detached outerForm}
	...
{/form}

自動的な描画

{input}{label} のタグのおかげで、どんなフォームにも使える汎用のテンプレートを簡単に作れます。それはすべての要素を順に回って描きます。ただし隠しの要素は除きます。それは </form> のタグでフォームを閉じたときに自動的に描かれるからです。このテンプレートは、描くフォームの名前が $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>

ここで使っている自分で閉じる対のタグ {label .../} は、PHP のコードのフォームの定義から来るラベルを表示します。

この汎用のテンプレートを、たとえば basic-form.latte というファイルに保存します。フォームを描くには、それを取り込んで $form のパラメータにフォームの名前(かインスタンス)を渡すだけです。

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

特定のフォームの描画の見た目だけを変えたい、たとえばある要素を違うふうに描きたいなら、いちばん簡単なのはテンプレートにあとから上書きできるブロックを用意することです。ブロックには動的な名前も付けられるので、描く要素の名前を差し込めます。たとえば次のようにです。

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

たとえば username という名前の要素なら input-username というブロックができ、{embed}のタグで簡単に上書きできます。

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

あるいは basic-form.latte テンプレートの中身全体を、$form のパラメータも含めてブロックとして定義できます。

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

これで呼び出しが少し簡単になります。

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

このブロックは 1 か所、レイアウトのテンプレートの先頭で取り込むだけで済みます。

{import basic-form.latte}

特別な場合

たとえばスニペットを送るときなど、<form> の HTML のタグなしでフォームの内側だけを描く必要があるなら、n:tag-if 属性でそれらを隠します。

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

フォームのコンテナの中の要素を描くには、{formContainer} のタグ、あるいは新しい {form scope}が役に立ちます。

<p>どのニュースを受け取りますか:</p>

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

Latte なしの描画

フォームを描くいちばん簡単な方法は、次を呼ぶことです。

$form->render();

描かれるフォームの見た目は、Rendererの設定と個々の要素で変えられます。

手での描画

フォームのそれぞれの要素には、フォームの項目とそのラベルの HTML のコードを生成するメソッドがあります。それらは文字列としても Nette\Utils\Htmlオブジェクトとしても返せます。

  • getControl(): Html|string は要素の HTML のコードを返します
  • getLabel($caption = null): Html|string|null は、あればラベルの HTML のコードを返します

おかげでフォームを要素ごとに描けます。

<?php $form->render('begin') ?>
<?php $form->render('ownerrors') ?>

<div>
	<?= $form['name']->getLabel() ?>
	<?= $form['name']->getControl() ?>
	<span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span>
</div>

<div>
	<?= $form['age']->getLabel() ?>
	<?= $form['age']->getControl() ?>
	<span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span>
</div>

// ...

<?php $form->render('end') ?>

要素によっては getControl() がひとつの HTML 要素(たとえば <input><select> など)を返しますが、HTML のコードのひとまとまり(CheckboxList、RadioList)を返すものもあります。その場合は、項目ごとに個々の入力とラベルを生成するメソッドを使えます。

  • getControlPart($key = null): Html はひとつの項目の HTML のコードを返します
  • getLabelPart($key = null): Html はひとつの項目のラベルの HTML のコードを返します

これらのメソッドに get の接頭辞が付いているのは歴史的な事情によるもので、呼ばれるたびに新しい Html 要素を作って返すので、generate のほうがふさわしいでしょう。

Renderer

これはフォームを描くことを受け持つオブジェクトです。$form->setRenderer() メソッドで設定できます。$form->render() メソッドが呼ばれると、そこへ制御が渡ります。

独自の描画器を設定しなければ、既定の描画器 Nette\Forms\Rendering\DefaultFormRendererが使われます。これはフォームの要素を HTML の表に描きます。出力は次のようになります。

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

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

<tr>
	<th><label>性別:</label></th>
	...

フォームの構造に表を使うべきかは議論の分かれるところで、多くのウェブデザイナーは定義リストのような別のマークアップを好みます。ですから DefaultFormRenderer を設定し直して、フォームをリストとして描かせましょう。設定は $wrappers配列を書き換えて行います。最初の添字はいつも領域を、2 つめはその属性を表します。個々の領域は図のとおりです。

既定では controls のまとまりは <table> で包まれ、それぞれの pair は表の行 <tr> を、labelcontrol の組はセル <th><td> を表します。では包む要素を変えましょう。controls の領域を <dl> のコンテナに入れ、pair の領域はコンテナなしにし、label<dt> に入れ、最後に control<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();

その結果、次の HTML のコードになります。

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

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


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

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


	<dt><label>性別:</label></dt>
	...
</dl>

wrappers の配列では、ほかにも多くの属性に手を入れられます。

  • フォームの要素の種類ごとに CSS のクラスを足す
  • 奇数と偶数の行を CSS のクラスで区別する
  • 必須の項目と省略できる項目を見た目で区別する
  • エラーのメッセージを要素のすぐ隣に表示するか、フォームの上に表示するかを決める

オプション

Renderer の振る舞いは、個々のフォームの要素に オプション を設定しても操れます。こうして入力欄の隣に現れる説明を設定できます。

$form->addText('phone', '番号:')
	->setOption('description', 'この番号は公開されません');

そこに HTML の内容を置きたいなら、Htmlクラスを使います。

use Nette\Utils\Html;

$form->addText('phone', '電話:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">利用条件。</a>')
	);

Html の要素はラベルの代わりにも使えます。$form->addCheckbox('conditions', $label) のようにです。

入力をまとめる

Renderer では、要素を見た目のまとまり(fieldset)にまとめられます。

$form->addGroup('個人情報');

新しいまとまりを作るとそれが有効になり、新しく足された要素はそこにも足されます。ですからフォームは次のように組み立てられます。

$form = new Form;
$form->addGroup('個人情報');
$form->addText('name', 'お名前:');
$form->addInteger('age', '年齢:');
$form->addEmail('email', 'メール:');

$form->addGroup('お届け先');
$form->addCheckbox('send', '住所へ届ける');
$form->addText('street', '通り:');
$form->addText('city', '市:');
$form->addSelect('country', '国:', $countries);

描画器はまずまとまりを描き、そのあとどのまとまりにも属さない要素を描きます。

Bootstrap への対応

examples のディレクトリには、Twitter Bootstrap 2Bootstrap 3Bootstrap 4向けに Renderer を設定する例があります。

HTML の属性

フォームの要素に好きな HTML の属性を設定するには、setHtmlAttribute(string $name, $value = true) メソッドを使います。

$form->addInteger('number', '番号:')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', '並び順:', ['価格', '名前'])
	->setHtmlAttribute('onchange', 'submit()'); // 変わったらフォームを送信します


// <form> 要素そのものの属性を設定するには
$form->setHtmlAttribute('id', 'myForm');

要素の種類を指定します。

$form->addText('tel', '電話番号:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', '電話番号を入力してください');

種類やそのほかの属性の設定は見た目のためだけのものです。入力が正しいかの確認はサーバー側で行わなければならず、それはふさわしいフォームの要素を選び、検証の規則を指定することで果たします。

ラジオやチェックボックスの一覧の個々の項目には、項目ごとに違う値の HTML の属性を設定できます。style: のうしろのコロンに注目してください。これでキーに応じて値が選ばれます。

$colors = ['r' => '赤', 'g' => '緑', 'b' => '青'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', '色:', $colors)
	->setHtmlAttribute('style:', $styles);

描かれるのは次のとおりです。

<label><input type="checkbox" name="colors[]" style="background:red" value="r">赤</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">緑</label>
<label><input type="checkbox" name="colors[]" value="b">青</label>

readonly のような真偽の属性を設定するには、疑問符を使う書き方が使えます。

$form->addCheckboxList('colors', '色:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // 複数のキーには配列を使います。たとえば ['r', 'g']

描かれるのは次のとおりです。

<label><input type="checkbox" name="colors[]" readonly value="r">赤</label>
<label><input type="checkbox" name="colors[]" value="g">緑</label>
<label><input type="checkbox" name="colors[]" value="b">青</label>

セレクトボックスでは、setHtmlAttribute() メソッドは <select> 要素の属性を設定します。個々の <option> 要素に属性を設定したいなら、setOptionAttribute() メソッドを使います。先ほどのコロンと疑問符の書き方もここで働きます。

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

描かれるのは次のとおりです。

<select name="colors">
	<option value="r" style="background:red">赤</option>
	<option value="g" style="background:green">緑</option>
	<option value="b">青</option>
</select>

プロトタイプ

HTML の属性を設定するもうひとつの方法は、HTML 要素が生成されるもとになるひな型を変えることです。このひな型は Html オブジェクトで、getControlPrototype() メソッドが返します。

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

getLabelPrototype() が返すラベルのひな型も同じやり方で変えられます。

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

Checkbox、CheckboxList、RadioList の要素では、要素全体を包む要素のひな型に手を入れられます。これは getContainerPrototype() が返します。既定では「空の」要素なので何も描かれませんが、名前を与えれば描かれるようになります。

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

CheckboxList と RadioList では、getSeparatorPrototype() メソッドが返す、個々の項目の区切りのひな型にも手を入れられます。既定では <br> の要素です。これを対の要素に変えると、項目を区切る代わりに個々の項目を包みます。さらに getItemLabelPrototype() が返す、個々の項目のラベルの HTML 要素のひな型にも手を入れられます。

翻訳

多言語のアプリケーションを作っているなら、フォームを言語ごとに描く必要が出てくるでしょう。Nette Framework はそのために翻訳のインターフェース Nette\Localization\Translatorを定めています。Nette には既定の実装がないので、Componetteにあるできあいの解のいくつかから、必要に応じて選べます。翻訳器の設定の仕方はそれぞれのドキュメントに書かれています。

フォームは翻訳器を通した文の出力に対応しています。setTranslator() メソッドで渡します。

$form->setTranslator($translator);

この時点から、すべてのラベルだけでなく、すべてのエラーのメッセージ、セレクトボックスの項目、入力のプレースホルダも対象の言語に翻訳されます。

フォームの要素ごとに違う翻訳器を設定することも、値を null にして翻訳を完全に切ることもできます。

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

検証の規則では、翻訳器に特定のパラメータも渡されます。たとえば次の規則では、

$form->addPassword('password', 'パスワード:')
	->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません', 8);

翻訳器は次のパラメータで呼ばれます。

$translator->translate('パスワードは %d 文字以上でなければなりません', 8);

ですから数に応じて、characters という語の正しい複数形を選べます。

onRender イベント

フォームが描かれる直前に、自分のコードを呼ばせられます。このコードはたとえば、正しく表示させるためにフォームの要素に HTML のクラスを足せます。コードは onRender 配列に足します。

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
バージョン: 4.x