Showing posts with label jQuery. Show all posts
Showing posts with label jQuery. Show all posts

07 July 2011

Auto-expanding django formset with jQuery

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>
...
{{ 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

18 May 2011

Reliable javascript checkbox events


Some sites have checkboxes which show/hide another element when you click them. This a handy feature, but not all sites take into account the fact that firefox remembers the contents of a form when you reload the page (this is a good thing).

So here's how you avoid that with jQuery:

<script type="text/javascript">
$(function() {
// initialise show/hide to match the checkbox value
$('.targetelements').toggle($('#mycheckbox').attr('checked'));
// attach click handler for show/hide to checkbox
$('#mycheckbox').click(function(){ $('.targetelements').toggle(this.checked);})
});
</script>

Simples!

You could use the same principle without jQuery if you need to. Simply read the value of the checkbox with javascript the old fashioned way before deciding whether to hide when you initialise you page.