Skip to content

Instantly share code, notes, and snippets.

@vandorjw
Created August 17, 2015 17:28
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save vandorjw/f884f0d51db3e7caaecd to your computer and use it in GitHub Desktop.
Save vandorjw/f884f0d51db3e7caaecd to your computer and use it in GitHub Desktop.
jquery.formset.js
;(function($) {
$.fn.formset = function(opts)
{
var options = $.extend({}, $.fn.formset.defaults, opts),
flatExtraClasses = options.extraClasses.join(' '),
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
childElementSelector = 'input,select,textarea,label,div',
$$ = $(this),
applyExtraClasses = function(row, ndx) {
if (options.extraClasses) {
row.removeClass(flatExtraClasses);
row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
}
},
updateElementIndex = function(elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
replacement = prefix + '-' + ndx + '-';
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
},
hasChildElements = function(row) {
return row.find(childElementSelector).length > 0;
},
showAddButton = function() {
return maxForms.length == 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0))
},
insertDeleteLink = function(row) {
if (row.is('TR')) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>');
} else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>');
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
}
row.find('a.' + options.deleteCssClass).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]'),
buttonRow = row.siblings("a." + options.addCssClass + ', .' + options.formCssClass + '-add'),
forms;
if (del.length) {
// We're dealing with an inline formset.
// Rather than remove this form from the DOM, we'll mark it as deleted
// and hide it, then let Django handle the deleting:
del.val('on');
row.hide();
forms = $('.' + options.formCssClass).not(':hidden');
} else {
row.remove();
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
}
for (var i=0, formCount=forms.length; i<formCount; i++) {
// Apply `extraClasses` to form rows so they're nicely alternating:
applyExtraClasses(forms.eq(i), i);
if (!del.length) {
// Also update names and IDs for all child controls (if this isn't
// a delete-able inline formset) so they remain in sequence:
forms.eq(i).find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, i);
});
}
}
// Check if we need to show the add button:
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) options.removed(row);
return false;
});
};
$$.each(function(i) {
var row = $(this),
del = row.find('input:checkbox[id $= "-DELETE"]');
if (del.length) {
// If you specify "can_delete = True" when creating an inline formset,
// Django adds a checkbox to each form in the formset.
// Replace the default checkbox with a hidden field:
if (del.is(':checked')) {
// If an inline formset containing deleted forms fails validation, make sure
// we keep the forms hidden (thanks for the bug report and suggested fix Mike)
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />');
row.hide();
} else {
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
}
// Hide any labels associated with the DELETE checkbox:
$('label[for="' + del.attr('id') + '"]').hide();
del.remove();
}
if (hasChildElements(row)) {
row.addClass(options.formCssClass);
if (row.is(':visible')) {
insertDeleteLink(row);
applyExtraClasses(row, i);
}
}
});
if ($$.length) {
var hideAddButton = !showAddButton(),
addButton, template;
if (options.formTemplate) {
// If a form template was specified, we'll clone it to generate new form instances:
template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
template.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, '__prefix__');
});
insertDeleteLink(template);
} else {
// Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
template.find(childElementSelector).not(options.keepFieldValues).each(function() {
var elem = $(this);
// If this is a checkbox or radiobutton, uncheck it.
// This fixes Issue 1, reported by Wilson.Andrew.J:
if (elem.is('input:checkbox') || elem.is('input:radio')) {
elem.attr('checked', false);
} else {
elem.val('');
}
});
}
// FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template;
if ($$.attr('tagName') == 'TR') {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
.addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a');
} else {
// Otherwise, insert it immediately after the last form:
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
}
addButton.click(function() {
var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this);
applyExtraClasses(row, formCount);
row.insertBefore(buttonRow).show();
row.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, formCount);
});
totalForms.val(formCount + 1);
// Check if we've exceeded the maximum allowed number of forms:
if (!showAddButton()) buttonRow.hide();
// If a post-add callback was supplied, call it with the added form:
if (options.added) options.added(row);
return false;
});
}
return $$;
}
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: 'form', // The form prefix for your django formset
formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: 'add-row btn btn-success btn-block', // CSS class applied to the add link
deleteCssClass: 'hidden', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added
removed: null // Function called each time a form is deleted
};
})(jQuery)
@dunkinman
Copy link

This code works great, but I've recently stumbled across a bug wherein if the formset row has a foreign key in update mode, and extra is set to 0, the foreign keys are displayed when you click on the add button. I looked through the code and it's a bit over my head to try to understand why this is happening. Is anyone able to look at this and see why the foreign keys are not blank if someone clicks on the add button for an existing row in a formset?

@ItsmeFeddi
Copy link

Hi there, this is a great help to new developers like me, I have applied the code all is working fine except the delete button, i cant understand why its not fuctioning, can you help me out in this matter,

i am working on a django app and in this formset i have set can_delete = True but still its not deleting rows not in html neither at backend.

@quimdev19
Copy link

@ItsmeFeddi It's because the management form values are not changed correctly. Same happened to me. the value "productshippingcost-TOTAL_FORMS" should not change when click delete, because then it will not be sent.
This code is somewhat outdated, you'll need to change some parts.

@quimdev19
Copy link

@dunkinman is because you are probably duplicating the last row of the formset, so it will take the current value of the last form in the formset.

@vandorjw
Copy link
Author

I didn't realize people are still using this. 👍

There is an updated version that doesn't rely on jQuery.

I've adapted it below to work for newer django project (as well as .net). (I believe INITIAL_FORM_COUNT is simply called FORM_COUNT these days.)

https://gist.github.com/vandorjw/4294a6cd37ea1b861eb31f9e2322d68e


Much better documentation here: https://schinckel.net/2017/02/05/django-dynamic-formsets/ . He has written a bunch more quality articles that are worth reading.

I am also a big fan of James Bennet's weblog here: https://www.b-list.org/weblog/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment