Django: How to add an extra form to a formset after it has been constructed?
Solution 1
class RequiredFormSet(BaseFormSet):
def add_form(self, **kwargs):
# add the form
tfc = self.total_form_count()
self.forms.append(self._construct_form(tfc, **kwargs))
self.forms[tfc].is_bound = False
# make data mutable
self.data = self.data.copy()
# increase hidden form counts
total_count_name = '%s-%s' % (self.management_form.prefix, TOTAL_FORM_COUNT)
initial_count_name = '%s-%s' % (self.management_form.prefix, INITIAL_FORM_COUNT)
self.data[total_count_name] = self.management_form.cleaned_data[TOTAL_FORM_COUNT] + 1
self.data[initial_count_name] = self.management_form.cleaned_data[INITIAL_FORM_COUNT] + 1
def add_fields(self, form, index):
super(RequiredFormSet, self).add_fields(form, index)
form.empty_permitted = False
That will do it. Only took 7 hours to figure out. And I still don't know why I need .is_bound = False
to make the initial values not screw up.
Solution 2
I do this using javascript. Since the formset renders three management fields
<input type="hidden" id="id_TOTAL_FORMS" value="1" name="TOTAL_FORMS">
<input type="hidden" id="id_INITIAL_FORMS" value="1" name="INITIAL_FORMS">.
<input type="hidden" id="id_MAX_NUM_FORMS" name="MAX_NUM_FORMS">
you can use javascript to increment the id_TOTAL_FORMS value, and just add in the extra fields. So I'd create my fieldset like this:
VehicleFormSet = modelformset_factory(StaffVehicleForm, extra = 0, max_num = None)
The tricky thing is to create the extra form fields in javascript. I usually use AJAX to fetch a new row from a custom view.
Solution 3
For posterity here is another way which works without JS (or alongside JS) and which does not require intimate knowledge of formset methods. Instead, you can just inspect the POST data and adjust it as if JS had done some work client-side. The following makes sure that there is always (at least) one empty form at the end of the formset:
def hsview( request):
HS_formset = formset_factory( HSTestForm, extra=3 )
prefix='XYZZY'
testinpost, empty = 'key', '' # field in the form and its default/empty value
extra=3
# I prefer to do the short init of unbound forms first, so I invert the usual test ...
if request.method != 'POST':
formset = HS_formset( prefix=prefix)
else:
# process POSTed forms data.
# pull all relevant things out of POST data, because POST itself is not mutable
# (it doesn't matter if prefix allows in extraneous items)
data = { k:v for k,v in request.POST.items() if k.startswith(prefix) }
#if there are no spare empty forms, tell it we want another form, in place of or extra to client-side JS
#don't want to crash if unvalidated POST data is nbg so catch all ...
try:
n = int( data[ prefix + '-TOTAL_FORMS'])
test = '{}-{}-{}'.format(prefix, n-1, testinpost)
#print(test)
test = data.get( test, empty)
except Exception:
test = 'bleagh'
# log the error if it matters enough ...
if test != empty:
data[ prefix + '-TOTAL_FORMS'] = n + 1
# now the usual formset processing ...
formset = HS_formset( data, prefix=prefix)
# other_form = OtherForm( request.POST)
if formset.is_valid():
...
Solution 4
I use RegEx in my Vue.js method:
addForm: function () {
this.count++
let form_count = this.count
form_count++
let formID = 'id_form-' + this.count
incremented_form = this.vue_form.replace(/form-\d/g, 'form-' + this.count)
this.formList.push(incremented_form)
this.$nextTick(() => {
let total_forms = document.getElementsByName('form-TOTAL_FORMS').forEach
(function (ele, idx) {
ele.value = form_count
})
})
},
delForm: function () {
if (this.count != 0) {
this.count--
let form_count = this.count
form_count++
let formID = 'id_form-' + this.count
this.formList.pop()
this.$nextTick(() => {
let total_forms = document.getElementsByName('form-TOTAL_FORMS').forEach
(function (ele, idx) {
ele.value = form_count
})
})
}
else return
},
Related videos on Youtube
Comments
-
mpen over 3 years
This is roughly what I'm trying to do:
def post(request): VehicleFormSet = formset_factory(StaffVehicleForm) if request.method == 'POST': vehicle_formset = VehicleFormSet(request.POST) if 'add_vehicle' in request.POST: if vehicle_formset.is_valid(): form_count = vehicle_formset.total_form_count() vehicle_formset.forms.append(vehicle_formset._construct_form(form_count))
Basically, if a user clicks the "Add" button and their entry is valid, I want to add another blank form to the formset, and hide the previous one.
The problem with the code above is that I can't figure out how to increase
total_form_count()
. The way I have it now, it will work once, and then if you press it again, nothing will happen, presumably becauseform_count
is the same. I also don't like calling_construct_form
and relying on the internals. -
Reiner Gerecke about 13 yearsThe
empty_form
attribute onBaseFormSet
might be of use here. docs.djangoproject.com/en/1.2/topics/forms/formsets/#empty-form "returns a form instance with a prefix of__prefix__
for easier use in dynamic forms with JavaScript" You could keep that as a template to clone from I think. -
Humphrey about 13 yearsOh good tip Reiner Gerecke! I will definitely use that next time :-)
-
Humphrey about 13 yearsis_bound should always be False unless your form is bound to submitted POST (or get) data.
-
mpen about 13 years@Humphrey: Yes... and it is. The entire formset is. I don't want the blank yet-to-be-added forms to be bound though.
-
mpen about 13 years@Reiner: The empty_form does not respect the form field defaults if the formset is bound... a rather annoying bug (the form will be completely blank, rather than contain the default values).
-
Shailen over 10 yearsHow can I call my view and ask it to send me an additional form, because my form contains a couple of dropdown fields with a lot of database-bound options? Can you tell us how you used AJAX to fetch a row from a custom view? Thanks!
-
Shailen over 10 yearsThis helped me: stackoverflow.com/questions/501719/…
-
CrazyGeek over 6 years@mpen How should I call this method and from where will I get TOTAL_FORM_COUNT and other variables.
-
mpen over 6 years@CrazyGeek Dunno. I wrote this 6 years ago. Sorry
-
raratiru over 4 yearsThank you for giving a time hint, sometimes I feel the hours are endless for a few lines and that worried me!