Auto-expanding django formset with jQuery

Tim Abell · July 7, 2011

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:

@login_required
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() {
  setupInvoiceFormset();
 });

 var initialRows;

 function setupInvoiceFormset() {
  initialRows = parseInt($('#id_invoiceitem_set-INITIAL_FORMS').val());
  // remove all but last two empty rows
  resizeInvoiceFormset();
  // 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++) {
    addRow(newRowIndex);
   }
  } else {
   return;
  }
  // update the hidden form with the new form count
  $('#id_invoiceitem_set-TOTAL_FORMS').val(targetRowCount);
 }

 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])) {
    blankRows++;
   } else {
    break;
   }
  }
  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.addClass('invoiceItemRow');
  newRow.removeAttr('id'); //prevent duplicated template row id
  newRow.show();
  // 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));
  });
  $('.invoiceItemRow:last').after(newRow);
 }
</script>
...

<tr id="templateItemRow" class="invoiceItemRow" style="display: none;">
 <td><strong>Item:</strong></td>
 <td>
  
  
  </td>
 <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.

References:

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

Share: Tweet | LinkedIn
Suggest improvments: page source on github
If you liked this post then sign up to my mailing list: