Nick
Nick

Reputation: 8309

Django Crispy Forms, Twitter Bootstrap, and Formsets with form-inline

I'm experimenting with django-crispy-forms and Twitter Bootstrap, which is what django-crispy-forms uses as its default template pack. I'm using Django 1.4.5, django-crispy-forms 1.2.3, and Twitter Bootstrap 2.3.2.

Relevant code

urls.py

from django.conf.urls import patterns, url
from core import views

urlpatterns = patterns('',
    url(r'^$', views.Survey.as_view(), name='survey'),
)

views.py

from django.views.generic.edit import FormView
from .forms import SurveyFormset


class Survey(FormView):
    form_class = SurveyFormset
    template_name = 'survey.html'

forms.py

from django import forms
from django.forms.formsets import formset_factory
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit


class SurveyForm(forms.Form):
    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_action = '.'
        self.helper.form_class = 'form-inline'

        self.helper.add_input(Submit('submit', 'Submit'))
        super(SurveyForm, self).__init__(*args, **kwargs)

    name = forms.CharField(
        widget=forms.TextInput(attrs={'placeholder': 'Name'}),
        label='',
        max_length=50
    )

    favorite_food = forms.CharField(
        widget=forms.TextInput(attrs={'placeholder': 'Favorite food'}),
        label='',
        max_length=50
    )

    favorite_game = forms.CharField(
        widget=forms.TextInput(attrs={'placeholder': 'Favorite game'}),
        label='',
        max_length=50
    )

SurveyFormset = formset_factory(SurveyForm, extra=2)

survey.html

{% load crispy_forms_tags %}

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Survey</title>
        <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
    </head>
    <body>
        {% crispy form form.form.helper %}
    </body>
</html>

Actual results

The form fields are all stacked even though I set the form's class to form-inline. This also makes it so that you can't tell that there are two distinct forms; it just looks like one long form with duplicate fields.

Here is the HTML being produced:

<form action="." class="form-inline" method="post">
    <div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='foo' /></div>

    <div>
        <input type="hidden" name="form-TOTAL_FORMS" value="2" id="id_form-TOTAL_FORMS" />
        <input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" />
        <input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS" />
    </div>

    <div id="div_id_form-0-name" class="control-group">
        <div class="controls">
            <input name="form-0-name" maxlength="50" placeholder="Name" type="text" class="textinput textInput" id="id_form-0-name" />
        </div>
    </div>

    <div id="div_id_form-0-favorite_food" class="control-group">
        <div class="controls">
            <input name="form-0-favorite_food" maxlength="50" placeholder="Favorite food" type="text" class="textinput textInput" id="id_form-0-favorite_food" />
        </div>
    </div>

    <div id="div_id_form-0-favorite_game" class="control-group">
        <div class="controls">
            <input name="form-0-favorite_game" maxlength="50" placeholder="Favorite game" type="text" class="textinput textInput" id="id_form-0-favorite_game" />
        </div>
    </div>

    <div id="div_id_form-1-name" class="control-group">
        <div class="controls">
            <input name="form-1-name" maxlength="50" placeholder="Name" type="text" class="textinput textInput" id="id_form-1-name" />
        </div>
    </div>

    <div id="div_id_form-1-favorite_food" class="control-group">
        <div class="controls">
            <input name="form-1-favorite_food" maxlength="50" placeholder="Favorite food" type="text" class="textinput textInput" id="id_form-1-favorite_food" />
        </div>
    </div>

    <div id="div_id_form-1-favorite_game" class="control-group">
        <div class="controls">
            <input name="form-1-favorite_game" maxlength="50" placeholder="Favorite game" type="text" class="textinput textInput" id="id_form-1-favorite_game" />
        </div>
    </div>

    <div class="form-actions">
        <input type="submit" name="submit" value="Submit" class="btn btn-primary" id="submit-id-submit" />
    </div>
</form>

Expected results

Since Twitter Bootstrap is what django-crispy-forms uses as its default template pack, I would expect it to handle this nicely by default. The fields should side by side, not stacked, and it should be obvious that there are two distinct forms.

I would expect the HTML to look something like this:

<form action="." class="form-inline" method="post">
    <div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='foo' /></div>

    <div>
        <input type="hidden" name="form-TOTAL_FORMS" value="2" id="id_form-TOTAL_FORMS" />
        <input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" />
        <input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS" />
    </div>

    <div id="id_form-0" class="control-group">
        <input type="text" id="id_form-0-name" class="textinput textInput" placeholder="Name" maxlength="50" name="form-0-name">
        <input type="text" id="id_form-0-favorite_food" class="textinput textInput" placeholder="Favorite food" maxlength="50" name="form-0-favorite_food">
        <input type="text" id="id_form-0-favorite_game" class="textinput textInput" placeholder="Favorite game" maxlength="50" name="form-0-favorite_game">
    </div>

    <div id="id_form-1" class="control-group">
        <input type="text" id="id_form-1-name" class="textinput textInput" placeholder="Name" maxlength="50" name="form-1-name">
        <input type="text" id="id_form-1-favorite_food" class="textinput textInput" placeholder="Favorite food" maxlength="50" name="form-1-favorite_food">
        <input type="text" id="id_form-1-favorite_game" class="textinput textInput" placeholder="Favorite game" maxlength="50" name="form-1-favorite_game">
    </div>

    <div class="form-actions">
        <input type="submit" name="submit" value="Submit" class="btn btn-primary" id="submit-id-submit" />
    </div>
</form>

Am I doing something wrong? What should I do to get the expected results?

Upvotes: 2

Views: 5436

Answers (1)

Michael B
Michael B

Reputation: 5388

I was able to replicate this issue as well. While I am not 100% certain as to the behavior, I was able to find the following in the django-crispy-forms documentation:

    #forms.py
    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_action = '.'
        self.helper.form_class = 'form-inline'
        self.helper.template = 'bootstrap/table_inline_formset.html'

        self.helper.add_input(Submit('submit', 'Submit'))
        super(SurveyForm, self).__init__(*args, **kwargs)

    # views.py
    from django.views.generic.edit import FormView
    from .forms import SurveyFormset, SurveyForm


    class Survey(FormView):
        form_class = SurveyForm
        template_name = 'survey.html'

        def get(self, request, *args, **kwargs):
            self.object = None
            form_class = self.get_form_class()
            form = self.get_form(form_class)
            formset = SurveyFormset()
            return self.render_to_response(
                self.get_context_data(
                    form=form,
                    formset=formset,
                )
            )

        def post(self, request, *args, **kwargs):
            """
            Handles POST requests, instantiating a form instance and its inline
            formsets with the passed POST variables and then checking them for
            validity.
            """
            self.object = None
            form_class = self.get_form_class()
            form = self.get_form(form_class)
            formset = SurveyFormset(self.request.POST)
            if (form.is_valid() and formset.is_valid()):
                return self.form_valid(form, formset)
            else:
                return self.form_invalid(form, formset)

        def form_valid(self, form, formset):
            """
            Called if all forms are valid. Creates a Recipe instance along with
            associated Ingredients and Instructions and then redirects to a
            success page.
            """
            self.object = form.save()
            formset.instance = self.object
            formset.save()
            return HttpResponseRedirect(self.get_success_url())

        def form_invalid(self, form, formset):
            """
            Called if a form is invalid. Re-renders the context data with the
            data-filled forms and errors.
            """
            return self.render_to_response(
                self.get_context_data(
                    form=form,
                    formset=formset,
                )
            )

    # survey.html
    {% load crispy_forms_tags %}

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <title>Survey</title>
            <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
        </head>
        <body>
            <div class="container">
                <div class="row">
                    <div class="span12">
                        {% crispy formset formset.form.helper 'bootstrap' %}
                    </div>
                </div>
            </div>
        </body>
    </html>

While I am curious of a solution using the divs as the django-crispy-forms documentation states it should work, I can confirm this solution works on my machine. Part of the code above is credited to Kevin Dias.

Upvotes: 2

Related Questions