Add row dynamically in django formset
Asked Answered
S

1

8

In my django app I have two models i.e Player and Team which are connected by many to many relationship. To add the data dynamically in my tables I want to use javascript to add Add row or Remove Row button in my forms but unable to do so.

Here are the details:

Models.py

class Player(models.Model):
    pname = models.CharField(max_length=50)
    hscore = models.IntegerField()
    age = models.IntegerField()

    def __str__(self):
       return self.pname

class Team(models.Model):
    tname = models.CharField(max_length=100)
    player= models.ManyToManyField(Player)

    def __str__(self):
        return self.tname

Forms.py

class PlayerForm(forms.Form):
    pname = forms.CharField()
    hscore= forms.IntegerField()
    age = forms.IntegerField()

PlayerFormset= formset_factory(PlayerForm)

class TeamForm(forms.Form):
   tname= forms.CharField()
   player= PlayerFormset()

Views.py

def post(request):

   if request.POST:
        form = TeamForm(request.POST)
        form.player_instances = PlayerFormset(request.POST)
        if form.is_valid():
            team= Team()
            team.tname= form.cleaned_data['tname']
            team.save()

        if form.player_instances.cleaned_data is not None:

            for item in form.player_instances.cleaned_data:
                player = Player()
                player.pname= item['pname']
                player.hscore= item['hscore']
                player.age= item['age']
                player.save()
                team.player.add(player)
            team.save()

   else:
        form = TeamForm()
        return render(request, 'new.html', {'form':form})

new.html

<html>
<head>

    <title>gffdfdf</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="/static/jquery.formset.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

</head>
<body>

<div class="container">

    <form id="myForm" action="" method="post" class="">
        {% csrf_token %}
        <h2> Team</h2>
        {% for field in form %}
            {{ field.errors }}
            {{ field.label_tag }} : {{ field }}
        {% endfor %}
        {{ form.player.management_form }}

        <h3> Product Instance(s)</h3>
        <table id="table-product" class="table">
            <thead>
            <tr>
                <th>player name</th>
                <th>highest score</th>
                <th>age</th>
            </tr>

            </thead>
            {% for player in form.player %}
                <tbody class="player-instances">

                <tr>
                    <td>{{ player.pname }}</td>
                    <td>{{ player.hscore }}</td>
                    <td>{{ player.age }}</td>
                </tr>

                </tbody>
            {% endfor %}
        </table>
        <button type="submit" class="btn btn-primary">save</button>

    </form>
</div>
<script>
    $(function () {
        $('#myForm tbody tr').formset();
    })
</script>
</body>
</html>

How can I use the javascript to add or delete the rows connected by many to many relationship ?

The above code gives us the following:

Sasin answered 10/4, 2020 at 7:6 Comment(0)
F
5

To keep it simple and generic, I reduced the OP's example to a single model and a basic formset, without the Team-Player many-to-many relation. The principle of the JavaScript part remains the same. If you do want to implement the many-to-many relation, you could use e.g. an inline formset, as explained here.

So, suppose we have a simple model:

class Player(models.Model):
    name = models.CharField(max_length=50)
    age = models.IntegerField()

Our view could look like this (based on the example in the docs):

def my_formset_view(request):
    response = None
    formset_class = modelformset_factory(
        model=Player, fields=('name', 'age'), extra=0, can_delete=True)
    if request.method == 'POST':
        formset = formset_class(data=request.POST)
        if formset.is_valid():
            formset.save()
            response = redirect(to='my_success_view')
    else:
        formset = formset_class()
    if response is None:
        response = render(
            request, 'myapp/my_formset_template.html', dict(formset=formset))
    return response

The my_formset_template.html django template below (skipping the boilerplate) enables us to add and remove formset-forms:

...
<template id="id_formset_empty_form">{{ formset.empty_form }}</template>
<form method="post" id="id_html_form" autocomplete="off">
    {% csrf_token %}
    <table id="id_formset_container">
        {{ formset }}
    </table>
    <div id="id_formset_add_button" style="text-decoration: underline; cursor: pointer;">Add</div>
    <input id="id_formset_submit_button" type="submit" value="Submit">
</form>
...

The HTML <template> element makes it easy to copy the content from formset.empty_form.

Side note: If we don't set autocomplete="off", the browser will cache the TOTAL_FORMS value on the management form, even after reloading the page.

Now, the following JavaScript does the job for me (no attempt was made to optimize, I just tried to make it easy to read):

window.addEventListener('load', (event) => {
    // get form template and total number of forms from management form
    const templateForm = document.getElementById('id_formset_empty_form');
    const inputTotalForms = document.querySelector('input[id$="-TOTAL_FORMS"]');
    const inputInitialForms = document.querySelector('input[id$="-INITIAL_FORMS"]');

    // get our container (e.g. <table>, <ul>, or <div>) and "Add" button
    const containerFormSet = document.getElementById('id_formset_container');
    const buttonAdd = document.getElementById('id_formset_add_button');
    const buttonSubmit = document.getElementById('id_formset_submit_button');

    // event handlers
    buttonAdd.onclick = addForm;
    buttonSubmit.onclick = updateNameAttributes;

    // form counters (note: proper form index bookkeeping is necessary
    // because django's formset will create empty forms for any missing
    // indices, and will discard forms with indices >= TOTAL_FORMS, which can
    // lead to funny behavior in some edge cases)
    const initialForms = Number(inputInitialForms.value);
    let extraFormIndices = [];
    let nextFormIndex = initialForms;

    function addForm () {
        // create DocumentFragment from template
        const formFragment = templateForm.content.cloneNode(true);
        // a django form is rendered as_table (default), as_ul, or as_p, so
        // the fragment will contain one or more <tr>, <li>, or <p> elements,
        // respectively.
        for (let element of formFragment.children) {
            // replace the __prefix__ placeholders from the empty form by the
            // actual form index
            element.innerHTML = element.innerHTML.replace(
                /(?<=\w+-)(__prefix__|\d+)(?=-\w+)/g,
                nextFormIndex.toString());
            // add a custom attribute to simplify bookkeeping
            element.dataset.formIndex = nextFormIndex.toString();
            // add a delete click handler (if formset can_delete)
            setDeleteHandler(element);
        }
        // move the fragment's children onto the DOM
        // (the fragment is empty afterwards)
        containerFormSet.appendChild(formFragment);
        // keep track of form indices
        extraFormIndices.push(nextFormIndex++);
    }

    function removeForm (event) {
        // remove all elements with form-index matching that of the delete-input
        const formIndex = event.target.dataset.formIndex;
        for (let element of getFormElements(formIndex)) {
            element.remove();
        }
        // remove form index from array
        let indexIndex = extraFormIndices.indexOf(Number(formIndex));
        if (indexIndex > -1) {
            extraFormIndices.splice(indexIndex, 1);
        }
    }

    function setDeleteHandler (containerElement) {
        // modify DELETE checkbox in containerElement, if the checkbox exists
        // (these checboxes are added by formset if can_delete)
        const inputDelete = containerElement.querySelector('input[id$="-DELETE"]');
        if (inputDelete) {
            // duplicate the form index instead of relying on parentElement (more robust)
            inputDelete.dataset.formIndex = containerElement.dataset.formIndex;
            inputDelete.onclick = removeForm;
        }
    }

    function getFormElements(index) {
        // the data-form-index attribute is available as dataset.formIndex
        // https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes#javascript_access
        return containerFormSet.querySelectorAll('[data-form-index="' + index + '"]');
    }

    function updateNameAttributes (event) {
        // make sure the name indices are consecutive and smaller than
        // TOTAL_FORMS (the name attributes end up as dict keys on the server)
        // note we do not need to update the indices in the id attributes etc.
        for (let [consecutiveIndex, formIndex] of extraFormIndices.entries()) {
            for (let formElement of getFormElements(formIndex)){
                for (let element of formElement.querySelectorAll('input, select')) {
                    if ('name' in element) {
                        element.name = element.name.replace(
                            /(?<=\w+-)(__prefix__|\d+)(?=-\w+)/g,
                            (initialForms + consecutiveIndex).toString());
                    }
                }
            }
        }
        updateTotalFormCount();
    }

    function updateTotalFormCount (event) {
        // note we could simply do initialForms + extraFormIndices.length
        // to get the total form count, but that does not work if we have
        // validation errors on forms that were added dynamically
        const firstElement = templateForm.content.querySelector('input, select');
        // select the first input or select element, then count how many ids
        // with the same suffix occur in the formset container
        if (firstElement) {
            let suffix = firstElement.id.split('__prefix__')[1];
            let selector = firstElement.tagName.toLowerCase() + '[id$="' + suffix + '"]';
            let allElementsForId = containerFormSet.querySelectorAll(selector);
            // update total form count
            inputTotalForms.value = allElementsForId.length;
        }
    }
}, false);


Note that simply adding and removing formset forms is not that complicated, until something goes wrong: Approximately half the lines above have to do with handling edge cases, such as failed validation on forms that were added dynamically.

Foxy answered 16/9, 2021 at 20:37 Comment(6)
Hi ,I just use this code in my project, the adding part is really great but i can't manage to make the removing option functionnal as it don't seems able to retrieve formIndex of related event. I see that you didn't render this part functionnal in the code. Any tips would be great.Assassin
@Assassin Form removal should also work. I just verified this on my system. Can you confirm that your forms have "delete" checkboxes with element id similar to "id_form-1-DELETE"?Foxy
Note that forms for existing database records are only removed after checking "delete" and clicking "Submit." Newly added forms (i.e. not submitted yet) are removed as soon as you check the "delete" box.Foxy
Well, apparently is missed something as the delete checkbox don't seems to appear when I add formAssassin
Have a look at the docs for can_delete, perhaps those can be of help.Foxy
Yes ! Thank you my problem was related to Django and not the JS... Sorry for the time...Assassin

© 2022 - 2024 — McMap. All rights reserved.