[PYTHON] Implement an add form button in your Django inline form set

Introduction

Django's in-line form set is just right for creating parent-child forms, such as the header part of a purchase order (date, customer name, etc.) and the order details (order, quantity, etc.) associated with it. is. (Handwriting example) However, it's a little confusing because you'll be dealing with multiple forms at once before you get used to it. Also, in the example of the purchase order, the line will have as many forms as the number set at the beginning for new input. However, if you run out of all the forms and want a new row, you have to implement the additional operation yourself. For that, jQuery is absolutely necessary.

In my case, I didn't understand the inline form set, and jQuery was about to be studied, so it didn't work as I expected and I had a hard time. It's easy once you understand everything. How can I add a row? I will share as much as I have learned.

things to do

Using jQuery as the subject of the order form, implement a button to add the form to the inline form set.

I won't explain the basics of Python and Django.

environment

Production

Let's start with a normal inline form set and see how Django manages a lot of forms. In the official tutorial, it's written around here [https://docs.djangoproject.com/en/3.0/topics/forms/formsets/) (Formset description, but form management is also inline formset It will be the same).

The source is put it here. Please enter the data of "Customer" and "Product" from the management screen.

The template to display the form is as follows. (Excerpt)

jutyu_form.html (excerpt)


<form action="" method="POST">
    {{ form }}
    <hr>
    {{ formset.management_form }}
    <div id="detail-area">
        {% for detail in formset %}
            {{ detail.as_table }}
            <br>
        {% endfor %}
    </div>
    {% csrf_token %}
    <button class="btn btn-primary" type="submit">Save</button>
</form>

{{form}} displays the "order date" and "customer" in the header part. The parts written in Django's template language are converted to HTML for web viewing. How is this part converted? Try viewing the page source in your browser. Django has written a lot of things. ↓ (I added annotations, line breaks, etc.)

<!-- {{ form }}Contents of-->
<tr>
    <th><label for="id_jutyu_date">Order date:</label></th>
    <td>
        <input type="text" name="jutyu_date" value="2020-07-02" required id="id_jutyu_date">
        <input type="hidden" name="initial-jutyu_date" value="2020-07-02" id="initial-id_jutyu_date">
    </td>
</tr>
<tr>
    <th><label for="id_customer">client:</label></th>
    <td>
        <select name="customer" required id="id_customer">
        <option value="" selected>---------</option>
        <option value="1">Showa Kogyo</option>
        <option value="2">Heisei Seisakusho</option>
        </select>
    </td>
</tr>

It seems that <label> and <input> are surrounded by <tr> so that they are lined up horizontally. Also, the initial value of the order date is recorded with the hidden attribute. The customer gave me a selece box without specifying anything. The initial value is unselected "---------".

Just write {{form}} and it will be converted to the appropriate HTML. It will be very helpful. But conversely, if you want to display something different from Django's conversion, for example, if you want a space instead of ":" at the end of the label name, or if you want the label and field to line up vertically, let's specify it in the template. There is no such thing, so I have to take another method.

How are inline form sets managed?

Let's take a look at the inline form set part. The inline formset is instantiated in the view and passed to the template with the name'formset'.

This part of the template

{{ formset.management_form }}
<div id="detail-area">
    {% for detail in formset %}
        {{ detail.as_table }}
        <br>
    {% endfor %}
</div>

It was converted to HTML like this. ↓ (I added annotations, line breaks, etc.) It has increased considerably. It seems that the key to this theme is hidden in the information that is increasing in various ways.

<!-- {{ formaset.management_form }}Contents of-->
<input type="hidden" name="jutyudetail_set-TOTAL_FORMS" value="1" id="id_jutyudetail_set-TOTAL_FORMS">
<input type="hidden" name="jutyudetail_set-INITIAL_FORMS" value="0" id="id_jutyudetail_set-INITIAL_FORMS">
<input type="hidden" name="jutyudetail_set-MIN_NUM_FORMS" value="0" id="id_jutyudetail_set-MIN_NUM_FORMS">
<input type="hidden" name="jutyudetail_set-MAX_NUM_FORMS" value="1000" id="id_jutyudetail_set-MAX_NUM_FORMS">

<div id="detail-area">
<tr>
    <th><label for="id_jutyudetail_set-0-part">parts:</label></th>
    <td>
        <select name="jutyudetail_set-0-part" id="id_jutyudetail_set-0-part">
        <option value="" selected>---------</option>
        <option value="1">111-222 /Stainless steel fittings</option>
        <option value="2">333-444 /O-ring</option>
        </select>
    </td>
</tr>
<tr>
    <th><label for="id_jutyudetail_set-0-quantity">Order quantity:</label></th>
    <td><input type="number" name="jutyudetail_set-0-quantity" id="id_jutyudetail_set-0-quantity"></td>
</tr>
<tr>
    <th><label for="id_jutyudetail_set-0-DELETE">Delete:</label></th>
    <td>
        <input type="checkbox" name="jutyudetail_set-0-DELETE" id="id_jutyudetail_set-0-DELETE">
        <input type="hidden" name="jutyudetail_set-0-id" id="id_jutyudetail_set-0-id">
        <input type="hidden" name="jutyudetail_set-0-jutyu_head" id="id_jutyudetail_set-0-jutyu_head">
    </td>
</tr>
<br>
</div>
TOTAL_FORMS is the number of forms included

It is written in HTML created from {{formset.management_form}}.

<input type="hidden" name="jutyudetail_set-TOTAL_FORMS" value="1" id="id_jutyudetail_set-TOTAL_FORMS">

It has a hidden attribute <input> with a long id. This line shows the number of forms in the inline form set, where value =" 1 " is. In this case it is one line.

Each field has an id that tells you which form it belongs to.

What about each field? For example, the HTML of the select box for selecting a part looks like this.

<tr>
    <th><label for="id_jutyudetail_set-0-part">parts:</label></th>
    <td>
        <select name="jutyudetail_set-0-part" id="id_jutyudetail_set-0-part">
        <option value="" selected>---------</option>
        <option value="1">111-222 /Stainless steel fittings</option>
        <option value="2">333-444 /O-ring</option>
        </select>
    </td>
</tr>

It consists of <label> and <select> and has a common id and name attribute. Looking at id, it means something like this. アセット 4.png It seems to be long, but it is regular. The name attribute just doesn't have an'id_'. The ** id_jutyudetail_set-** part was also used in TOTAL_FORMS. This is the original model ** Jutyu Detail ** in all lowercase. That is the set. Also, I found out earlier that the number of forms included in the inline form set is one. The numbers are numbered from 0, so it's the 0th of the form, its ** part ** field.

Then, what is the id that should be given to the order quantity field when adding a second form, which is not yet available?

What is the id of the order quantity field in the second form?


id="id_jutyudetail_set-0-quantity"

If so, it looks good.

No other hidden fields required

Django has added two hidden attributes.

<input type="hidden" name="jutyudetail_set-0-id" id="id_jutyudetail_set-0-id">
<input type="hidden" name="jutyudetail_set-0-jutyu_head" id="id_jutyudetail_set-0-jutyu_head">

Looking at ʻid = , it is the id of the JutyuDetail model and the jutyu_head of the foreign key field. I used {{detail_form.as_table}}` in the template for the sake of simplicity, because Django generated the HTML, including the fields that aren't displayed. The line you add does not need to include these two.

Try to implement

If you write the procedure to add a line in words,

  1. Get the number of forms included in the inline forms set (= get the value of TOTAL_FORMS).
  2. Add each field with an id that contains a number that represents the number.
  3. Increase TOTAL_FORMS by 1.

Now suppose you get the value of TOTAL_FORMS and it is 3. There are 0, 1 and 2 forms. The newly added form will have the id of ʻid_jutyudetail_set-3-field name`.

I'm sorry. I'm studying jQuery and I'm not confident, but I can write it as follows.

{% block extrajs %}
    <script>
        jQuery(function ($) {
            // 1.Get the number of forms included in the inline form set (= TOTAL)_Get the FORMS value)
            var totalManageElement = $("input#id_jutyudetail_set-TOTAL_FORMS");
            var currentJutyuDetailCount = parseInt(totalManageElement.val());

            //When the "Add Row" button is pressed
            //2. Add each field with an id containing a number to indicate the number
            $("button#addForm").on("click", function () {
                //parts
                var partElement = $("<select>", {
                    name: "jutyudetail_set-" + currentJutyuDetailCount + "-part",
                    id: "id_jutyudetail_set-" + currentJutyuDetailCount + "-part",
                });
                partElement.append($("<option>").html("---------").val(""));
                {% for part in part_list %}
                    partElement.append($("<option>").html("{{ part }}").val("{{ part.pk }}"));
                {% endfor %}
                //Order quantity
                var quantityElement = $("<input>", {
                    type: "number",
                    name: "jutyudetail_set-" + currentJutyuDetailCount + "-quantity",
                    id: "id_jutyudetail_set-" + currentJutyuDetailCount + "-quantity",
                });
                //Delete
                var deleteElement = $("<input>", {
                    type: "checkbox",
                    name: "jutyudetail_set-" + currentJutyuDetailCount + "-DELETE",
                    id: "id_jutyudetail_set-" + currentJutyuDetailCount + "-DELETE",
                });
                //add to
                $(partElement).appendTo("div#detail-area");
                $(quantityElement).appendTo("div#detail-area");
                $(deleteElement).appendTo("div#detail-area");
                $("<br>").appendTo("div#detail-area");

                // 3. TOTAL_Increase FORMS by 1
                currentJutyuDetailCount += 1
                $("input#id_jutyudetail_set-TOTAL_FORMS").attr("value", currentJutyuDetailCount);
            });
        });
    </script>
{% endblock extrajs %}

Label display is omitted. It's also noisy that every line is labeled. If you actually use it, you should think about the form design itself.

Future improvements

For the time being, I have something that works. However, if there are more fields, it is quite troublesome to write. It needs to be rewritten even if the order of the fields changes. There should be an easier way, just because I don't know, so I'll write about that again.

reference

--Narito Blog "Django implements add form button in form set"

Recommended Posts

Implement an add form button in your Django inline form set
Implementing Add Form Buttons in Django Inline Forms Set Part2
Dynamically add form fields in Django
Dynamically add fields to Form objects in Django
Set the form DateField to type = date in Django
Set the DateTime type output format in your Django template
Implement follow functionality in Django
Set placeholders in input fields in Django
Register your Django application in your project
Like button implementation in Django + Ajax
Implement a Custom User Model in Django
[Django memo] I want to set the login user information in the form in advance
Implement the ability to reserve what happens regularly in your Django Todo list