0

I have a Django formset within a form that is build using django-crispy-forms and bootstrap 4.0.0-alpha.6. It looks like so:

{% block content %}
    <div>
        <h1 class="text-center">Create New Activity</h1>
        <div class="row">
            <div class="col"></div>
            <div class="col-md-8 col-lg-8">
                <form role="form" method="post">
                    {% csrf_token %}
                    {{ form|crispy }}
                    {{ activitykeycharacteristics_formset|crispy }}
                    <hr>
                    <button class="primaryAction btn btn-primary pull-right ml-1" type="submit">{% trans "Submit" %}</button>
                    <a class="btn btn-secondary pull-right" href="{{ request.META.HTTP_REFERER }}" role="button">{% trans "Cancel" %}</a>
                </form>
            </div>
            <div class="col"></div>
        </div>
    </div>
{% endblock content %}

What I have been trying to do is include add and delete buttons so that I can add or delete forms from the formset. At the moment it renders with one form in the formset and so only an add button should be seen, but once there is more than one a delete button should also be seen.

I'm pretty sure the best way to do this is to use jQuery to add and remove the forms but I haven't been able to get this to work properly. I thought that I had the add button working using Dave's answer in this SO question. But I couldn't get it to correctly update the indices of the inputs. I couldn't get the accepted answer in that question to work, and I'm not sure why.

The form in the formset is composed of two django-autocomplete-light dropdowns.

If anyone could help me with this I would really appreciate it.

Thanks for your time.

--UPDATE--

Here is the js code with an updated template:

js file:

function updateElementIndex(el, prefix, ndx) {
    let id_regex = new RegExp('(' + prefix + '-\\d+)');
    let 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.key_characteristic) el.key_characteristic = el.key_characteristic.replace(id_regex, replacement);
    if (el.data_type) el.data_type = el.data_type.replace(id_regex, replacement);
}

function cloneMore(selector, prefix) {
    let newElement = $(selector).clone(true);
    let total = $('#id_' + prefix + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        // Not sure how to adapt this to work with two inputs
        let id;
        let key_characteristic = $(this).attr('key_characteristic');
        let data_type = $(this).attr('data_type');
        if (key_characteristic) {
            key_characteristic.replace('-' + (total-1) + '-', '-' + total + '-');
            id = 'id_' + key_characteristic;
        } else if (data_type) {
            data_type.replace('-' + (total-1) + '-', '-' + total + '-');
            id = 'id_' + data_type;
        }
        $(this).attr({'key_characteristic': key_characteristic, 'data_type': data_type, 'id': id}).val('').removeAttr('checked');
    });
    total++;
    $('#id_' + prefix + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
}

function deleteForm(prefix, btn) {
    let total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
    if (total > 1){
        btn.closest('#formset').remove();
        let forms = $('#formset');
        $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
        for (let i=0, formCount=forms.length; i<formCount; i++) {
            $(forms.get(i)).find(':input').each(function() {
                updateElementIndex(this, prefix, i);
            });
        }
    }
}

$('#add-form').on('click', function(e) {
    e.preventDefault();
    cloneMore('#formset:last', 'form');
});

$('#remove-form').on('click', function(e) {
    e.preventDefault();
    deleteForm('form', $(this));
});

Updated Template:

{% block content %}
    <div>
        <h1 class="text-center">Create New Activity</h1>
        <div class="row">
            <div class="col"></div>
            <div class="col-md-8 col-lg-8">
                <form role="form" method="post">
                    {% csrf_token %}
                    {{ form|crispy }}
                    <div id="formset">
                        {{ activitykeycharacteristics_formset.management_form }}
                        {% for form in activitykeycharacteristics_formset.forms %}
                            {{ form|crispy }}
                            {% if forloop.counter != 1 %}
                                <button class="btn btn-primary" id="add-form"><i class="fa fa-plus"></i></button>
                                <button class="btn btn-danger" id="remove-form"><i class="fa fa-minus"></i></button>
                            {% else %}
                                <button class="btn btn-primary" id="add-form"><i class="fa fa-plus"></i></button>
                            {% endif %}
                        {% endfor %}
                    </div>
{#                    {{ activitykeycharacteristics_formset|crispy }}#}
                    <hr>
                    <button class="primaryAction btn btn-primary pull-right ml-1" type="submit">{% trans "Submit" %}</button>
                    <a class="btn btn-secondary pull-right" href="{{ request.META.HTTP_REFERER }}" role="button">{% trans "Cancel" %}</a>
                </form>
            </div>
            <div class="col"></div>
        </div>
    </div>
{% endblock content %}
BeeNag
  • 1,764
  • 8
  • 25
  • 42
  • This is indeed something you should do with javascript/jQuery. The SO question you're pointing to is definitely the right way to go. It's difficult to help you not knowing what your code was (the `.js` code) and what exactly went wrong. It shouldn't be too difficult to debug, by stepping through the javascript line by line with the debugger. – dirkgroten Nov 09 '17 at 15:54
  • So I think my main problem is that I can't figure out how to get the clone to work with two inputs rather than one. I have updated my post with the current JS code I am using which isn't working. – BeeNag Nov 09 '17 at 16:19
  • @dirkgroten You were right there was a typo in my jQuery selector that I missed. However, I'm still struggling to get the clone to work properly with then two inputs. Any thoughts? – BeeNag Nov 09 '17 at 16:58
  • Scratch that it still isn't working as I would like overall. When I add a form now, it seems to create a new copy of the formset instead of the form. I think it is something wrong with the cloneMore function but I can't really figure that out – BeeNag Nov 09 '17 at 17:16
  • 1
    the selector you use is `$("#formset:last")` which is selecting the last `div` with `id="formset"`. That's not the right selector. You need to inspect your HTML and see what is the div type or class you need to clone. It might be something like `$("#formset form:last")`. Not sure what `{{ form|crispy }}` produces. Otherwise, wrap `{{ form|crispy }}` in a `
    ` and select `$("#formset .form-to-clone:last")`
    – dirkgroten Nov 09 '17 at 17:27
  • Thanks yeah that sorted it – BeeNag Nov 09 '17 at 17:38

0 Answers0