0

Im having problems with a dynamic formset in django

I was watching this tutorial from which I took out the base code https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0

But it does not work as it should, since it does not let me add more forms

I think the problem is in javascript, but not knowing much about it I can not fix it

Image of how its the html, when i press the "+" button, nothing ocurr. If i change "extra" of formset to 5 for example, and then press the "-" button this work of i expect.

forms.py

OrdenCompraModelFormset = modelformset_factory(
    OrdenCompraProducto,
    fields=[
        'tipo_producto',
        'cantidad',
        'fecha_entrega',
        'proveedor_surgerido',
    ],
    extra=1,
    widgets={
        'fecha_entrega': forms.DateInput(attrs=
                                         {'class': 'form-control', 'type': 'date'}),
    }
)

views.py

def formv_ordencompra(request):
form=OrdenCompraForm, extra=1)
    if request.method == 'GET':
        print('0')
        formset = OrdenCompraModelFormset(queryset=OrdenCompraProducto.objects.none())
    elif request.method == 'POST':
        print('1')
        formset = OrdenCompraModelFormset(request.POST)
        if formset.is_valid():
            formset_obj = formset.save(commit=False)
            ordencompra = OrdenCompra.objects.create()

            for form_obj in formset_obj:
                form_obj.orden = ordencompra
                form_obj.save()
            messages.success(request, f'La orden fue cargada!')
            return redirect('.')
    else:
        print('2')
        formset = OrdenCompraModelFormset()
    return render(request, 'webapp/formularios/ordencompra.html', {'formset': formset})

ordencompra.html

{% extends 'webapp/base.html' %}
{% load crispy_forms_tags %}
{% load static %}
{% block content_principal %}


    <div class="container-fluid">
        <h1 class="h3 mb-2 text-gray-800">Cargar "Orden de devolucion"</h1>
        <p class="mb-4">Esta orden define quien trae y cuando un recurso que este en obra</p>
        <p class="mb-4">¿Cuando cargar?: En el momento en el que se quieran traer recursos desde obra</p>

        <hr>





        <form class="form-horizontal" method="POST" action="">
            {% csrf_token %}
            {{ formset.management_form }}
            {% for form in formset %}

                <div class="row form-row spacer">
                    <div class="input-group">


                        <div class="input-group-append">
                            <button class="btn btn-success add-form-row">+</button>
                            <button class="btn btn-danger remove-form-row">-</button>
                        </div>


                        {{form|crispy}}
                    </div>
                </div>
                <hr>

            {% endfor %}

            <div class="row spacer">
                <div class="">
                    <button type="submit" class="btn btn-block btn-primary">Create</button>
                </div>
            </div>
        </form>


    </div>


{% endblock %}


{% block content_plugins %}

    <script type='text/javascript'>
        function updateElementIndex(el, prefix, ndx) {
            var id_regex = new RegExp('(' + prefix + '-\\d+)');
            var replacement = prefix + '-' + ndx;
            if ($(el).attr("for")) $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
            if (el.id) el.id = el.id.replace(id_regex, replacement);
            if (el.name) el.name = el.name.replace(id_regex, replacement);
        }
        function cloneMore(selector, prefix) {
            var newElement = $(selector).clone(true);
            var total = $('#id_' + prefix + '-TOTAL_FORMS').val();
            newElement.find(':input:not([type=button]):not([type=submit]):not([type=reset])').each(function() {
                var name = $(this).attr('name').replace('-' + (total-1) + '-', '-' + total + '-');
                var id = 'id_' + name;
                $(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
            });
            newElement.find('label').each(function() {
                var forValue = $(this).attr('for');
                if (forValue) {
                    forValue = forValue.replace('-' + (total-1) + '-', '-' + total + '-');
                    $(this).attr({'for': forValue});
                }
            });
            total++;
            $('#id_' + prefix + '-TOTAL_FORMS').val(total);
            $(selector).after(newElement);
            var conditionRow = $('.form-row:not(:last)');
            conditionRow.find('.btn.add-form-row')
                .removeClass('btn-success').addClass('btn-danger')
                .removeClass('add-form-row').addClass('remove-form-row')
                .html('<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>');
            return false;
        }
        function deleteForm(prefix, btn) {
            var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
            if (total > 1){
                btn.closest('.form-row').remove();
                var forms = $('.form-row');
                $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
                for (var i=0, formCount=forms.length; i<formCount; i++) {
                    $(forms.get(i)).find(':input').each(function() {
                        updateElementIndex(this, prefix, i);
                    });
                }
            }
            return false;
        }
        $(document).on('click', '.add-form-row', function(e){
            e.preventDefault();
            cloneMore('.form-row:last', 'form');
            return false;
        });
        $(document).on('click', '.remove-form-row', function(e){
            e.preventDefault();
            deleteForm('form', $(this));
            return false;
        });
    </script>
{% endblock %}
Gasti
  • 1

1 Answers1

1

You may find an easier approach to use the empty form in the Django formset. This is a hidden, empty form with __prefix__ taking the place of the form number. You can clone this form and then replace __prefix__ with the new form number. It's way less code. This is how I got the add form to work.

{% load crispy_forms_tags %}

{% block content_principal %}

       {{ formset.management_form }}

       <div id="form_set">
         {% for form in formset.forms %}
              {{form.non_field_errors}}
              {{form.errors}}
              {% crispy form %}
          {% endfor %}
        </div>

        <input type="button" value="+" id="add_more"> 

        <div id="empty_form" style="display:none"> 
            {% crispy formset.empty_form %}            
        </div>

{% endblock %}

{% block content_plugins %}

  <script type="text/javascript">

    $(document).ready(function() { 

      $("#add_more").on("click", function() {         
          var form_idx = $("#id_form-TOTAL_FORMS").val();
          $newform = $("#empty_form").clone(true,true)
          $("#form_set").append($newform.html().replace(/__prefix__/g, form_idx)); 
          $("#id_form-TOTAL_FORMS").val(parseInt(form_idx)+1); 
      });

    });
</script>

{% endblock %}

For clarity, I included just one button to add a new form. We are cloning an empty form and not a specific form in the set. No need to have the add button on every form in the set. You may have a reason to do that in your code, but not for this illustration. Notice you can render the empty form using Crispy, too!

Forms in the formset are designated by a form number that is prepended to id's and classes. So, the field, cantidad, would have the id id_form-0-cantidad for the first form in the set and id_form-1-cantidad for the second, etc. The form numbering begins with 0, so a form set with 1 form will have its largest prefix = 0. All of this is to say, TOTAL_FORMS also equals the new prefix you will need to give the new form. Then, increment TOTAL_FORMS by 1 to avoid validation errors.

So, I created a clone of the empty form using .clone(true, true). The true arguments ensure event listeners are added to the elements when they are cloned. This is important if you have event handlers for the form set. Without it, the cloned elements won't respond to the events.

I then appended the .html() of the cloned $newform to the formset div and used a Regular Expression to replace all of the __prefix__'s in the id's, names, and classes with the new form number: .replace(/__prefix__/g, form_idx).

Haven't tried deleting the form, but I'd use jQuery to do it. Similar process - identify the form number, select the parent, delete it. Decrement TOTAL_FORMS by 1.

Fr George
  • 23
  • 9