Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

javascript - Add row dynamically in django formset

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:

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...