在 Django 表单集中动态添加/删除日期字段

问题描述 投票:0回答:1

我有一个非常具体的问题,与 django-bootstrap-datepicker-plus 包相关。

在我的待办事项列表应用程序中,我希望能够在多个特定日期弹出任务。我设置了模型,设置了包含表单集的表单,甚至还有一个处理动态添加/删除过程的 JavaScript。

我遇到的问题是我的

DateField
的克隆过程在某种程度上与
DatePicker
div 混在一起 - 请参阅下面最后一个代码块中的结果。

# model.py
from django.db import models
from datetime import time

# Create your models here.
class Task(models.Model):

    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=50, default="")
    description = models.CharField(max_length=500, default="")
    entry_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)
    specific_dates = models.ManyToManyField('SpecificDate', blank=True)
    due_until = models.TimeField(auto_now_add=False, default=time(12, 0))


class SpecificDate(models.Model):
    todo = models.ForeignKey(Task, on_delete=models.CASCADE)
    date = models.DateField(auto_now_add=False, blank=True, null=True)

    class Meta:
        # ensure each specific date is unique per task
        unique_together = ('todo', 'date')
# forms.py
from bootstrap_datepicker_plus.widgets import DatePickerInput, TimePickerInput
from django import forms
from .models import Task, SpecificDate

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = [
            'title',
            'description',
            'due_until',
            ]
        widgets = {
            'due_until': TimePickerInput(options={'stepping': 5, 'format': 'HH:mm'}),
            'description': forms.Textarea({'rows': 3}),
        }


class SpecificDateForm(forms.ModelForm):
    class Meta:
        model = SpecificDate
        fields = ['date']
        widgets = {
            'date': DatePickerInput(),
        }


SpecificDateFormset = forms.inlineformset_factory(
    Task, SpecificDate,
    form=SpecificDateForm,
    fields=('date',),
    extra=1,
    can_delete=True
)
<!-- task_form.html -->
{% extends "task/base.html" %}
{% load crispy_forms_tags %}
{% load bootstrap4 %}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}
{% block content %}
<div class="content-section">
    <form method="POST">
        {% csrf_token %}
        <fieldset class="form-group">
            <legend class="border-bottom mb-4">New entry</legend>
            {% if form.non_field_errors %}
            <div class="alert alert-danger">
                {{ form.non_field_errors }}
            </div>
            {% endif %}
            {% crispy form %}
            <div id="formset-container">
                {{ formset.management_form }}
                {% for form in formset %}
                    {% if form.errors %}
                    <div class="alert alert-danger">
                        {{ form.errors }}
                    </div>
                    {% endif %}
                    <div id="formset-date" class="formset-date d-flex align-items-center">
                        <div class="flex-grow-1 mr-2">
                            {{ form.date|as_crispy_field }}
                        </div>
                        <div>
                            <button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
                            <button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
                        </div>
                    </div>
                {% endfor %}
            </div>
        </fieldset>
        <div class="form-group">
            <button class="btn btn-primary" type="submit" name="add_new">Save</button>
            <a class="btn btn-warning" href="{% url 'task-new' %}">Restart</a>
        </div>
    </form>
    {{ form.media }}
</div>
{% endblock content %}
// JavaScript for add/remove buttons
document.addEventListener('DOMContentLoaded', function() {
    const formsetContainer = document.getElementById('formset-container');
    const formsetPrefix = 'specificdate_set';
    const totalFormsElement = document.getElementById('id_'+formsetPrefix+'-TOTAL_FORMS');
    
    // Adjusts name and id for cloned form elements
    function adjustNamesAndIds(newForm, newIndex) {
        newForm.querySelectorAll('[id], label').forEach(element => {
            // debugger;

            // First up, let's do a few if statements for elements we want
            // to skip.
            // ... buttons
            if (element.tagName === 'BUTTON') {
                console.log('Skipping button ' + element.name);
                return;
            }
            
            // ... formset management fields as they already have valid names
            if (element.type === 'hidden' && (element.value === '')) {
                console.log('Skipping formset management field or empty hidden field: ' + element.name);
                return;
            }
            
            if (element.tagName === 'LABEL' && element.htmlFor) {
                const htmlForMatch = element.htmlFor.match(/^(.+)-\d+-(.+)$/);
                if (htmlForMatch) {
                    element.htmlFor = `${htmlForMatch[1]}-${newIndex}-${htmlForMatch[2]}`;
                }
            }
            
            if (element.dataset.name) {
                const nameMatch = element.dataset.name.match(/^(.+)-\d+-(.+)$/);
                if (nameMatch) {
                    element.dataset.name = `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`;
                }
            }

            if (element.name) {
                const nameMatch = element.name.match(/^(.+)-\d+-(.+)$/);
                if (nameMatch) {
                    element.name = `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`;
                }
            }
            
            // Update IDs for inputs and corresponding labels, if applicable
            if (element.id) {
                const idMatch = element.id.match(/^(.+)-\d+-(.+)$/);
                if (idMatch) {
                    const newId = `${idMatch[1]}-${newIndex}-${idMatch[2]}`;
                    element.id = newId;
                    const label = newForm.querySelector(`label[for="${element.id}"]`);
                }
            }
        });
        
        // // Additionally, update div IDs related to the form fields for
        // // consistency
        // newForm.querySelectorAll('div[id]').forEach(div => {
        //     const idMatch = div.id.match(/^(.+)-\d+-(.+)$/);
        //     if (idMatch) {
        //         div.id = `${idMatch[1]}-${newIndex}-${idMatch[2]}`;
        //     }
        // });
    }
    
    function cloneForm() {
        let totalForms = parseInt(totalFormsElement.value, 10);
        let newFormIndex = totalForms;
        totalFormsElement.value = totalForms + 1;
        
        let newForm = formsetContainer.querySelector('.formset-date:last-of-type').cloneNode(true);
        if (newForm && typeof newFormIndex !== 'undefined') {
            adjustNamesAndIds(newForm, newFormIndex);
            formsetContainer.appendChild(newForm);
        } else {
            console.error('Error cloning form: newForm or newFormIndex is invalid.');
        }
    }
    
    formsetContainer.addEventListener('click', function(event) {
        if (event.target.classList.contains('add-date')) {
            console.log('Add');
            event.preventDefault();
            cloneForm();
        } else if (event.target.classList.contains('remove-date')) {
            console.log('Remove');
            event.preventDefault();
            if (formsetContainer.querySelectorAll('.formset-date').length > 1) { // Ensure at least one form remains
                let formToRemove = event.target.closest('.formset-date');
                formToRemove.remove();
            }
        }
    });
});

单击“添加”按钮后带有表单集的 HTML 片段如下所示:

<div id="formset-container">
  <input type="hidden" name="specificdate_set-TOTAL_FORMS" value="2" id="id_specificdate_set-TOTAL_FORMS"><input type="hidden" name="specificdate_set-INITIAL_FORMS" value="0" id="id_specificdate_set-INITIAL_FORMS"><input type="hidden" name="specificdate_set-MIN_NUM_FORMS" value="0" id="id_specificdate_set-MIN_NUM_FORMS"><input type="hidden" name="specificdate_set-MAX_NUM_FORMS" value="1000" id="id_specificdate_set-MAX_NUM_FORMS">
  <div id="formset-date" class="formset-date d-flex align-items-center">
    <div class="flex-grow-1 mr-2">
      <div id="div_id_specificdate_set-0-date" class="form-group">
        <label for="id_specificdate_set-0-date" class="">
          Date
        </label>
        <div>
          <div class="input-group dbdp">
            <input type="text" class="datepickerinput form-control" id="id_specificdate_set-0-date" data-dbdp-config="{&quot;variant&quot;: &quot;date&quot;, &quot;backend_date_format&quot;: &quot;YYYY-MM-DD&quot;, &quot;options&quot;: {&quot;locale&quot;: &quot;de-DE&quot;, &quot;format&quot;: &quot;DD.MM.YYYY&quot;}}" data-dbdp-debug="" data-name="specificdate_set-0-date">
            <div class="input-group-addon input-group-append input-group-text">
              <i class="bi-calendar"></i>
            </div>
          </div><input type="hidden" name="specificdate_set-0-date" value="">
        </div>
      </div>
    </div>
    <div>
      <button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
      <button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
    </div>
  </div>
  <div id="formset-date" class="formset-date d-flex align-items-center">
    <div class="flex-grow-1 mr-2">
      <div id="div_id_specificdate_set-1-date" class="form-group">
        <label for="id_specificdate_set-1-date" class="">
          Date
        </label>
        <div>
          <div class="input-group dbdp">
            <input type="text" class="datepickerinput form-control" id="id_specificdate_set-1-date" data-dbdp-config="{&quot;variant&quot;: &quot;date&quot;, &quot;backend_date_format&quot;: &quot;YYYY-MM-DD&quot;, &quot;options&quot;: {&quot;locale&quot;: &quot;de-DE&quot;, &quot;format&quot;: &quot;DD.MM.YYYY&quot;}}" data-dbdp-debug="" data-name="null">
            <div class="input-group-addon input-group-append input-group-text">
              <i class="bi-calendar"></i>
            </div>
          </div><input type="hidden" name="null" value=""><input type="hidden" name="specificdate_set-0-date" value="">
        </div>
      </div>
    </div>
    <div>
      <button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
      <button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
    </div>
  </div>
</div>

如您所见(如果您仔细观察),DatePicker 的 dataset.name 属性不会更新。或者,好吧,它得到了更新 -

element.dataset.name
在处理过程中具有正确的名称 - 但在渲染的 HTML 中,它又回到了原始名称,并且我还得到了那些
<input type="hidden" name="null" value="">
元素。

问题很简单:如何在各个

<input>
字段中获取正确的 dataset.names,以便提交时的
POST
请求具有正确的元素?

javascript django formset django-bootstrap4
1个回答
0
投票

虽然经过了几次迭代,但我最终得到了解决我的问题的有效 JavaScript 代码。它还负责禁用除最后一个添加/删除按钮之外的所有按钮。虽然不漂亮,但是很管用。

document.addEventListener('DOMContentLoaded', function() {
    const formsetContainer = document.getElementById('formset-container');
    const formsetPrefix = 'specificdate_set';
    const totalFormsElement = document.getElementById('id_' + formsetPrefix + '-TOTAL_FORMS');

    function updateLabels() {
        $('.formset-date').each(function(index) {
            $(this).find('label').text(`Date ${index + 1}`);
        });
    }

    function manageAddRemoveButtons() {
        let formsetDivs = formsetContainer.querySelectorAll('.formset-date');
        formsetDivs.forEach((div, index) => {
            // disable add button for all but the last row
            div.querySelector('.add-date').disabled = (
                index !== formsetDivs.length - 1);
            // disable remove button for all but the last
            // row, but not if only one row is present
            div.querySelector('.remove-date').disabled = (
                formsetDivs.length === 1) || (
                    index !== formsetDivs.length - 1);
        });
    }

    function adjustNamesAndIds(newForm, newIndex) {
        const regex = /^(.+)-\d+-(.+)$/;

        newForm.querySelectorAll('[id], label, .datepickerinput').forEach(element => {
            if (element.tagName === 'BUTTON') return; // Skip buttons entirely

            ['id', 'htmlFor', 'name'].forEach(attribute => {
                if (element[attribute]) {
                    const match = element[attribute].match(regex);
                    if (match) {
                        element[attribute] = `${match[1]}-${newIndex}-${match[2]}`;
                    }
                }
            });

            if (element.classList.contains('datepickerinput')) {
                const $datepickerInput = $(element);
                if ($datepickerInput.length && $datepickerInput.data('name')) {
                    const nameMatch = $datepickerInput.data('name').match(regex);
                    if (nameMatch) {
                        $datepickerInput.attr('name', `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`);
                    }
                }
            }
        });
    }

    function cloneForm() {
        let totalForms = parseInt(totalFormsElement.value, 10);
        let newFormIndex = totalForms;
        totalFormsElement.value = totalForms + 1;

        let newForm = formsetContainer.querySelector('.formset-date:last-of-type').cloneNode(true);
        if (newForm && typeof newFormIndex !== 'undefined') {
            adjustNamesAndIds(newForm, newFormIndex);
            formsetContainer.appendChild(newForm);
            // Remove Extra Hidden Fields
            let inputName = `${formsetPrefix}-${totalForms-1}-date`;
            newForm.querySelectorAll(`input[type="hidden"][name="${inputName}"]`).forEach(
                hiddenInput => hiddenInput.remove()
            ); 
        } else {
            console.error('Error cloning form: newForm or newFormIndex is invalid.');
        }
    }

    formsetContainer.addEventListener('click', function(event) {
        if (event.target.classList.contains('add-date')) {
            event.preventDefault();
            cloneForm();
        } else if (event.target.classList.contains('remove-date')) {
            event.preventDefault();
            let formToRemove = event.target.closest('.formset-date');
            formToRemove.remove();
        }
        updateLabels();
        manageAddRemoveButtons();
    });

    updateLabels(); 
    manageAddRemoveButtons();
});
© www.soinside.com 2019 - 2024. All rights reserved.