Auto-expanding django formset with jQuery

2 minute read (444 words)

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


Tweet This || Post to LinkedIn || Page Source

Subscribe for updates on software development, contracting, side projects, blog posts and who knows what else. Read the archives for an idea of content.

Mailing list powered by the excellent buttondown.email.