09日目: フォーム

昨日までで Ark の基本的な開発の流れは説明しました。 今日はフォームフレームワークに焦点を合わせましょう。

フォームフレームワーク

ほとんどのWebサイトにはフォームがあります。 シンプルな問い合わせから、たくさんのフィールドがある複雑なものまで、さまざまなフォームがあります。 フォームを作る作業は、Web 開発者にとってもっとも複雑で退屈な作業の1つです。 HTML フォームを書き、それぞれのフィールド用のバリデーションルールを実装し、値を処理してデータベースに保存し、エラーメッセージを表示し、エラーの場合はフィールドを再設定することなどが必要です。

もちろん、何度も車輪の再発明をする代わりに、Ark はフォームの管理を簡単にするフレームワークを提供します。フォームフレームワークは3つの部分で構成されます:

フォーム

Ark のフォームは 1 フォーム 1 クラスで表されます。 それぞれのクラスは 1 つ以上のフィールドから構成されます。 それぞれのフィールドは名前、バリデーションルール、ウィジェットなどを持ちます。

次のクラスではシンプルな Contact フォームを定義しています。

package MyContactForm;
use Ark 'Form';

param subject => (
    label       => 'Subject',
    type        => 'TextField',
    constraints => [
        'NOT_NULL',
        [ 'LENGTH', 0, 100 ],
    ],
);

param message => (
    label       => 'Message',
    type        => 'TextField',
    widget      => 'textarea',
    constraints => ['NOT_NULL'],
);

param sender => (
    label       => 'E-Mail',
    type        => 'EmailField',
    constraints => ['NOT_NULL'],
);

1;

フォームクラスはこのように param シンタックスを使いフィールドを定義していきます。 今回の例では subject(題名)、message(メッセージ)、sender(送信者) の 3 つのフィールドをフォームに持たせます。

TextFieldEmailField はフィールドの型を表します。 TextField は普通の <input type="text".../> なフィールドを表します。EmailFieldTextField に加え、Email 用のバリデーションルールが加わります。

また TextField 型は基本的には input フィールドを出力しますが、widget を指定することで見た目を自由に変えることができます。(組み込みで気に入る widget がない場合は自分で定義することもできます) ここでは message を textarea にするために textarea ウィジェットを選択しています。

constraints の項目には型にあらかじめ用意されているバリデーション以外のルールを追加する場合に使用し、ここでは FormValidator::Lite に用意されたルールを指定することができます。

フォームの継承

フォームを作るごとにクラスを 1 つ作成する必要があるのは面倒に感じるかもしれません。 しかしクラスにすることによって、継承などのプログラム言語のメリットをフォームでも得ることができるようになります。 Ark のフォームでは共通の部品はベースクラスに定義しておき、固有の部分だけ追加する、置き換えるということが行えます。

先程のコンタクトフォームを継承したフォームを作ってみましょう。

package MyContactForm2;
use Ark 'Form';

extends 'MyContactForm';

param '+message' => (
    constraints => [
        'NOT_NULL',
        ['LENGTH', 0, 3000],
    ],
);

param url => (
    label => 'URL',
    type  => 'TextField',
);

1;

このフォームは message フィールドにバリデーションルールを追加し、新しく URL フィールドを追加しています。 このように親で定義されているフィールドの一部を上書きする場合は param '+name' などのように + をつけることで、定義した部分だけ上書きすることができます。 + をつけないと新しく定義しなおすという意味になります。

求人フォームの作成

それでは Jobeet の世界に戻りましょう。 ユーザーが求人を投稿することができる求人フォームを作成していきます。

求人用のフォームクラスを以下のように定義してみましょう:

package Jobeet::Form::Job;
use Ark 'Form';

use Jobeet::Models;

param category => (
    label   => 'Category',
    type    => 'ChoiceField',
    choices => [map { $_->slug => $_->name } models('Schema::Category')->all],
    constraints => [
        'NOT_NULL',
    ],
);

param type => (
    label   => 'Type',
    type    => 'ChoiceField',
    choices => [
        'full-time' => 'Full time',
        'part-time' => 'Part time',
        'freelance' => 'Freelance',
    ],
    constraints => [
        'NOT_NULL',
    ],
);

param company => (
    label       => 'Company',
    type        => 'TextField',
    constraints => [
        'NOT_NULL',
    ],
);

param url => (
    label => 'URL',
    type  => 'URLField',
);

param position => (
    label       => 'position',
    type        => 'TextField',
    constraints => [
        'NOT_NULL',
    ],
);

param location => (
    label       => 'Location',
    type        => 'TextField',
    constraints => [
        'NOT_NULL',
    ],
);

param description => (
    label       => 'Description',
    type        => 'TextField',
    widget      => 'textarea',
    attr        => {
        cols => 30,
        rows => 4,
    },
    constraints => [
        'NOT_NULL',
    ],
);

param how_to_apply => (
    label       => 'How to apply?',
    type        => 'TextField',
    widget      => 'textarea',
    attr        => {
        cols => 30,
        rows => 4,
    },
    constraints => [
        'NOT_NULL',
    ],
);

param email => (
    label       => 'Email',
    type        => 'TextField',
    constraints => [
        'NOT_NULL',
    ],
);

1;

Job コントローラからこのフォームクラスを使用します。 フォームを使用するときはコントローラに

with 'Ark::ActionClass::Form';

という行を入れましょう。これを入れるとアクションに :Form 属性を使うことができるようになります。 ここでは求人登録のアクション create アクションにこのフォームクラスを紐付けます。

sub create :Local :Form('Jobeet::Form::Job') {
    my ($self, $c) = @_;

    $c->stash->{form} = $self->form;
}

こうしておくとこのアクション内では $self->form でフォームクラスにアクセスすることができます。 またこのアクションに対応するテンプレート job/create.mt はまだ作っていませんでした。ここで作成しましょう。

? extends 'common/jobs_base';

? block content => sub {

<h1>New Job</h1>

<form method="post">
<?= $c->stash->{form}->render ?>
<input type="submit" value="Preview your job" />
</form>

? } # endblock content

このように render メソッドを使用することでフォームのhtmlをレンダリングすることができます。 また render('name') などのようにフィールド名を指定するとそのフィールドだけをレンダリングすることもできます。

また render の代わりに label、input、error、メソッドを使用するとそれぞれラベル、inputフィールド、エラー文字列をそれぞれ個別にレンダリングすることもできます。ここではこれを使用してテンプレートを変更していきます。

? my $form = $c->stash->{form};

? extends 'common/jobs_base';

? block content => sub {

<h1>New Job</h1>

<form method="post">

<table id="job_form">
  <tfoot>
    <tr>
      <td colspan="2">
        <input type="submit" value="Preview your job" />
      </td>
    </tr>
  </tfoot>
  <tbody>
? for my $field (qw/category type company url position location description how_to_apply email/) {
    <tr>
      <th><?= raw_string $form->label($field) ?></th>
      <td>
? if ($form->is_error($field)) {
        <ul class="error_list">
? for my $err (@{ $form->error_messages($field) }) {
          <li><?= raw_string $err ?></li>
? } # endfor $err
        </ul>
? } # endif
        <?= raw_string $form->input($field) ?>
      </td>
    </tr>
? } # endfor $field
  </tbody>
</table>
</form>

? } # endblock content

スタイルの効いたフォームが表示されたでしょうか。

ここで、空のままフォームを送信すると適切なエラーが表示されます。また入力されたデータはフォームをサブミットしたあとでもキープされる事がわかると思います。フォームクラスはこのようなフォームにまつわる様々な面倒なことをかわりにやってくれます。

また明日

今日はフォームクラスの基本的な使い方を学習しました。

求人フォームにはまだまだやらなければあります、プレビューの実装、その後のデータ更新。これらは今日の宿題とします! 明日答え合わせをしましょう。

それではまた明日!