239
This is how I do it, using jQuery:
My template:
<h3>My Services</h3>
{{ serviceFormset.management_form }}
{% for form in serviceFormset.forms %}
<div class='table'>
<table class='no_error'>
{{ form.as_table }}
</table>
</div>
{% endfor %}
<input type="button" value="Add More" id="add_more">
<script>
$('#add_more').click(function() {
cloneMore('div.table:last', 'service');
});
</script>
In a javascript file:
function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
What it does:
cloneMore
accepts selector
as the first argument, and the type
of formset as the 2nd one. What the selector
should do is pass it what it should duplicate. In this case, I pass it div.table:last
so that jQuery looks for the last table with a class of table
. The :last
part of it is important because the selector
is also used to determine what the new form will be inserted after. More than likely you’d want it at the end of the rest of the forms. The type
argument is so that we can update the management_form
field, notably TOTAL_FORMS
, as well as the actual form fields. If you have a formset full of, say, Client
models, the management fields will have IDs of id_clients-TOTAL_FORMS
and id_clients-INITIAL_FORMS
, while the form fields will be in a format of id_clients-N-fieldname
with N
being the form number, starting with 0
. So with the type
argument the cloneMore
function looks at how many forms there currently are, and goes through every input and label inside the new form replacing all the field names/ids from something like id_clients-(N)-name
to id_clients-(N+1)-name
and so on. After it is finished, it updates the TOTAL_FORMS
field to reflect the new form and adds it to the end of the set.
This function is particularly helpful to me because the way it is setup it allows me to use it throughout the app when I want to provide more forms in a formset, and doesn’t make me need to have a hidden "template" form to duplicate as long as I pass it the formset name and the format in which the forms are laid out.
133
Simplified version of Paolo’s answer using empty_form
as a template.
<h3>My Services</h3>
{{ serviceFormset.management_form }}
<div id="form_set">
{% for form in serviceFormset.forms %}
<table class='no_error'>
{{ form.as_table }}
</table>
{% endfor %}
</div>
<input type="button" value="Add More" id="add_more">
<div id="empty_form" style="display:none">
<table class='no_error'>
{{ serviceFormset.empty_form.as_table }}
</table>
</div>
<script>
$('#add_more').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
</script>
- [Django]-Trying to migrate in Django 1.9 — strange SQL error "django.db.utils.OperationalError: near ")": syntax error"
- [Django]-Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?
- [Django]-How do I filter query objects by date range in Django?
17
Paolo’s suggestion works beautifully with one caveat – the browser’s back/forward buttons.
The dynamic elements created with Paolo’s script will not be rendered if the user returns to the formset using the back/forward button. An issue that may be a deal breaker for some.
Example:
1) User adds two new forms to the formset using the “add-more” button
2) User populates the forms and submits the formset
3) User clicks the back button in the browser
4) Formset is now reduced to the original form, all dynamically added forms are not there
This is not a defect with Paolo’s script at all; but a fact of life with dom manipulation and browser’s cache.
I suppose one could store the values of the form in the session and have some ajax magic when the formset loads to create the elements again and reload the values from the session; but depending on how anal you want to be about the same user and multiple instances of the form this may become very complicated.
Anyone has a good suggestion for dealing with this?
Thanks!
- [Django]-What is the difference render() and redirect() in Django?
- [Django]-Django.db.utils.ProgrammingError: relation "bot_trade" does not exist
- [Django]-Function decorators with parameters on a class based view in Django
11
Simulate and imitate:
- Create a formset which corresponds to the situation before clicking the “add” button.
- Load the page, view the source and take a note of all
<input>
fields. - Modify the formset to correspond to the situation after clicking the “add” button (change the number of extra fields).
- Load the page, view the source and take a note of how the
<input>
fields changed. - Create some JavaScript which modifies the DOM in a suitable way to move it from the before state to the after state.
- Attach that JavaScript to the “add” button.
While I do know formsets use special hidden <input>
fields and know approximately what the script must do, I don’t recall the details off the top of my head. What I described above is what I would do in your situation.
- [Django]-Django: how to do calculation inside the template html page?
- [Django]-VueJS + Django Channels
- [Django]-ModuleNotFoundError: No module named 'grp' on windows
7
For the coders out there who are hunting resources to understand the above solutions a little better:
After reading the above link, the Django documentation and previous solutions should make a lot more sense.
As a quick summary of what I was getting confused by: The Management Form contains an overview of the forms within. You must keep that information accurate in order for Django to be aware of the forms you add. (Community, please give me suggestions if some of my wording is off here. Im new to Django.)
- [Django]-Django switching, for a block of code, switch the language so translations are done in one language
- [Django]-Django model one foreign key to many tables
- [Django]-Uwsgi installation error in windows 7
4
One option would be to create a formset with every possible form, but initially set the unrequired forms to hidden – ie, display: none;
. When it’s necessary to display a form, set it’s css display to block
or whatever is appropriate.
Without know more details of what your “Ajax” is doing, it’s hard to give a more detailed response.
- [Django]-Visual Editor for Django Templates?
- [Django]-How to get the current URL within a Django template?
- [Django]-How to run celery as a daemon in production?
4
Another cloneMore version, which allows for selective sanitization of fields. Use it when you need to prevent several fields from being erased.
$('table tr.add-row a').click(function() {
toSanitize = new Array('id', 'product', 'price', 'type', 'valid_from', 'valid_until');
cloneMore('div.formtable table tr.form-row:last', 'form', toSanitize);
});
function cloneMore(selector, type, sanitize) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var namePure = $(this).attr('name').replace(type + '-' + (total-1) + '-', '');
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).removeAttr('checked');
if ($.inArray(namePure, sanitize) != -1) {
$(this).val('');
}
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
- [Django]-Django – Clean permission table
- [Django]-Testing nginx without domain name
- [Django]-How to get the name of current app within a template?
3
Because all answers above use jQuery and make some things a bit complex I wrote following script:
function $(selector, element) {
if (!element) {
element = document
}
return element.querySelector(selector)
}
function $$(selector, element) {
if (!element) {
element = document
}
return element.querySelectorAll(selector)
}
function hasReachedMaxNum(type, form) {
var total = parseInt(form.elements[type + "-TOTAL_FORMS"].value);
var max = parseInt(form.elements[type + "-MAX_NUM_FORMS"].value);
return total >= max
}
function cloneMore(element, type, form) {
var totalElement = form.elements[type + "-TOTAL_FORMS"];
total = parseInt(totalElement.value);
newElement = element.cloneNode(true);
for (var input of $$("input", newElement)) {
input.name = input.name.replace("-" + (total - 1) + "-", "-" + total + "-");
input.value = null
}
total++;
element.parentNode.insertBefore(newElement, element.nextSibling);
totalElement.value = total;
return newElement
}
var addChoiceButton = $("#add-choice");
addChoiceButton.onclick = function() {
var choices = $("#choices");
var createForm = $("#create");
cloneMore(choices.lastElementChild, "choice_set", createForm);
if (hasReachedMaxNum("choice_set", createForm)) {
this.disabled = true
}
};
First you should set auto_id to false and so disable the duplication of id and name. Because the input names have to be unique in there form, all identification is done with them and not with id’s.
You also have to replace the form
, type
and the container of the formset. (In the example above choices
)
- [Django]-Itertools.groupby in a django template
- [Django]-Django. A good tutorial for Class Based Views
- [Django]-Django MEDIA_URL and MEDIA_ROOT
2
There is a small issue with the cloneMore function. Since it’s also cleaning the value of the django auto-generated hidden fields, it causes django to complain if you try to save a formset with more than one empty form.
Here is a fix:
function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
if ($(this).attr('type') != 'hidden') {
$(this).val('');
}
$(this).attr({'name': name, 'id': id}).removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
- [Django]-Using Python's os.path, how do I go up one directory?
- [Django]-How to force Django models to be released from memory
- [Django]-Django model constraint for related objects
1
Yea I’d also recommend just rendering them out in the html if you have a finite number of entries. (If you don’t you’ll have to user another method).
You can hide them like this:
{% for form in spokenLanguageFormset %}
<fieldset class="languages-{{forloop.counter0 }} {% if spokenLanguageFormset.initial_forms|length < forloop.counter and forloop.counter != 1 %}hidden-form{% endif %}">
Then the js is really simple:
addItem: function(e){
e.preventDefault();
var maxForms = parseInt($(this).closest("fieldset").find("[name*='MAX_NUM_FORMS']").val(), 10);
var initialForms = parseInt($(this).closest("fieldset").find("[name*='INITIAL_FORMS']").val(), 10);
// check if we can add
if (initialForms < maxForms) {
$(this).closest("fieldset").find("fieldset:hidden").first().show();
if ($(this).closest("fieldset").find("fieldset:visible").length == maxForms ){
// here I'm just hiding my 'add' link
$(this).closest(".control-group").hide();
};
};
}
- [Django]-Django-tables2: How to use accessor to bring in foreign columns?
- [Django]-What is pip install -q -e . for in this Travis-CI build tutorial?
- [Django]-Detect mobile, tablet or Desktop on Django
0
I can recommend django-dynamic-formsets for everyone who is just looking for a solution that works out of the box.
It replaced all the code I derived from the solutions proposed and gives some additional functionality, such as deleting forms or stylizing related buttons.
- [Django]-Django: show the count of related objects in admin list_display
- [Django]-Django Generic Views using decorator login_required
- [Django]-How to print BASE_DIR from settings.py from django app in terminal?