Recently, I created a form (in Django 1.3) where users could add extra rows of form fields as required, similar to how you can add extra reminders in Google Calendar. Patternry calls it an inline input adder. I found it a bit tricky to achieve this, so I thought I’d write about it.
Note: This is only a ‘one-way’ form. That is, you add further extra rows/items and submit the form. You can’t view or edit previously entered rows/items in the form again .
Update, 12 Aug, 2012: Here’s a better, work-in-progress code which you might find useful: dynamicformtodo on GitHub
There are two main steps:
Use JavaScript to dynamically create additional forms (on the client side).
Capture submitted forms on the server side.
Let’s deal with the second part first, because I found this simpler to implement. I’m going to be using a simple todo list application as an example. You can download the example project here.
Server Side Splendour
Django Formsets
Django has the concept of
Formsets that
help when dealing with multiple identical forms on the same page. This will
make capturing submitted forms a cinch.
# forms.py# Change as necessaryfromdynamicform.todo.modelsimport*fromdjango.formsimportModelFormclassTodoListForm(ModelForm):classMeta:model=TodoListclassTodoItemForm(ModelForm):classMeta:model=TodoItemexclude=('list',)
# views.pyfromdjango.shortcutsimportrender_to_responsefromdjango.core.context_processorsimportcsrffromdjango.templateimportRequestContext# For CSRFfromdjango.forms.formsetsimportformset_factory,BaseFormSetfromdjango.httpimportHttpResponse,HttpResponseRedirectfromdynamicform.todo.formsimport*defindex(request):# This class is used to make empty formset forms required# See http://stackoverflow.com/questions/2406537/django-formsets-make-first-required/4951032#4951032classRequiredFormSet(BaseFormSet):def__init__(self,*args,**kwargs):super(RequiredFormSet,self).__init__(*args,**kwargs)forforminself.forms:form.empty_permitted=FalseTodoItemFormSet=formset_factory(TodoItemForm,max_num=10,formset=RequiredFormSet)ifrequest.method=='POST':# If the form has been submitted...todo_list_form=TodoListForm(request.POST)# A form bound to the POST data# Create a formset from the submitted datatodo_item_formset=TodoItemFormSet(request.POST,request.FILES)iftodo_list_form.is_valid()andtodo_item_formset.is_valid():todo_list=todo_list_form.save()forformintodo_item_formset.forms:todo_item=form.save(commit=False)todo_item.list=todo_listtodo_item.save()returnHttpResponseRedirect('thanks')# Redirect to a 'success' pageelse:todo_list_form=TodoListForm()todo_item_formset=TodoItemFormSet()# For CSRF protection# See http://docs.djangoproject.com/en/dev/ref/contrib/csrf/ c={'todo_list_form':todo_list_form,'todo_item_formset':todo_item_formset,}c.update(csrf(request))returnrender_to_response('todo/index.html',c)
And that’s pretty much it for the server-side stuff!
Client Side Coolness
Now for the template with the JavaScript stuff. The main point here is that
whenever a form is added or deleted, the
ManagementForm data is also updated, as well as the
relevant names and IDs of the form fields.
Here’s the JavaScript (note that I’m using jQuery here):
$(document).ready(function(){// Code adapted from http://djangosnippets.org/snippets/1389/ functionupdateElementIndex(el,prefix,ndx){varid_regex=newRegExp('('+prefix+'-\\d+-)');varreplacement=prefix+'-'+ndx+'-';if($(el).attr("for"))$(el).attr("for",$(el).attr("for").replace(id_regex,replacement));if(el.id)el.id=el.id.replace(id_regex,replacement);if(el.name)el.name=el.name.replace(id_regex,replacement);}functiondeleteForm(btn,prefix){varformCount=parseInt($('#id_'+prefix+'-TOTAL_FORMS').val());if(formCount>1){// Delete the item/form$(btn).parents('.item').remove();varforms=$('.item');// Get all the forms // Update the total number of forms (1 less than before)$('#id_'+prefix+'-TOTAL_FORMS').val(forms.length);vari=0;// Go through the forms and set their indices, names and IDsfor(formCount=forms.length;i<formCount;i++){$(forms.get(i)).children().children().each(function(){if($(this).attr('type')=='text')updateElementIndex(this,prefix,i);});}}// End ifelse{alert("You have to enter at least one todo item!");}returnfalse;}functionaddForm(btn,prefix){varformCount=parseInt($('#id_'+prefix+'-TOTAL_FORMS').val());// You can only submit a maximum of 10 todo items if(formCount<10){// Clone a form (without event handlers) from the first formvarrow=$(".item:first").clone(false).get(0);// Insert it after the last form$(row).removeAttr('id').hide().insertAfter(".item:last").slideDown(300);// Remove the bits we don't want in the new row/form// e.g. error messages$(".errorlist",row).remove();$(row).children().removeClass("error");// Relabel or rename all the relevant bits$(row).children().children().each(function(){updateElementIndex(this,prefix,formCount);$(this).val("");});// Add an event handler for the delete item/form link $(row).find(".delete").click(function(){returndeleteForm(this,prefix);});// Update the total form count$("#id_"+prefix+"-TOTAL_FORMS").val(formCount+1);}// End ifelse{alert("Sorry, you can only enter a maximum of ten items.");}returnfalse;}// Register the click event handlers$("#add").click(function(){returnaddForm(this,"form");});$(".delete").click(function(){returndeleteForm(this,"form");});});