Auto-expanding django formset with jQuery

Tim Abell · July 7, 2011

Reading time: 2 minutes

As it took me quite a while to get it how I like it, here’s the relevant bits for making a django formset (custom markup in a table), that automatically adds rows (formset forms) client-side / in the browser keeping up as you fill in the form.

Do with the code as you wish, no licence needed.

In the view (.html file server side) I have:

def invoiceEdit(request, invoice_id):
    InlineInvoiceItemsFormSet = inlineformset_factory(Invoice, InvoiceItem, form=DeleteIfEmptyModelForm, formset=DeleteIfEmptyInlineFormSet, can_delete=True, extra=10)
    itemFormSet = InlineInvoiceItemsFormSet()
    return render_to_response('foo/edit.html', {'invoiceForm': invoiceForm, 'itemFormSet': itemFormSet, 'invoice': invoice}, context_instance=RequestContext(request))

In the template I have:

<script type="text/javascript">
 $(function() {

 var initialRows;

 function setupInvoiceFormset() {
  initialRows = parseInt($('#id_invoiceitem_set-INITIAL_FORMS').val());
  // remove all but last two empty rows
  // add handlers to all inputs to automate row adding
  $('.invoiceItemRow :input').blur(resizeInvoiceFormset);

 const targetExtra = 2; // number of extra rows desired

 function resizeInvoiceFormset() {
  // count the blank rows at the end
  var rows = $('.invoiceItemRow').filter(':not(#templateItemRow)');
  var totalRows = rows.length
  var blankRows = countBlankRows(rows);
  var targetRowCount = totalRows - blankRows + targetExtra;
  targetRowCount = Math.max(targetRowCount,initialRows); // don't trim off real rows otherwise delete breaks
  if (totalRows > targetRowCount) {
   // if there too many blank rows remove the extra rows
   rows.slice(targetRowCount).remove(); // negative to strip from ends
  } else if (totalRows < targetRowCount) {
   // add new blank rows to bring the total up to the desired number
   for (var newRowIndex = totalRows; newRowIndex < targetRowCount; newRowIndex++) {
  } else {
  // update the hidden form with the new form count

 function countBlankRows(rows) {
  // count the empty rows from the bottom up, stopping at the first non-blank row
  var blankRows = 0;
  for (var i = rows.length -1; i>=0; i--) {
   if (isEmptyRow(rows[i])) {
   } else {
  return blankRows;

 function isEmptyRow(row) {
  // loop through all the inputs in the row, return true if they are all blank
  // whitespace is ignored
  var inputs = $(row).find(':input').filter(':not(:hidden)');
  for (var j = 0; j < inputs.length; j++) {
   if ($.trim(inputs[j].value).length) {
    return false;
  return true;

 function addRow(newRowIndex) {
  var newRow = $('#templateItemRow').clone(true);
  newRow.removeAttr('id'); //prevent duplicated template row id;
  // replace placeholder with row index
  newRow.find(':input').each(function() {
   $(this).attr("name", $(this).attr("name").replace('__prefix__', newRowIndex));
   $(this).attr("id", $(this).attr("id").replace('__prefix__', newRowIndex));

<tr id="templateItemRow" class="invoiceItemRow" style="display: none;">
 <td class="price">£ </td></tr>


The result is a form that intuitively shrinks/grows as the content is added/removed.

The javascript is of course actually in a separate .js file.


Footnote. You may have noticed the delete-if-empty customisation which I like for usability. References for this at

Share: Tweet | LinkedIn
Suggest improvements: page source on github

Get extra content that's just for my list. Get new blog posts to your inbox.
Join me on my journey through software and business.