django-crispy-forms WebSiteの訳

django-crispy-formsを使ってみようと思ったので手始めに下記サイトを訳してみました。

django-crispy-forms.readthedocs.io


{% crispy %} tag with forms

django-crispy-formsはフォームのレンダリングのふるまいを定義した、FormHelperというクラスが組み込まれています。ヘルパーはフォームの属性やレイアウトをPythonのプログラム方式でコントロールする方法を提供します。この方法は可能な限り少ないHTML記述で済ませ、全てのロジックはフォームやビューのファイルの中で完結できます。

基本

このドキュメントの残りはヘルパーの使い方を示した次のフォームの例を使います。このフォームはいく人かのユーザーインフォメーションをまとめる役割を担ってます。

class ExampleForm(forms.Form):
    like_website = forms.TypedChoiceField(
        label = "Do you like this website?",
        choices = ((1, "Yes"), (0, "No")),
        coerce = lambda x: bool(int(x)),
        widget = forms.RadioSelect,
        initial = '1',
        required = True,
    )

    favorite_food = forms.CharField(
        label = "What is your favorite food?",
        max_length = 80,
        required = True,
    )

    favorite_color = forms.CharField(
        label = "What is your favorite color?",
        max_length = 80,
        required = True,
    )

    favorite_number = forms.IntegerField(
        label = "Favorite number",
        required = False,
    )

    notes = forms.CharField(
        label = "Additional notes or feedback",
        required = False,
    )

1つづついくつかの例の説明をしながらヘルパーの動きを見ていきましょう。まずFormHelperを組み込む必要があります。

from crispy_forms.helper import FormHelper

ヘルパーはクラスレベルの変数かインスタンスレベルの変数になります。もしこの意味がわからなければこの記事 "Be careful how yu use static variables in forms"を読むのが良いかも知れません。大まかに言うとビューの中などのコードの中でFormHelperを触らなければ静的なヘルパーを使うべきでそうでは無い場合インスタンスレベルのヘルパーを使うべきです。もしまだ二つの微妙な違いが分からなければインスタンスレベルのヘルパーを使うべきです。なぜならばサイドエフェクトに最後苦しまない為です。次のステップではフォームヘルパーの動かす方法の説明にインスタンスレベルのヘルパーを作りましょう。こちらがその方法です。

from crispy_forms.helper import FormHelper

class ExampleForm(forms.Form):
    [...]
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()

分かることはsuperを使って基底クラスを呼びコンストラクターをオーバーライドする必要があります。このヘルパーはどのようなフォームの属性も設定しないでなにも役には立ちません。基本的なFormHelperの属性の設定方法を見ましょう。

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit

class ExampleForm(forms.Form):
    [...]
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_id = 'id-exampleForm'
        self.helper.form_class = 'blueForms'
        self.helper.form_method = 'post'
        self.helper.form_action = 'submit_survey'

        self.helper.add_input(Submit('submit', 'Submit'))

レイアウトのオブジェクトであるSubmitというクラスをインポートしている事に注意してください。あとでレイアウトオブジェクトについてはみることにして今はこれは作成する”みんながそのアンケートを送ることが出来る”フォームのためのサブミットボタンを加えると言うことだけとしておきましょう。

またいくつかのヘルパーのマジックを使えます。FormHelperはメインのフォームの属性に影響を与えることが出来る属性リストを持っています。これらのフォームはDOM idとしてid-exampleFormを持っていてDOM CSSとしてblueFormsクラスがあます。情報を送るためにhttpのPOSTを使い、その送信はrevers(submit_survey)を呼ぶことで行われます。   テンプレートでのフォームをレンダリングするやり方を見てみましょう。もしexample_formというテンプレートのコンテキストがあるフォームを持っているとするとレンダリングのさせ方は

{% load crispy_forms_tags %}
{% crispy example_form example_form.helper %}

{% crispy %}タグは2つのパラメータがあることに注意してください。一つ目はフォームの変数でもう一つはそのヘルパーです。このケースではフォームに書いたFormHelperを使っていますがFormHelperインスタンスを作ってコンテキストの変数としてそれを渡すことができます。大抵の場合はヘルパーに付属しているものを使いたいでしょう。もしあなたがhelper属性を FormHelperと名付けたら次のようにするだけで良いことに注意してください。

{% crispy form %}

これがまさに出力されるhtmlです。

<form action="/submit/survey/" class="uniForm blueForms" method="post" id="id-exampleForm">
    <div style='display:none'>
        <input type="hidden" name="csrfmiddlewaretoken" value="a643fab735d5ce6377ff456e73c4b1af" />
    </div>
    <fieldset>
        <legend></legend>
        <div id="div_id_like_website" class="ctrlHolder">
            <label for="id_like_website" class="requiredField">¿Do you like this website?<span class="asteriskField">*</span></label>
            <ul>
                <li><label for="id_like_website_0"><input checked="checked" name="like_website" value="1" id="id_like_website_0" type="radio" class="radioselect" /> Yes</label></li>
                <li><label for="id_like_website_1"><input value="0" type="radio" class="radioselect" name="like_website" id="id_like_website_1" /> No</label></li>
            </ul>
        </div>
        <div id="div_id_favorite_food" class="ctrlHolder">
            <label for="id_favorite_food" class="requiredField">What is you favorite food?<span class="asteriskField">*</span></label>
            <input id="id_favorite_food" class="textinput textInput" type="text" name="favorite_food" maxlength="80" required="required" />
        </div>
        <div id="div_id_favorite_color" class="ctrlHolder">
            <label for="id_favorite_color" class="requiredField">What is you favorite color?<span class="asteriskField">*</span></label>
            <input id="id_favorite_color" class="textinput textInput" type="text" name="favorite_color" maxlength="80" required="required" />
        </div>
        <div id="div_id_favorite_number" class="ctrlHolder">
            <label for="id_favorite_number">Favorite number</label>
            <input id="id_favorite_number" type="text" name="favorite_number" class="textinput textInput" />
        </div>
        <div id="div_id_notes" class="ctrlHolder">
            <label for="id_notes">Additional notes or feedback</label>
            <input id="id_notes" type="text" name="notes" class="textinput textInput" />
        </div>
    </fieldset>
    <div class="buttonHolder">
        <input type="submit" name="submit" value="Submit" class="submit submitButton" id="submit-id-submit" />
    </div>
</form>

取得できるものは素晴らしいHTMLのレンダリングされたフォームです。具体的に言うと

  • 最初と終わりのid付きのフォームタグ、actionとmethodはヘルパーにセットされます。
<form action="/submit/survey/" class="uniForm blueForms" method="post" id="id-exampleForm">
    [...]
</form>
<div style='display:none'>
    <input type="hidden" name="csrfmiddlewaretoken" value="a643fab735d5ce6377ff456e73c4b1af" />
</div>
  • Submitボタン
<div class="buttonHolder">
    <input type="submit" name="submit" value="Submit" class="submit submitButton" id="submit-id-submit" />
</div>

ビューの中でのヘルパーの操作

ビューの中でどんなヘルパーのプロパティーが変更できるか見ていきましょう。

@login_required()
def inbox(request, template_name):
    example_form = ExampleForm()
    redirect_url = request.GET.get('next')

    # Form handling logic
    [...]

    if redirect_url is not None:
        example_form.helper.form_action = reverse('submit_survey') + '?next=' + redirectUrl

    return render_to_response(template_name, {'example_form': example_form}, context_instance=RequestContext(request))

nextを伴ったGETパラメータで呼び出されたビューの中でform_actionヘルパーを変更しています。

ヘルパーでいくつかのフォームをレンダリング

”どのように二度

タグを使わずに{% crispy %}タグを使いそれぞれのヘルパーを用いて2つやそれ以上のフォームをレンダリンするのですか?”と良く聞かれます。全てのヘルパーの中にform_tagヘルパープロパティをFalseに設定すれば簡単です。

self.helper.form_tag = False

それでフォーム周りの少しのHTMLコードを書く必要があるでしょう。

<form action="{% url 'submit_survey' %}" class="uniForm" method="post">
    {% crispy first_form %}
    {% crispy second_form %}
</form>

Helper attributes you can setの一覧を読めばそれらがなにをしているか分かります。

必須のフィールドの'*'を変える。

もし必須のフィールドを意味するアスタリスク)を使いたくなければ2つのオプションがあります。

  • アスタリスクは一つのasteriskFieldクラスセットを持つ。そのためCSSルールを使ってそれを消すことが出来る。
.asteriskField {
    display: none;
}
  • カスタムなフィールドでfield.htmlをオーバーライドする。

crispy-formsを大きな声でフェイルさせる。

デフォルトではcrispy-formsがエラーになった時は可能な限りログを取り動作を続けて静かにフェイルする。CRISPY_FAIL_SILENTLYと呼ぶセッティング変数が加えられることでこの振る舞いをコントロールできる。もしロギングする代わりに代わりに例外を上げたいのであればデバックモードで開発中にどうなっているら知らせるために次のようにセットできる。

CRISPY_FAIL_SILENTLY = not DEBUG

crispy-forms<input>のデフォルトクラスを変更する。

Djangoフィールドはデフォルトクラスを出力する。crispy-formはこれらを扱いCSSフレームワークと互換性がある他のクラスを加える。   例えばcharFieldは<input class="textinput" ...しかしuni formではtextInput(大文字のIになっている)があることがクラスに必要です。crispy-formsは<input class="textinput textInput"...の様に出力する。全ての公式なテンプレートパックは自動的に取り扱われる。しかしcrispy-formsと一緒に新しいCSSフレームワークもしくは会社のカスタムフレームワークを結合するかも知れません。そしてデフォルトの変換の変更が必要かも知れません。このためにCRISPY_CLASS_CONVERTERSというセッティングの変数を使います。これはPythonの辞書を使います。

CRISPY_CLASS_CONVERTERS = {'textinput': "textinput inputtext"}

例えばこのセッティングは<input class"textinput inputtext" ...を出力します。辞書のキーであるtextinputDjangoのデフォルトクラスでその値はこの場合ではtextinputを保持したまま置換されます。

Pythonコード内でのフォームのレンダー

たまにDjangoビューの様なPythonコード内でCrispy-formsを使ってフォームをレンダーすることは役に立つかも知れません。そのために素晴らしいrender_crispy_formヘルパーが有ります。メソッドのプロトタイプはrender_crispy_form(form, helper=None, context=None)です。これを使うことができます。サブミットできるホームをレンダーするならコンテキスト辞書を使ってヘルパーメソッドへCSRFトークンを渡すことを覚えておいてください。

AJAX検証レシピ

いろんなフォームエラーの結果を最表示するためのAJAXを通したcrispy-formの検証を望むかも知れません。一つの方法はrender_crispy_formをつかったフォームの検証やHTMLをレンダーするビューをセットアップすることです。このHTMLはクライアントのAJAXリクエストを返します。例を見てください。

サーバーサイドのコードは次の様になっています。

from django.template.context_processors import csrf
from crispy_forms.utils import render_crispy_form

@json_view
def save_example_form(request):
    form = ExampleForm(request.POST or None)
    if form.is_valid():
        # You could actually save through AJAX and return a success code here
        form.save()
        return {'success': True}


    ctx = {}
    ctx.update(csrf(request))
    form_html = render_crispy_form(form, context=ctx)
    return {'success': False, 'form_html': form_html}

デコレーターフォームdjango-jsonviewを使っています。

必要なCSRFトークンを**render_crispy_formへ与える必要があります。さもなければ動きません。

jQueryを使ったクライアントサイドのコードです。

var example_form = '#example-form';

$.ajax({
    url: "{% url 'save_example_form' %}",
    type: "POST",
    data: $(example_form).serialize(),
    success: function(data) {
        if (!(data['success'])) {
            // Here we replace the form, for the
            $(example_form).replaceWith(data['form_html']);
        }
        else {
            // Here you can show the user a success message or do whatever you need
            $(example_form).find('.success-message').show();
        }
    },
    error: function () {
        $(example_form).find('.error-message').show()
    }
});

警告:HTMLのフォームを置き換えるときはjQueryメソッドのliveもしくはonを使ってイベントを繋ぐ必要があります。

Bootstrap3 水平フォーム

f:id:mechagocha:20201214223425j:plain

Bootstrapバージョン3での水平フォームの方法はlabelやdivで囲まれたフィールドいくつかのcol-lg-xクラスをセッティングします。これはこれらのクラスのセッティングのためにたくさんの面倒なレイアウトオブジェクトのアップデートを意味します。代わりにいくつかのFormHelper属性はこれを簡単に達成するための助けを与える。ただ3つの属性をセットするだけでいいです。

helper.form_class = 'form-horizontal'
helper.label_class = 'col-lg-2'
helper.field_class = 'col-lg-8'
helper.layout = Layout(
    'email',
    'password',
    'remember_me',
    StrictButton('Sign in', css_class='btn-default'),
)

もちろん好きなように幅を設定できます。全くこの通りにする必要はありません。

Bootstrap3 インラインフォーム

f:id:mechagocha:20201214224829j:plain

Bootstrapバージョン3でのインラインフォームのやり方です。

helper.form_class = 'form-inline'
helper.field_template = 'bootstrap3/layout/inline_field.html'
helper.layout = Layout(
    'email',
    'password',
    'remember_me',
    StrictButton('Sign in', css_class='btn-default'),
)

フィールドの中に属性を設定する必要がある場合はFieldの代わりにInlineFieldを使わないといけない。

from crispy_forms.bootstrap import InlineField

helper.layout = Layout(
    InlineField('email', readonly=True),
    'password',
    [...]
)