Add Row Dynamically In Django Formset
Solution 1:
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:
classPlayer(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
Our view could look like this (based on the example in the docs):
defmy_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 isNone:
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:
...
<templateid="id_formset_empty_form">{{ formset.empty_form }}</template><formmethod="post"id="id_html_form"autocomplete="off">
{% csrf_token %}
<tableid="id_formset_container">
{{ formset }}
</table><divid="id_formset_add_button"style="text-decoration: underline; cursor: pointer;">Add</div><inputid="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 formconst 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" buttonconst 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;
functionaddForm () {
// create DocumentFragment from templateconst 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++);
}
functionremoveForm (event) {
// remove all elements with form-index matching that of the delete-inputconst formIndex = event.target.dataset.formIndex;
for (let element ofgetFormElements(formIndex)) {
element.remove();
}
// remove form index from arraylet indexIndex = extraFormIndices.indexOf(Number(formIndex));
if (indexIndex > -1) {
extraFormIndices.splice(indexIndex, 1);
}
}
functionsetDeleteHandler (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;
}
}
functiongetFormElements(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_accessreturn containerFormSet.querySelectorAll('[data-form-index="' + index + '"]');
}
functionupdateNameAttributes (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 ofgetFormElements(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();
}
functionupdateTotalFormCount (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 dynamicallyconst 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 containerif (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.
Post a Comment for "Add Row Dynamically In Django Formset"