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>
...
{{ itemFormSet.management_form }}
<tr id="templateItemRow" class="invoiceItemRow" style="display: none;">
<td><strong>Item:</strong></td>
<td>
{{ itemFormSet.empty_form.id }}
{{ itemFormSet.empty_form.description }}
{{ itemFormSet.empty_form.description.errors }}</td>
<td class="price">£{{ itemFormSet.empty_form.price }} {{ itemFormSet.empty_form.price.errors }}</td></tr>
{% for item in itemFormSet.forms %}
<tr class="invoiceItemRow">
<td><strong>Item:</strong></td>
<td>
{{ item.id }}
{{ item.description }}
{{ item.description.errors }}</td>
<td class="price">£{{ item.price }} {{ item.price.errors }}</td></tr>
{% endfor %}
...
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