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

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月10日15:53に更新(約1日前)
2018年12月6日15:43に作成(約5日前)

フォームオブジェクトを作成したら、それをテンプレートへ渡します。{{ form }}{% for field in form %}など様々な表示方法がありますが、今回はそれを紹介していきます。

ウィジェットの変更やclass属性変更については、Django、モデルフォームのウィジェットを変更する幾つかの方法をご覧ください。

Bootstrap4を使う場合の例だったり、柔軟な表示方法についても紹介していきます。

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

from django import forms

RATING_CHOICES = [
    (1, '★1'),
    (2, '★2'),
    (3, '★3'),
    (4, '★4'),
    (5, '★5'),
]

GOOD_POINT_CHOICES = [
    ('1', '丁寧な対応'),
    ('2', '良い人そう'),
    ('3', 'お値段以上'),
    ('4', '高品質'),
    ('5', '素早い対応'),
]


class ReviewForm(forms.Form):
    """レビュー用フォーム"""
    name = forms.CharField(
        label='お名前', max_length=50,
        required=False, help_text='※匿名が良い場合は空欄にしてください'
    )
    rating = forms.ChoiceField(label='評価', choices=RATING_CHOICES, initial=3)
    good_point = forms.MultipleChoiceField(
        label='良かったとこ', choices=GOOD_POINT_CHOICES,
        required=False, help_text='CTRLキーで複数選択できます。'
    )

ちょっとした評価用のフォームです。実際には、モデルを定義しそのモデルフォームを使ったほうが集計処理などもしやすいです。

今回はモデルでの保存処理ではなくフォームの表示方法がメインなのと、通常のフォームのサンプルもかねて、forms.Formとして作っています。

手作業で取り出す

ちょっと面倒ですが、一番確実な方法です。

{{ form.non_field_errors }}

{{ form.name.label_tag }}
{{ form.name }} 
{{ form.name.help_text }}
{{ form.name.errors }}
<hr>

{{ form.rating.label_tag }}
{{ form.rating }}
{{ form.rating.errors }}
<hr>

{{ form.good_point.label_tag }}
{{ form.good_point }}
{{ form.good_point.help_text }}
{{ form.good_point.errors }}
<hr>

フォームエラーがなければ、以下のようなHTMLになります。

<label for="id_name">お名前:</label>
<input type="text" name="name" maxlength="50" id="id_name">
※匿名が良い場合は空欄にしてください
<hr>

<label for="id_rating">評価:</label>
<select name="rating" id="id_rating">
    <option value="1">★1</option>
    <option value="2">★2</option>
    <option value="3" selected>★3</option>
    <option value="4">★4</option>
    <option value="5">★5</option>
</select>
<hr>

<label for="id_good_point">良かったとこ:</label>
<select name="good_point" id="id_good_point" multiple>
    <option value="1">丁寧な対応</option>
    <option value="2">良い人そう</option>
    <option value="3">お値段以上</option>
    <option value="4">高品質</option>
    <option value="5">素早い対応</option>
</select>
※CTRLキーで複数選択できます。
<hr>

エラーがあった場合は、後で紹介しますが<ul><li>エラー内容1</li></ul>といった要素が挿入されます。

見た目は次のような感じです。

form.フィールド名

手作業でフォームのフィールドを取り出す場合は、{{ form.フィールド名 }}とします。これで<input><select>といった要素を取り出すことができます。 画像でいうと、次の部分です。

例えば{{ form.name }}ならば、<input type="text" name="name" maxlength="50" id="id_name">として生成されます。name属性はモデルやフォームのフィールド名、id属性はその先頭にid_をつけたものです。

form.フィールド名.label_tag

これを使うかは任意ですが、使っておいたほうがユーザーに優しい入力欄になります。次の丸で囲んだ部分です。

HTML上では、次のようなlabel要素になります。

<label for="id_name">お名前:</label>

これを使うと何が良いかというと、「お名前」といった部分をクリックすると入力欄にフォーカスされ、すぐに入力できるようになります。入力欄と項目名を紐づけてくれます。

スマホのように、各入力欄が小さくてタップしずらい場合等は、ラベル部分をクリックするほうが楽です。

label要素をもう少しカスタマイズしたい場合もあります。例えばclassに指定をしたり、label要素の中に入力欄がネストされるような場合もあるかもしれません。

そういった場合のために、項目名だけを取り出すこともできます。

<label for="{{ form.name.id_for_label }}">{{ form.name.label }}:</label>

ちょっと長いですが、これで<label for="id_name">お名前:</label>と同じ内容になります。後は好きにカスタマイズしましょう。

form.フィールド名.help_text

モデルやフォームフィールドでのhelp_text属性の部分です。画像でいうと、次の丸で囲んだ部分です。

help_textがない場合もあるので、使うかどうかは自由です。とはいえ、テンプレートでは属性が見つからなかった等のエラーは無視されて何も表示されないので、とりあえずつけておいても大丈夫でしょう。

help_textの名の通り、その欄のヘルプ的なテキストです。入力欄に関する補足や説明に使います。{{ form.name.help_text }}のように、 form.フィールド名.help_textで取り出すことができます。

たまにあるのですが、help_textにhtmlを含めたいことがあります。例えばヘルプテキスト中の空欄の部分を太字にしたいのかもしれません。

help_text='※匿名が良い場合は<b>空欄</b>にしてください'

テンプレートへ渡される文字列にhtmlが含まれていればそれはエスケープ処理されますので、htmlとして表示させたいならば少し追加の処理が必要です。

from django.utils.safestring import mark_safe
...
...
help_text=mark_safe('※匿名が良い場合は<b>空欄</b>にしてください')

mark_safeとするか、テンプレートにてsafeフィルタを使いましょう。

{{ form.name.help_text | safe }}

form.フィールド名.errors

そのフィールドに紐づくエラー内容です。基本的には表示させることになるでしょう。フォームのis_valid()メソッドが呼ばれた際に設定されます。空欄ダメだよと言ってるのに空欄で送信されたとか、EmailFieldなのに入力されたのはメールアドレスっぽくないとか、そういうのを表示してくれます。

試しに表示させてみました。

このエラーはどうやって作ったかというと、ReviewFormに以下のメソッドを追加することで作れます。今回のフォームの表示とは関係ないのであまり触れないようにしますが、このように入力欄のチェック処理を自分で作ることも可能です。

    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

話を戻して、form.フィールド名.errorsとした場合は上の画像のような感じで表示されます。htmlとしては、次のようなものを生成してくれます。

<ul class="errorlist">
    <li>名前に暴言を含めないでください。</li>
</ul>

<ul class="errorlist">という要素を作ってくれます。よくやるのは、●部分を消して赤く表示するとかですね。以下のようなCSS設定で簡単にできます。

ul.errorlist {
    list-style-type: none;
    padding: 0;
    color: red;
}

次のように表示されます。

errorsという名前や上のリスト要素で察しているかもしれませんが、エラー内容が複数ある場合もあります。

エラー1つ1つを取り出して自分なりに表示したいかもしれません。幸いにもforで取り出すことができます。これはul要素は生成しないので、好きにカスタマイズできます。`

{% for error in form.name.errors %}
    {{ error }}<br>
{% endfor %}
<hr>

form.non_field_errors

以外に忘れがちですが、この{{ form.non_field_errors }}の存在を忘れないようにしましょう。これは特定のフィールドには紐づかなかったり、複数のフィールドに跨るエラー内容が詰まっています。

例えばですが、「良かったとこ」が2つ以上あるのに「★1」をつけるのがシステムにそぐわないとしましょう。この場合、2つのフィールドに関連しているのでnon_field_errorに格納されるのが素直な実装に思います。

他によくあるのは、パスワードと確認用パスワードの欄がありそれらが一致しないとか、IDとパスワードのどちらかが間違っているよ、と教える場合ですね(IDが間違っています、パスワードが間違っています、という内容は情報を教えすぎなので良くないです)。Djangoが提供しているログイン用フォームやユーザー作成用フォームでは、実際にそのようにしています。

次のようにclearn()メソッドを定義してnon_field_errorsにエラーを格納しておきます。

    def clean(self):
        good_point = self.cleaned_data.get('good_point')
        rating = self.cleaned_data.get('rating')
        if rating == '1' and len(good_point) >= 2:
            self.add_error(None, '良いところが2つ以上あれば、★1はつけれません。')
        return self.cleaned_data

次のように表示されます。

以下のようなul要素を出力します。classにnonfieldとありますので、フィールドのエラーリストと区別したい場合に活用してください。

<ul class="errorlist nonfield">
    <li>良いところが2つ以上あれば、★1はつけれません。</li>
</ul>

non_field_errrosもforで取り出すことが可能です。

form.errorsでエラーをまとめる

form.フィールド名.errorsとform.non_field_errorsは、{{ form.errors }}とすることで一緒くたにすることができます。まずhtmlを修正しましょう。

{{ form.errors }}

{{ form.name.label_tag }}
{{ form.name }}
{{ form.name.help_text }}
<hr>

{{ form.rating.label_tag }}
{{ form.rating }}
<hr>


{{ form.good_point.label_tag }}
{{ form.good_point }}
{{ form.good_point.help_text }}
<hr>

見た目は次のような感じです。 ちゃんとまとまっていますが、nameや__all__などの部分が気になりますね。

エラー表示部分を少し変えましょう。forで取り出せるのですが、少々複雑です。

{% for errors in form.errors.values %}
    {% for error in errors %}
        {{ error }}<br>
    {% endfor %}
{% endfor %}

見た目がちょっと悪くて紛らわしいのですが、ちゃんと全てのエラーのテキストが表示できました。後は、かっこよくカスタマイズしましょう!

一括で取り出す

一行でフォームを出力できるので、非常に手軽です。3種類あります。

form or form.as_table

<table>
    {{ form }}
</table>

table要素で囲むことを忘れないようにしましょう

{{ form }}、又は{{ form.as_table }}とすると、次のように出力されます。

<tr>
    <th><label for="id_name">お名前:</label></th>
    <td><input type="text" name="name" maxlength="50" id="id_name"><br><span class="helptext">※匿名が良い場合は空欄にしてください</span>
    </td>
</tr>

<tr>
    <th><label for="id_rating">評価:</label></th>
    <td><select name="rating" id="id_rating">
        <option value="1">★1</option>
        <option value="2">★2</option>
        <option value="3" selected>★3</option>
        <option value="4">★4</option>
        <option value="5">★5</option>
    </select></td>
</tr>

<tr>
    <th><label for="id_good_point">良かったとこ:</label></th>
    <td><select name="good_point" id="id_good_point" multiple>
        <option value="1">丁寧な対応</option>
        <option value="2">良い人そう</option>
        <option value="3">お値段以上</option>
        <option value="4">高品質</option>
        <option value="5">素早い対応</option>
    </select><br><span class="helptext">※CTRLキーで複数選択できます。</span></td>
</tr>

なので、{{ form }}table要素で囲めばテーブルレイアウトで表示できます。 tableのclassだとかを好きに指定できるわけです。

フォームにエラーがあると次のようになります。エラー関係も全て表示されますね。

form.as_ul

こちらはli要素でそれぞれの欄が生成されます。

<ul>
    {{ form.as_ul }}
</ul>

{{ form.as_ul }}は次のようなHTMLとなります。

<li>
    <label for="id_name">お名前:</label>
    <input type="text" name="name" maxlength="50" id="id_name">
    <span class="helptext">※匿名が良い場合は空欄にしてください</span>
</li>

<li>
    <label for="id_rating">評価:</label>
    <select name="rating" id="id_rating">
        <option value="1">★1</option>
        <option value="2">★2</option>
        <option value="3" selected>★3</option>
        <option value="4">★4</option>
        <option value="5">★5</option>
    </select>
</li>

<li>
    <label for="id_good_point">良かったとこ:</label>
    <select name="good_point" id="id_good_point" multiple>
        <option value="1">丁寧な対応</option>
        <option value="2">良い人そう</option>
        <option value="3">お値段以上</option>
        <option value="4">高品質</option>
        <option value="5">素早い対応</option>
    </select>
    <span class="helptext">※CTRLキーで複数選択できます。</span>
</li>

以下のような見た目になります。

エラーがある場合です。 今回の例だと 見た目的にわかりづらいですが、名前欄のエラーは名前部分の<li>の中にあります。全てのエラーを最初に表示している訳ではありません。

form.as_p

これはp要素ですね。

{{ form.as_p }}

次のようなHTMLになります。

<p>
    <label for="id_name">お名前:</label>
    <input type="text" name="name" maxlength="50" id="id_name">
    <span class="helptext">※匿名が良い場合は空欄にしてください</span>
</p>

<p>
    <label for="id_rating">評価:</label>
    <select name="rating" id="id_rating">
        <option value="1">★1</option>
        <option value="2">★2</option>
        <option value="3" selected>★3</option>
        <option value="4">★4</option>
        <option value="5">★5</option>
    </select>
</p>

<p>
    <label for="id_good_point">良かったとこ:</label>
    <select name="good_point" id="id_good_point" multiple>
        <option value="1">丁寧な対応</option>
        <option value="2">良い人そう</option>
        <option value="3">お値段以上</option>
        <option value="4">高品質</option>
        <option value="5">素早い対応</option>
    </select>
    <span class="helptext">※CTRLキーで複数選択できます。</span>
</p>

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

エラーがある場合です。

forループで取り出す

それなりに記述も短く済み、そこそこ柔軟なのがこの方法です。

{{ form.non_field_errors }}

{% for field in form %}
    {{ field.label_tag }}
    {{ field }}
    {{ field.help_text }}
    {{ field.errors }}
    <hr>
{% endfor %}

手作業で取り出す場合と同じです。違うのは、{{ form.name }} のように書いていた部分がforループ内の{{ field }}として使える点です。

Bootstrap4の例

Bootstrap4の場合のフォーム例も紹介します。

テキストボックス、セレクト、テキストエリア

type="text"なinput要素とselect要素、あとはtextareaも簡単に扱えます。class属性にform-controlをつけるだけで、テンプレートでは素直にフィールドを取り出せます。{{ form.as_p }}なんかで表示しても割と良い感じです。

今回の例は単純なので、フォームの__init__で一括してclass属性を設定します。

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

{{ form.as_p }}での表示です。悪くないですね。

<table class="table">{{ form.as_table }}</table>も良い感じです。

forで取り出すのも簡単です。

{{ form.non_field_errors }}

{% for field in form %}
    <div class="form-group">
        {{ field.label_tag }}
        {{ field }}
        {{ field.help_text }}
        {{ field.errors }}
    </div>
{% endfor %}

forで、更にグリッドなフォームにしてみます。labelのclass属性をカスタマイズしなきゃなので、少し難しいlabelの書き方をしていることに注意です。

{{ form.non_field_errors }}

{% for field in form %}
    <div class="form-group row">
        <label for="{{ field.id_for_label }}" class="col-sm-2 col-form-label">{{ field.label }}</label>
        <div class="col-sm-10">
            {{ field }}
            {{ field.help_text }}
            {{ field.errors }}
        </div>
    </div>
{% endfor %}

これは良いですね。

チェックボックス、ラジオ

チェックボックスやラジオに関してはテンプレートでの書き方も少し変わります。まず、デフォルトのselectからinput type="checkbox"等に変更してみます。

forms.pyを修正します。

class ReviewForm(forms.Form):
    """レビュー用フォーム"""
    name = forms.CharField(
        label='お名前', max_length=50,
        required=False, help_text='※匿名が良い場合は空欄にしてください',
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    rating = forms.ChoiceField(
        label='評価', choices=RATING_CHOICES, initial=3,
        widget=forms.RadioSelect
    )
    good_point = forms.MultipleChoiceField(
        label='良かったとこ', choices=GOOD_POINT_CHOICES,
        required=False, help_text='※CTRLキーで複数選択できます。',
        widget=forms.CheckboxSelectMultiple
    )

ウィジェットの変更やclass属性の設定については、Django、モデルフォームのウィジェットを変更する幾つかの方法をご覧ください。

Bootstra4フォームの詳しい機能については、公式ドキュメントをご覧ください。

ratingとgood_pointにclass属性(form-check-input)の指定がないのは意図的です。今回は不要です。

まず押さえておきたいのは、ラジオボタンに変更した場合、{{ form.rating }}のようにすると以下のように出力されることです。

<ul id="id_rating" class="form-check-input">
    <li>
        <label for="id_rating_0">
            <input type="radio" name="rating" value="1" class="form-check-input" required id="id_rating_0">
            ★1
        </label>
    </li>

    <li>
        <label for="id_rating_1">
            <input type="radio" name="rating" value="2" class="form-check-input" required id="id_rating_1">
            ★2
        </label>
    </li>

    <li>
        <label for="id_rating_2">
            <input type="radio" name="rating" value="3" class="form-check-input" required id="id_rating_2" checked>
            ★3
        </label>
    </li>

    <li>
        <label for="id_rating_3">
            <input type="radio" name="rating" value="4" class="form-check-input" required id="id_rating_3">
            ★4
        </label>
    </li>

    <li>
        <label for="id_rating_4">
            <input type="radio" name="rating" value="5" class="form-check-input" required id="id_rating_4">
            ★5
        </label>
    </li>

</ul>

ul li要素が作られます。これはBootstrap4に使えません。

こういった場合に有効なのが、フィールドの各選択肢部分を取り出す書き方です。

{% for value, text in form.rating.field.choices %}
    <div class="form-check">
        <input class="form-check-input" type="radio" name="rating"
               id="{{ form.rating.auto_id }}_{{ forloop.counter0 }}" value="{{ value }}">
        <label class="form-check-label"
               for="{{ form.rating.auto_id }}_{{ forloop.counter0 }}">{{ text }}</label>
    </div>
{% endfor %}

次のようなHTMLになります。こういうHTMLが作りたかったはずです。

<div class="form-check">
    <input class="form-check-input" type="radio" name="rating"
           id="id_rating_0" value="1">
    <label class="form-check-label"
           for="id_rating_0">★1</label>
</div>

<div class="form-check">
    <input class="form-check-input" type="radio" name="rating"
           id="id_rating_1" value="2">
    <label class="form-check-label"
           for="id_rating_1">★2</label>
</div>

<div class="form-check">
    <input class="form-check-input" type="radio" name="rating"
           id="id_rating_2" value="3">
    <label class="form-check-label"
           for="id_rating_2">★3</label>
</div>

<div class="form-check">
    <input class="form-check-input" type="radio" name="rating"
           id="id_rating_3" value="4">
    <label class="form-check-label"
           for="id_rating_3">★4</label>
</div>

<div class="form-check">
    <input class="form-check-input" type="radio" name="rating"
           id="id_rating_4" value="5">
    <label class="form-check-label"
           for="id_rating_4">★5</label>
</div>

テンプレートの全体は次のようになります。レーティングは縦に項目を並べていますが、良いところは横に項目を並べてみました。

{{ form.non_field_errors }}

<div class="form-group">
    <label for="{{ form.name.id_for_label }}">{{ form.name.label }}:</label>
    {{ form.name }}
    {{ form.name.help_text }}
    {{ form.name.errors }}
</div>

<div class="form-group">
    <label>{{ form.rating.label }}:</label>
    {% for value, text in form.rating.field.choices %}
        <div class="form-check">
            <input class="form-check-input" type="radio" name="rating"
                   id="{{ form.rating.auto_id }}_{{ forloop.counter0 }}" value="{{ value }}"
                   {% if value == form.rating.value %}checked{% endif %}>
            <label class="form-check-label"
                   for="{{ form.rating.auto_id }}_{{ forloop.counter0 }}">{{ text }}</label>
        </div>
    {% endfor %}
    {{ form.rating.help_text }}
    {{ form.rating.errors }}
</div>

<div class="form-group">
    <label>{{ form.good_point.label }}:</label>
    {% for value, text in form.good_point.field.choices %}
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="checkbox" name="good_point"
                   id="{{ form.good_point.auto_id }}_{{ forloop.counter0 }}" value="{{ value }}"
                   {% if value in form.good_point.value %}checked{% endif %}>
            <label class="form-check-label"
                   for="{{ form.good_point.auto_id }}_{{ forloop.counter0 }}">{{ text }}</label>
        </div>
    {% endfor %}
    {{ form.good_point.help_text }}
    {{ form.good_point.errors }}
</div>

{% if value == form.rating.value %}checked{% endif %}{% if value in form.good_point.value %}checked{% endif %}の部分は、更新用のフォームやエラーで再表示された際にチェック済みにするための仕掛けです。

ちゃんと作れています。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする