1

I am trying to utilize crispy forms & bootstrap with formsets but I can't seem to figure out the CSS styling. I need to style it in the backend as I want to allow the client to dynamically add more of the same form, and when you add an empty form into the HTML it does not keep the same styling.

create.html

{% load crispy_forms_tags %}
<!--RECIPE INGREDIENTS-->
{% if formset %}
<h3>Ingredients</h3>
{{ formset.management_form|crispy }}

<div id='ingredient-form-list'>
    {% for ingredient in formset %}
        <div class='ingredient-form row'>
    
            {{ ingredient|crispy }}

        
        </div>
    {% endfor %}
</div>

forms.py

class RecipeIngredientForm(forms.ModelForm):
class Meta:
    model = RecipeIngredient
    fields = ['name', 'quantity', 'unit', 'description']
    labels = {
        'name': "Ingredient",
        "quantity:": "Ingredient Quantity",
        "unit": "Unit",
        "description:": "Ingredient Description"}
    

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.helper = FormHelper()
    self.helper.layout = Layout(
        Row(
            Column('name', css_class='form-group col-md-6 mb-0'),
            Column('quantity', css_class='form-group col-md-4 mb-0'),
            Column('unit', css_class='form-group col-md-4 mb-0'),
            css_class='form-row'
        ),
        Row(
            Column('description', css_class='form-group col-md-6 mb-0'),
            css_class='form-row'
        ),
    )

views.py

def recipe_create_view(request):
    form = RecipeForm(request.POST or None)
    RecipeIngredientFormset = formset_factory(RecipeIngredientForm)
    formset = RecipeIngredientFormset(request.POST or None)
    
    context = {
        "form": form,
        "formset": formset,
    }
    if request.method == "POST":
        #print(request.POST)
        if form.is_valid() and formset.is_valid() and instructionFormset.is_valid():
            parent = form.save(commit=False)
            parent.user = request.user
            parent.save()
            
            #recipe ingredients
            for form in formset:
                child = form.save(commit=False)
                if form.instance.name.strip() == '':
                    pass
                else:
                    child.recipe = parent
                    child.save()
            
    else:
        form = RecipeForm(request.POST or None)
        formset = RecipeIngredientFormset()
        
    return render(request, "recipes/create.html", context)

This is how this renders out, and there is an empty div that wraps the input fields of the form. and my FormHelper CSS is nowhere to be seen.

Django Formset

Nick
  • 223
  • 2
  • 11
  • What is `{{ ingredient }}` please? – Swift Nov 26 '21 at 00:37
  • @Swift - ingredient is from the for loop {% for ingredient in formset %} – Nick Nov 26 '21 at 02:01
  • Can you just crispy the whole formset or am I making that up in my head? – Swift Nov 26 '21 at 09:18
  • I've found some of the solution as per my answer below @Swift, but the {% crispy formset.empty_form %} is creating a whole new
    , which is a problem I'm trying to work around now.
    – Nick Nov 26 '21 at 09:31
  • @Swift - have found a answer, not the prettiest and I imagine there is a better way to achieve this result but it works! – Nick Nov 26 '21 at 09:47

2 Answers2

1

You need to load crispy tags and also crispy the form fiel in the template.

When we render the form now using:

{% load crispy_forms_tags %}
{% crispy example_form %}

Or in your case

{{ ingredient|crispy }}
Swift
  • 1,663
  • 1
  • 10
  • 21
  • Sorry Swift I accidentally omitted this while I was writing the question. I already was using this in local files to no success. – Nick Nov 26 '21 at 02:02
1

Edit #4

The initial issue was due to how I was rendering the form in the create.html template. As seen in the documentation, a better way is to load it is using {% crispy ..... %}, as seen below for both the for loop and empty form. However, rendering the extra form as a {% crispy formset.empty_form %} was causing issues because it was generating a whole new <form>, so for the extra form I manage to render this manually with CSS in the template. This is probably by no means the best way to do this, but alas, it works.

See below for updated files:

forms.py

class RecipeIngredientForm(forms.ModelForm):
class Meta:
    model = RecipeIngredient
    fields = ['name', 'quantity', 'unit', 'description']
    labels = {
        'name': "Ingredient",
        "quantity:": "Ingredient Quantity",
        "unit": "Unit",
        "description:": "Ingredient Description"}
    

def __init__(self, *args, **kwargs):
    super(RecipeIngredientForm, self).__init__(*args, **kwargs)

    self.helper = FormHelper()
    self.helper.form_id = 'id-entryform'
    self.helper.form_class = 'form-inline'
    self.helper.layout = Layout(
        Div(
            Div(Field("name", placeholder="Chickpeas"), css_class='col-6 col-lg-4'),
            Div(Field("quantity", placeholder="2 x 400"), css_class='col-6 col-md-4'),
            Div(Field("unit", placeholder="grams"), css_class='col-5 col-md-4'),
            Div(Field("description", placeholder="No added salt tins"), css_class='col-12'),
        
        css_class="row",
       ),
       
    )

create.html

<!--RECIPE INGREDIENTS-->
                {% if formset %}
                    <h3>Ingredients</h3>
                    {{ formset.management_form|crispy }}
                    {% load crispy_forms_tags %}
                    <div id='ingredient-form-list'>
                        {% for ingredient in formset %}
                       
                                <div class='ingredient-form'>
                                    
                                    {% crispy ingredient %}
                                    
                                </div>
                        {% endfor %}
                    </div>

                    <div id='empty-form' class='hidden'>
                        <div class="row">
                            <div class="col-6">{{ formset.empty_form.name|as_crispy_field }}</div>
                            <div class="col-6">{{ formset.empty_form.quantity|as_crispy_field }}</div>
                            <div class="col-6">{{ formset.empty_form.unit|as_crispy_field }}</div>
                            <div class="col-6">{{ formset.empty_form.description|as_crispy_field }}</div>
                        </div>
                    </div>
                    <button class="btn btn-success" id='add-more' type='button'>Add more ingredients</button>
                {% endif %}
<script>
//ingredients add form
const addMoreBtn = document.getElementById('add-more')
const totalNewForms = document.getElementById('id_ingredient-TOTAL_FORMS')

addMoreBtn.addEventListener('click', add_new_form)
function add_new_form(event) {
    if (event) {
        event.preventDefault()
    }
    const currentIngredientForms = document.getElementsByClassName('ingredient-form')
    const currentFormCount = currentIngredientForms.length // + 1
    const formCopyTarget = document.getElementById('ingredient-form-list')
    const copyEmptyFormEl = document.getElementById('empty-form').cloneNode(true)
    copyEmptyFormEl.setAttribute('class', 'ingredient-form')
    copyEmptyFormEl.setAttribute('id', `ingredient-${currentFormCount}`)
    const regex = new RegExp('__prefix__', 'g')
    copyEmptyFormEl.innerHTML = copyEmptyFormEl.innerHTML.replace(regex, currentFormCount)
    totalNewForms.setAttribute('value', currentFormCount + 1)
    // now add new empty form element to our html form
    formCopyTarget.append(copyEmptyFormEl)
}
Nick
  • 223
  • 2
  • 11
  • 1
    Loads the tags at the beginning of your template file. Just because otherwise you load that tag each time and its not efficient – Swift Nov 26 '21 at 17:29
  • 1
    But other than that, it doesn't seem to look wrong at all. Looks perfect. And if it works? Well.... awesome! – Swift Nov 26 '21 at 17:31