Django、フォームの表示方法まとめ

/ PythonDjangoBulmaBootstrap4

7日前に更新

概要

Djangoのフォームオブジェクトを表示する際、様々な選択肢があります。

{{ form_as_p }}で簡単に表示することもできますし、{% for field in form %} でフォーム内のフィールドを順番に取り出すこともできます。{{ form.name }}のように、一つ一つ自力で取り出すことも可能です。今回はこういった色々な方法をまとめていきます。

前提となるフォーム

次のフォームを例に使っていきます。

from django import forms


CATEGORIES = (
    ('1', 'お仕事の依頼'),
    ('2', 'サイト内容に関する問い合わせ'),
)


class ContactForm(forms.Form):
    """問い合わせ用フォーム"""
    name = forms.CharField(
        label='お名前', max_length=50,
        required=False, help_text='※任意'
    )
    email = forms.EmailField(
        label='メールアドレス', required=False, help_text='※任意'
    )
    text = forms.CharField(label='問い合わせ内容', widget=forms.Textarea)
    category = forms.ChoiceField(label='カテゴリ', choices=CATEGORIES)

    def clean_name(self):
        name = self.cleaned_data.get('name')
        if name in ('ばか', 'あほ', 'まぬけ', 'うんこ'):
            self.add_error('name', '名前に暴言を含めないでください。')
            if name == 'ばか':
                self.add_error('name', 'ばかは特にダメです。')
        return name

    def clean(self):
        category = self.cleaned_data.get('category')
        email = self.cleaned_data.get('email')
        if category == '1' and not email:
            self.add_error(None, 'お仕事の依頼の場合、メールアドレスは必須です。')
        return self.cleaned_data

お問い合わせ用に使うフォームで、名前、メールアドレス、内容、問い合わせカテゴリの4つあります。名前とメールアドレスは、required=Falseで入力を任意にしています。問い合わせ内容は複数行の入力ができるように、widget=forms.Textareaとしています。

エラーの表示も確認したかったので、少し処理を追加しています。clean_name()は、名前欄に「あほ」とか「ばか」とか入っていないかの確認です。clean()は、カテゴリが「お仕事の依頼」だった場合に、メールアドレスが入力されているかを確認します。

一括で取り出す

フォームを表示する一番簡単な方法から紹介します。テンプレートファイルに、次のように書きましょう。

<form action="" method="POST">
    {{ form.as_p }}
    {% csrf_token %}
    <button type="submit">送信</button>
</form>

これは次のように表示されます。
form.as_pでの表示

{{ form.as_p }}と書くだけで、フォーム内容が全て表示されます。また、help_textのような補足部分もちゃんと表示してくれます。

もちろん、エラーも全て表示されます。
form.as_p エラーあり

form.as_pは、名前のとおり各入力欄をp要素で囲んでいます。as_pの他には、as_tableとas_ulがあります。

{{ form.as_table }}は、名前のとおりtable要素を利用する際に使えます。<table>で囲んでおくことを忘れないようにしましょう。

<form action="" method="POST">
    <table>
        {{ form.as_table }}
    </table>
    {% csrf_token %}
    <button type="submit">送信</button>
</form>

見た目は次のようになります。 form.as_table

{{ form.as_ul }}という、ul要素を利用する際に使えるものもあります。

<form action="" method="POST">
    <ul>
        {{ form.as_ul }}
    </ul>
    {% csrf_token %}
    <button type="submit">送信</button>
</form>

見た目は次のようになります。
form.as_ul

これらは1行でフォーム内容を完全に表示できるので、フォームの動作確認といった場合に非常に有効です。

ただ、柔軟性には欠けています。全てのエラー内容を上部に集めたい場合や、もしかしたら右下とかに表示したいかもしれませんが、そういった場合には対処できません。

table要素、ul要素が必ず作られるas_tableとas_ulは使いどころが限られるでしょう。form.as_pはまだ見込みがありそうですが、p要素の中にはulやdiv要素を入れれないという致命的な弱点があります(興味のある方は、コンテンツモデルについて調べてみましょう)。

今回のように入力欄にエラーがあるとそれは<ul class="errorlist">のようなul要素ができ、1つ1つのエラーが<li>エラー内容<li>として作成されます。しかし、p要素はul要素を入れれないので、ulがpの外側に出てしまうのです。実を言うと、上のform.as_pの例は実際にそうなっています。

つまり、次のようなHTMLになるかと思いきや...

<p>
    <ul class="errorlist">
          <li>名前に暴言を含めないでください。</li>
            <li>ばかは特にダメです。</li>
     </ul>
    <label for="id_name">お名前:</label>
        <input type="text" name="name" value="ばか" maxlength="50" id="id_name">
        <span class="helptext">※任意</span>
</p>

実際には、次のようなHTMLになるのです。

<ul class="errorlist">
        <li>名前に暴言を含めないでください。</li>
    <li>ばかは特にダメです。</li>
</ul>
<p>
    <label for="id_name">お名前:</label>
        <input type="text" name="name" value="ばか" maxlength="50" id="id_name">
        <span class="helptext">※任意</span>
</p>

問題は他にもあります。カテゴリの選択欄をselect要素ではなく、radioにしたいとしましょう。このようなウィジェットの変更やCSSのclass属性変更については、Django、モデルフォームのウィジェットを変更する幾つかの方法で説明しています。

category = forms.ChoiceField(label='カテゴリ', choices=CATEGORIES, widget=forms.RadioSelect)

Djangoが作成するラジオボタンというのは、ul要素に囲まれて出力されます。次のようなHTMLとして出力しようとしますが...

<p>
    <label for="id_category_0">カテゴリ:</label>
        <ul id="id_category">
        <li>
                    <label for="id_category_0"><input type="radio" name="category" value="1" required id="id_category_0">お仕事の依頼</label>
             </li>
             <li>
                 <label for="id_category_1"><input type="radio" name="category" value="2" required id="id_category_1"> サイト内容に関する問い合わせ</label>
             </li>
        </ul>
</p>

p要素にはulは入れれないので、次のようになります。

<p>
    <label for="id_category_0">カテゴリ:</label>
</p>
<ul id="id_category">
    <li>
            <label for="id_category_0"><input type="radio" name="category" value="1" required id="id_category_0">お仕事の依頼</label>
        </li>
        <li>
              <label for="id_category_1"><input type="radio" name="category" value="2" required id="id_category_1"> サイト内容に関する問い合わせ</label>
        </li>
</ul>

この挙動はラジオボタンだけでなく、チェックボックスに変更した際も同様です。また、Djangoで、サジェスト機能付きフォームを作るのような凝ったウィジェットでは、div要素を内部的に作成することもありますが、当然pにdivは入りません。

こういった問題に対処しようとすると、結局のところ他の方法を利用することになります。

forでフィールドを取り出す

割と楽で、そこそこ柔軟なのがこの方法です。

as_p等のように、ラベル、入力欄、ヘルプテキスト、エラーを表示したい場合は、次のように書きましょう。

<form action="" method="POST">
    {{ form.non_field_errors }}
    {% for field in form %}
        <div class="field">
            {{ field.label_tag }}
            {{ field }}
            {% if field.help_text %}
                <span class="helptext">{{ field.help_text }}</span>
            {% endif %}
            {{ field.errors }}
        </div>
    {% endfor %}

    <div class="field">
        <button type="submit">送信</button>
    </div>

    {% csrf_token %}
</form>

{% for field in form %}は、フォームの各フィールドを取り出しています。

今回、div.fieldという要素で各フィールドを囲んでいます。div要素は中に何でも入れれるので、↑で触れたような問題も起きません。

{{ field.label_tag }}は、label要素を作成しています。<label for="id_name">お名前:</label> といった要素ですね。たまにあるのが、label要素のclassを指定したいというケースです。この場合は、<label class="myclass" for="{{ field.id_for_label }}">{{ field.label }}:</label>としましょう。

{{ field }}は、<input type="text" name="name" maxlength="50" id="id_name">といった要素を作成します。

{{ field.help_text }}は、help_textの部分ですね。help_textはHTML要素を作らずテキストのみなので、<span class="help_text">という要素で囲んでいます。そうしておくと、あとでこの部分だけ小さくしたり、色を変えたりといったことができるでしょう。help_textがない場合もあるので、ifタグを使って判断しています。

{{ field.errors }}は、フィールドに紐づくエラーが全て表示されます。

for文の上に{{ form.non_field_errors }}という記述がありますが、これは特定のフィールドに紐づかないエラーを表示するための記述です。今回の例ならば、「お仕事の依頼の場合、メールアドレスは必須です。」というエラーメッセージはnon_field_errorsで表示されます。{{ form.non_field_errors }}を書き忘れると幾つかのエラーが表示されなくなり、「なんで上手く送信できないのか分からない...」と悩むことになりますので、忘れないようにしてください。

せっかくなので、少し見た目もこだわりましょう。次のようなCSSを書いておきます。

/* エラーの場合に作成されるul要素は、errorlistというクラス名がついている */
.errorlist {
    margin: 0;
    padding: 0;
    list-style-type: none;
    color: red;
}

div.field {
    margin-top: 30px;
}

div.field > span.helptext {
    font-size: 14px;
    color: #999;
}

div.field > label {
    display: block;
}

div.field > input, div.field > textarea, div.field > select, div.field > button {
    width: 100%;
    padding: 6px 12px;
    box-sizing: border-box;
    border-radius: 4px;
    border: solid 1px #999;
}

これならば、割と見れますね。
フォームをかっこよくした

もしかしたら、エラーがul要素で作成されるのが気に入らないかもしれません。フォームフィールドと同様に、エラーもforで1つずつ取り出すことができます。

<form action="" method="POST">
    {% if form.non_field_errors %}
        <div class="errorlist">
            {% for error in form.non_field_errors %}
                {{ error }}<br>
            {% endfor %}
        </div>
    {% endif %}

    {% for field in form %}
        <div class="field">
            {{ field.label_tag }}
            {{ field }}
            {% if field.help_text %}
                <span class="helptext">{{ field.help_text }}</span>
            {% endif %}

            {% if field.errors %}
                <div class="errorlist">
                    {% for error in field.errors %}
                        {{ error }}<br>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
    {% endfor %}

    <div class="field">
        <button type="submit">送信</button>
    </div>

    {% csrf_token %
</form>

{% for error in form.non_field_errors %}や {% for error in field.errors %}の部分に注目です。このようにして、エラー内容を自分で取り出すことができます。

また、全てのエラーを一ヵ所にまとめたいならば、次のようにできます。これは一番上にエラーを全て集めている例です。

<form action="" method="POST">
    {% if form.errors %}
        <div class="errorlist">
            {% for errors in form.errors.values %}
                {% for error in errors %}
                    {{ error }}<br>
                {% endfor %}
            {% endfor %}
        </div>
    {% endif %}

    {% for field in form %}
        <div class="field">
            {{ field.label_tag }}
            {{ field }}
            {% if field.help_text %}
                <span class="helptext">{{ field.help_text }}</span>
            {% endif %}
        </div>
    {% endfor %}

    <div class="field">
        <button type="submit">送信</button>
    </div>

    {% csrf_token %
</form>

次のように表示されます。
エラーを全て一番上に集める

手作業で取り出す

forで取り出す方法は柔軟でしたが、凝ったレイアウトなんかだと、手作業での取り出しが必要になることもあります。

やることは単純で、forが自動でやっていた部分を自分で書くだけです。

<form action="" method="POST">
    {{ form.non_field_errors }}
    <div class="field">
        {{ form.name.label_tag }}
        {{ form.name }}
        <span class="helptext">{{ form.name.help_text }}</span>
        {{ form.name.errors }}
    </div>
    <div class="field">
        {{ form.email.label_tag }}
        {{ form.email }}
        <span class="helptext">{{ form.email.help_text }}</span>
        {{ form.email.errors }}
    </div>
    <div class="field">
        {{ form.text.label_tag }}
        {{ form.text }}
        {{ form.text.errors }}
    </div>
    <div class="field">
        {{ form.category.label_tag }}
        {{ form.category }}
        {{ form.category.errors }}
    </div>

    <div class="field">
        <button type="submit">送信</button>
    </div>

    {% csrf_token %}
</form>

問い合わせ内容やカテゴリはhelp_text属性はないので、省きました。

この記事の関連記事

コメント欄

記事にコメントする

名無し

こんにちは。ブログ大変参考になります!

2つ質問なのですが、 (1): forms.ModelFormを使用した際に、MultipleChoiceFieldのような複数選択を可能にする方法はあるのでしょうか。 CharField(choices = )とすることで、単一の選択フィールドを作ることはできそうなのですが、複数選択の場合が分かりませんでした。素直にforms.Formを使用したほうが良いのでしょうか。

(2): 生年月日のように、一つの入力欄に複数の選択項目を作成するにはどのような方法があるのでしょうか。

ご回答お願い致します。

コメントに返信する

なりと

(1) モデル側でManyToManyFieldを使うのがスムーズだと思います。

(2) Djangoに、SelectDateWidgetというのがあるのでそちらを利用してください。

名無し

本稿とはあまり関連しない内容なのですが、ご回答頂けますと幸いです。

DjangoのFormクラスのバリデーションの実行タイミングについて ご教示いただけないでしょうか。 例えば、ModelFormを使用した特定のモデルの登録(CreateView)だと 大まかな流れとして 次のように認識しているのですが問題ないでしょうか。

  • HTMLページのフォームに値を入力して「登録」などのボタンを押下する

  • CreateViewでFormクラスのis_valid()が実行され、バリデーションが実行される

  • バリデーションに問題が無ければ、CreateViewのform_valid()が実行される

  • バリデーションに問題があれば、CreateViewのform_invalid()が実行され、ValidationErrorが含まれたformをテンプレートにレンダリングする。

コメントに返信する

なりと

はい、その認識で問題ございません。

もうちょっと細かい話だと、formクラスのis_valid()が呼ばれた後は、フォームやフィールドにある様々なメソッドで検証が行われていきます。モデルフォームの場合は、モデルにある検証用のメソッドも+αで呼ばれます。バリデーションが問題なさそうなフィールドは、form.cleaned_daya['フィールド名']で値を取り出せるようにもなります。

詳しい流れは公式ドキュメントあたりも見てみてください。

バリデーションが全て問題なかった場合はビューのform_valid()メソッド内の処理に入りますが、CreateViewやUpdateViewならば作成・更新処理の後にリダイレクト処理、FormViewならば何もしないでリダイレクト処理です。

問題があればビューのform_invalid()メソッド内に入り、もう一度フォームなどをテンプレートファイルに渡して描画されます。