Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added options addWrap, deleteWrap, addInsert and formInsert #80

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 93 additions & 60 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,73 +234,106 @@ Formset options
You can customize this plugin's behavior by passing an options hash. A
complete list of available options is shown below::

``prefix``
Use this to specify the prefix for your formset if it's anything
other than the default ("form"). This option must be supplied for
inline formsets.

``addText``
Use this to set the text for the generated add link. The default
text is "add another".

``deleteText``
Use this to set the text for the generated delete links. The
default text is "remove".

``addCssClass``
Use this to change the default CSS class applied to the generated
add link (possibly, to avoid CSS conflicts within your templates).
The default class is "add-row".

``deleteCssClass``
Use this to change the default CSS class applied to the generated
delete links. The default class is "delete-row".

``added``
If you set this to a function, that function will be called each
time a new form is added. The function should take a single argument,
``row``; it will be passed a jQuery object, wrapping the form that
was just added.

``removed``
Set this to a function, and that function will be called each time
a form is deleted. The function should take a single argument,
``row``; it will be passed a jQuery object, wrapping the form that
was just removed.
``prefix``
Use this to specify the prefix for your formset if it's anything
other than the default ("form"). This option must be supplied for
inline formsets.

``addText``
Use this to set the text for the generated add link. The default
text is "add another".

``deleteText``
Use this to set the text for the generated delete links. The
default text is "remove".

``addCssClass``
Use this to change the default CSS class applied to the generated
add link (possibly, to avoid CSS conflicts within your templates).
The default class is "add-row".

``deleteCssClass``
Use this to change the default CSS class applied to the generated
delete links. The default class is "delete-row".

``added``
If you set this to a function, that function will be called each
time a new form is added. The function should take a single argument,
``row``; it will be passed a jQuery object, wrapping the form that
was just added.

``removed``
Set this to a function, and that function will be called each time
a form is deleted. The function should take a single argument,
``row``; it will be passed a jQuery object, wrapping the form that
was just removed.

.. versionadded:: 1.1

``formCssClass``
Use this to set the CSS class applied to all forms within the same
formset. Internally, all forms with the same class are assumed to
belong to the same formset. If you have multiple formsets on a single
HTML page, you MUST provide unique class names for each formset. If
you don't provide a value, this defaults to "dynamic-form".

For more information, see the section on :ref:`Using multiple Formsets
on the same page <using-multiple-formsets>`, and check out the example
in the demo project.
``formCssClass``
Use this to set the CSS class applied to all forms within the same
formset. Internally, all forms with the same class are assumed to
belong to the same formset. If you have multiple formsets on a single
HTML page, you MUST provide unique class names for each formset. If
you don't provide a value, this defaults to "dynamic-form".

For more information, see the section on :ref:`Using multiple Formsets
on the same page <using-multiple-formsets>`, and check out the example
in the demo project.

.. versionadded:: 1.2
``formTemplate``
Use this to override the form that gets cloned, each time a new form
instance is added. If specified, this should be a jQuery selector.

``formTemplate``
Use this to override the form that gets cloned, each time a new form
instance is added. If specified, this should be a jQuery selector.
``extraClasses``
Set this to an array of CSS class names (defaults to an empty array),
and the classes will be applied to each form in the formset in turn.
This can easily be used to acheive row-striping effects, which can
make large formsets easier to deal with visually.

``extraClasses``
Set this to an array of CSS class names (defaults to an empty array),
and the classes will be applied to each form in the formset in turn.
This can easily be used to acheive row-striping effects, which can
make large formsets easier to deal with visually.

.. versionadded:: 1.3

``keepFieldValues``
Set this to a jQuery selector, which should resolve to a list of elements
whose values should be preserved when the form is cloned.
Internally, this value is passed directly to the ``$.not(...)`` method.
This means you can also pass in DOM elements, or a function (in newer
versions of jQuery) as your selector.
``keepFieldValues``
Set this to a jQuery selector, which should resolve to a list of elements
whose values should be preserved when the form is cloned.
Internally, this value is passed directly to the ``$.not(...)`` method.
This means you can also pass in DOM elements, or a function (in newer
versions of jQuery) as your selector.

``addWrap``
This value will be passed to jQuery ``.wrap`` to wrap the add button
in elements for additional styling.

``deleteWrap``
This value will be passed to jQuery ``.wrap`` to wrap the delete button
in elements for additional styling.

``addInsert``
If you set this to a function, that function will take responsibility
for inserting the add button into the page. It should take two
arguments: ``$$``, the original jQuery object you are applying the
formset to; and ``buttonRow``, the button element (or element
containing the button).
It does not need to return anything.

If you do not set ``formInsert`` (see below), the button needs to be
added as a sibling of ``$$``.

This can be used to put the add button at the top of the forms, or
above a hidden ``formTemplate`` to avoid styling issues.

``formInsert``
If you set this to a function, that function will take responsibility
for inserting new forms into the page and showing them (eg with jQuery
``.show()``). It should take three arguments:
``$$``, the original jQuery object you are applying the formset to;
``buttonRow``, the row or element containing the add button; and
``formRow``, the new row or element containing the new form.
It does not need to return anything.

This can be used to add animations to reveal new forms
(eg ``formRow.insertBefore(buttonRow).slideDown();``), to insert new
forms above existing ones, or to use ``addInsert`` (see above) to place
the add button outside the formset.

.. note:: The ``addCssClass`` and ``deleteCssClass`` options must be unique.
Internally, the plugin uses the class names to target the add and delete
Expand Down
71 changes: 52 additions & 19 deletions src/jquery.formset.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
childElementSelector = 'input,select,textarea,label,div',
$$ = $(this),
addButtonRow,

applyExtraClasses = function(row, ndx) {
if (options.extraClasses) {
Expand All @@ -39,8 +40,13 @@
},

showAddButton = function() {
var forms = $$.not('.formset-custom-template'),
isInline = (forms.find('input:hidden[id $= "-DELETE"]').length > 0);
return maxForms.length == 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
maxForms.val() == '' ||
(maxForms.val() - totalForms.val() > 0) ||
(isInline && maxForms.val() - forms.filter(':visible').length > 0)
;
},

insertDeleteLink = function(row) {
Expand All @@ -59,10 +65,9 @@
// last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
}
row.find('a.' + delCssSelector).click(function() {
var delButton = row.find('a.' + delCssSelector).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]'),
buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
forms;
if (del.length) {
// We're dealing with an inline formset.
Expand All @@ -73,6 +78,7 @@
forms = $('.' + options.formCssClass).not(':hidden');
} else {
row.remove();
$$ = $$.not(row);
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
Expand All @@ -89,11 +95,14 @@
}
}
// Check if we need to show the add button:
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
if (addButtonRow.is(':hidden') && showAddButton()) addButtonRow.show();
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) options.removed(row);
return false;
});
if (options.deleteWrap) {
delButton.wrap(options.deleteWrap);
}
};

$$.each(function(i) {
Expand Down Expand Up @@ -125,8 +134,7 @@
});

if ($$.length) {
var hideAddButton = !showAddButton(),
addButton, template;
var 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);
Expand Down Expand Up @@ -158,30 +166,51 @@
if ($$.is('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');
var numCols = $$.eq(0).children().length; // This is a bit of an assumption :|
addButtonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
.addClass(options.formCssClass + '-add');
if (options.addInsert) {
options.addInsert($$, addButtonRow);
} else {
$$.parent().append(addButtonRow);
}
addButton = addButtonRow.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 = $('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
addButtonRow = addButton;
if (options.addInsert) {
options.addInsert($$, addButton);
} else {
addButton.insertAfter($$.filter(':last'));
}
}
if (options.addWrap) {
// Wrap the addButton
addButton.wrap(options.addWrap);
// Find the wrapper's container by wrapping an empty element
// outside the DOM and finding how many parents it has
addButtonRow = addButton.parents().eq(
$('<div/>').wrap(options.addWrap).parents().length - 1
);
}
if (!showAddButton()) addButtonRow.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);
row = options.formTemplate.clone(true).removeClass('formset-custom-template');
applyExtraClasses(row, formCount);
row.insertBefore(buttonRow).show();
if (options.formInsert) {
options.formInsert($$, addButtonRow, row);
} else {
row.insertBefore(addButtonRow).show();
}
$$ = $$.add(row);
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 (!showAddButton()) addButtonRow.hide();
// If a post-add callback was supplied, call it with the added form:
if (options.added) options.added(row);
return false;
Expand All @@ -199,6 +228,10 @@
deleteText: 'remove', // Text for the delete link
addCssClass: 'add-row', // CSS class applied to the add link
deleteCssClass: 'delete-row', // CSS class applied to the delete link
addWrap: null, // Argument for jQuery .wrap for add link
deleteWrap: null, // Argument for jQuery .wrap for delete link
addInsert: null, // Function called to insert the add link
formInsert: null, // Function called to insert a new form
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
Expand Down
72 changes: 72 additions & 0 deletions tests/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
assert.equal($.fn.formset.defaults.deleteText, 'remove', 'deleteText: "remove"');
assert.equal($.fn.formset.defaults.addCssClass, 'add-row', 'addCssClass: "add-row"');
assert.equal($.fn.formset.defaults.deleteCssClass, 'delete-row', 'deleteCssClass: "delete-row"');
assert.equal($.fn.formset.defaults.addWrap, null, 'addWrap: null');
assert.equal($.fn.formset.defaults.deleteWrap, null, 'deleteWrap: null');
assert.equal($.fn.formset.defaults.addInsert, null, 'addInsert: null');
assert.equal($.fn.formset.defaults.formInsert, null, 'formInsert: null');
assert.equal($.fn.formset.defaults.formCssClass, 'dynamic-form', 'formCssClass: "dynamic-form"');
assert.deepEqual($.fn.formset.defaults.extraClasses, [], 'extraClasses: []');
assert.equal($.fn.formset.defaults.keepFieldValues, '', 'keepFieldValues: ');
Expand Down Expand Up @@ -178,4 +182,72 @@
assert.equal($('#id_form-TOTAL_FORMS').val(), '0', 'Updated "Total Forms" count.');
assert.equal($('#stacked-form div').size(), 0, 'Removed form.');
});


module('Basic Formset Tests', {
setup: function () {
$('#stacked-form div').formset({
addCssClass: 'btn-add',
deleteCssClass: 'btn-delete',
// Use spans to avoid clashing with #stacked-form divs
addWrap: '<span class="add-wrap"></span>',
deleteWrap: '<span class="delete-wrap"></span>'
});
}
});

test('Test Form Addition With addWrap', function (assert) {
var $btn = $('#stacked-form .btn-add');
assert.equal($('#id_form-TOTAL_FORMS').val(), '1', 'Default form is present.');
assert.ok($btn.hasClass('btn-add'), 'Add button has class "btn-add" applied to it.');
assert.ok($btn.parent().is('span'), 'Add button is wrapped in "span".');
assert.ok($btn.parent().hasClass('add-wrap'), 'Add button is wrapped in "span.add-wrap".');
$btn.trigger('click');
assert.equal($('#id_form-TOTAL_FORMS').val(), '2', 'Updated "Total Forms" count.');
assert.equal($('#stacked-form div').size(), 2, 'Added new form.');
});

test('Test Form Removal With deleteWrap', function (assert) {
var $btn = $('#stacked-form .btn-delete');
assert.equal($('#id_form-TOTAL_FORMS').val(), '1', 'Default form is present.');
assert.ok($btn.hasClass('btn-delete'), 'Remove button has class "btn-delete" applied to it.');
assert.ok($btn.parent().is('span'), 'Remove button is wrapped in "span".');
assert.ok($btn.parent().hasClass('delete-wrap'), 'Remove button is wrapped in "span.delete-wrap".');
$btn.trigger('click');
assert.equal($('#id_form-TOTAL_FORMS').val(), '0', 'Updated "Total Forms" count.');
assert.equal($('#stacked-form div').size(), 0, 'Removed form.');
});


module('Basic Formset Tests', {
setup: function () {
$('#stacked-form div').formset({
addCssClass: 'btn-add',
// Test by adding to the top instead of bottom
addInsert: function ($$, addButton) {
$$.parent().prepend(addButton);
},
formInsert: function ($$, buttonRow, formRow) {
formRow.insertAfter(buttonRow).show();
}
});
}
});

test('Test Form Addition With custom addInsert and formInsert', function (assert) {
var idRegex = /id_form-(\d+)-(\w+)/,
$btn = $('#stacked-form .btn-add');
assert.equal($('#id_form-TOTAL_FORMS').val(), '1', 'Default form is present.');
assert.equal($('#stacked-form div').size(), 1, 'Default form is div.');
assert.ok($btn.hasClass('btn-add'), 'Add button has class "btn-add" applied to it.');
assert.equal($('#stacked-form :first')[0], $btn[0], 'Add button is at top.');

assert.equal($('#stacked-form div:first label').attr('for').match(idRegex)[1], 0, 'Default form is first form.');
$btn.trigger('click');
assert.equal($('#id_form-TOTAL_FORMS').val(), '2', 'Updated "Total Forms" count.');
assert.equal($('#stacked-form div').size(), 2, 'Added new form.');
assert.equal($('#stacked-form div:first label').attr('for').match(idRegex)[1], 1, 'Added form is first form.');
assert.equal($('#stacked-form :first')[0], $btn[0], 'Add button is still at top.');
});

}(jQuery));