Skip to content

Commit

Permalink
- Added support for the MAX_NUM_FORMS parameter, added to the managem…
Browse files Browse the repository at this point in the history
…ent form in Django 1.2

- Fixed the autocomplete example
- Switched to Google's Closure compiler for minification

git-svn-id: https://django-dynamic-formset.googlecode.com/svn/trunk@14 9f2ace40-7153-11de-83e1-4fc93b4a6815
  • Loading branch information
stan.madueke committed Jul 4, 2010
1 parent 84851a3 commit 1894c1b
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 49 deletions.
2 changes: 2 additions & 0 deletions demo/example/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class ContactInfoForm(forms.Form):
preferred = fields.BooleanField(required=False)

ContactFormset = formsets.formset_factory(ContactInfoForm)
# Define a formset, which will allow a maximum of 5 contacts, no more:
MaxFiveContactsFormset = formsets.formset_factory(ContactInfoForm, extra=5, max_num=5)
# Define the same formset, with no forms (so we can demo the form template):
EmptyContactFormset = formsets.formset_factory(ContactInfoForm, extra=0)

Expand Down
3 changes: 2 additions & 1 deletion demo/example/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.conf.urls.defaults import *
from example.forms import AutoCompleteOrderedItemForm, OrderedItemForm, ContactFormset, EmptyContactFormset, EventFormset
from example.forms import AutoCompleteOrderedItemForm, OrderedItemForm, ContactFormset, MaxFiveContactsFormset, EmptyContactFormset, EventFormset

urlpatterns = patterns('example.views',
url(r'^stacked/$', 'formset', {'formset_class': ContactFormset, 'template': 'example/formset-stacked.html'}, name='example_stacked'),
url(r'^table/$', 'formset', {'formset_class': ContactFormset, 'template': 'example/formset-table.html'}, name='example_table'),
url(r'^max-forms/$', 'formset', {'formset_class': MaxFiveContactsFormset, 'template': 'example/max-forms.html'}, name='example_max_forms'),
url(r'^form-template/$', 'formset_with_template', {'formset_class': EmptyContactFormset, 'template': 'example/form-template.html'}, name='example_form_template'),
url(r'^admin-widget/$', 'formset', {'formset_class': EventFormset, 'template': 'example/formset-admin-widget.html'}, name='example_admin_widget'),
url(r'^multiple-formsets/$', 'multiple_formsets', {'template': 'example/formset-multiple-formsets.html'}, name='example_multiple_formsets'),
Expand Down
84 changes: 61 additions & 23 deletions demo/static/js/jquery.formset.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// ==ClosureCompiler==
// @output_file_name jquery.formset.min.js
// @compilation_level SIMPLE_OPTIMIZATIONS
// ==/ClosureCompiler==

/**
* jQuery Formset 1.1
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
Expand All @@ -14,6 +19,8 @@
{
var options = $.extend({}, $.fn.formset.defaults, opts),
flatExtraClasses = options.extraClasses.join(' '),
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
$$ = $(this),

applyExtraClasses = function(row, ndx) {
Expand All @@ -35,6 +42,11 @@
return row.find('input,select,textarea,label').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
Expand All @@ -51,26 +63,35 @@
}
row.find('a.' + options.deleteCssClass).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]');
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:
// 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 form count.
// Also update names and IDs for all remaining form controls so they remain in sequence:
var forms = $('.' + options.formCssClass).not('.formset-custom-template');
$('#id_' + options.prefix + '-TOTAL_FORMS').val(forms.length);
for (var i=0, formCount=forms.length; i<formCount; i++) {
applyExtraClasses(forms.eq(i), i);
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
}
// Apply extraClasses to form rows so they're nicely alternating.
// Also update names and IDs for all child controls, if this isn't a delete-able
// inline formset, so they remain in sequence.
for (var i=0, formCount=forms.length; i<formCount; i++) {
applyExtraClasses(forms.eq(i), i);
if (!del.length) {
forms.eq(i).find('input,select,textarea,label').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;
Expand All @@ -84,22 +105,34 @@
// 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:
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
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)) {
insertDeleteLink(row);
row.addClass(options.formCssClass);
applyExtraClasses(row, i);
if (row.is(':visible')) {
insertDeleteLink(row);
applyExtraClasses(row, i);
}
}
});

if ($$.length) {
var addButton, template;
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).addClass('formset-custom-template');
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
template.find('input,select,textarea,label').each(function() {
updateElementIndex($(this), options.prefix, 2012);
});
Expand All @@ -126,25 +159,30 @@
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;
$$.parent().append('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>');
addButton = $$.parent().find('tr:last a');
addButton.parents('tr').addClass(options.formCssClass + '-add');
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($('#id_' + options.prefix + '-TOTAL_FORMS').val()),
var formCount = parseInt(totalForms.val(), 10),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
buttonRow = $(this).parents('tr.' + options.formCssClass + '-add').get(0) || this;
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this);
applyExtraClasses(row, formCount);
row.insertBefore($(buttonRow)).show();
row.insertBefore(buttonRow).show();
row.find('input,select,textarea,label').each(function() {
updateElementIndex($(this), options.prefix, formCount);
});
$('#id_' + options.prefix + '-TOTAL_FORMS').val(formCount + 1);
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;
Expand Down
5 changes: 3 additions & 2 deletions demo/templates/example/inline-formset-autocomplete.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// ('form') isn't correct.
// Django appears to generate the prefix from the lowercase plural
// name of the related model, with camel-case converted to underscores.
prefix: 'ordered_items',
prefix: '{{ formset.prefix }}',
added: function(row) {
var txt = row.find('.autocomplete-me');
// Event handlers are cloned with each new form, so we first unbind
Expand Down Expand Up @@ -47,7 +47,8 @@ <h2>Order details</h2>
{% for form in formset.forms %}
<tr id="{{ form.prefix }}-row">
<td>
{{ form.order.as_hidden }}
{% for fld in form.hidden_fields %}{{ fld }}{% endfor %}
{% if form.instance.pk %}{{ form.DELETE }}{% endif %}
{{ form.product }}
</td>
<td>{{ form.quantity }}</td>
Expand Down
73 changes: 73 additions & 0 deletions demo/templates/example/max-forms.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{% extends "base.html" %}

{% block title %}Basic Formset (Table layout){% endblock %}

{% block extrahead %}
<script type="text/javascript">
$(function() {
$('#id_contact_info_table tbody tr').formset({
extraClasses: ['row1', 'row2', 'row3']
})
})
</script>
<!-- Here's an example of how you can style add/delete buttons with CSS -->
<style type="text/css">
.add-row {
padding-left:18px;
background:url({{ MEDIA_URL }}images/add.png) no-repeat left center;
}
.delete-row {
float:right;
display:block;
margin:5px 0 0 5px;
text-indent:-6000px;
background:url({{ MEDIA_URL }}images/delete.png) no-repeat left center;
width:16px;
height:16px;
}
tr.row1 td { background-color: #f9f9f9; }
tr.row2 td { background-color: #f3f3f3; }
tr.row3 td { background-color: #ededed; }
</style>
{% endblock %}

{% block content %}
<div>
<div class="entry">
<form method="post" action="">
<table id="id_contact_info_table" border="0" cellpadding="0" cellspacing="5">
<thead>
<tr>
<th scope="col">Preferred</th>
<th scope="col">Type</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
{% for form in formset.forms %}
<tr id="{{ form.prefix }}-row">
<td style="text-align:center;">{{ form.preferred }}</td>
<td>{{ form.type }}</td>
<td>{{ form.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>
{{ formset.management_form }}
<input type="submit" value="Submit" />
</p>
</form>
</div>
</div>
{% endblock %}

{% block sidebar %}
<p>
Django 1.2 added a "MAX_NUM_FORMS" field to the management form; its value maps to the value passed in "max_num"
to the formset factory function.
</p>
<p>
If you're running Django 1.2, you won't be able to have more than 5 forms in this formset.
</p>
{% endblock %}
1 change: 1 addition & 0 deletions demo/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h1 class="title">Examples</h1>
<li><a href="{% url example_admin_widget %}">Basic Formset with Admin Widget</a></li>
<li><a href="{% url example_multiple_formsets %}">Multiple formsets on the same page</a></li>
<li><a href="{% url example_form_template %}">Custom Form Template</a></li>
<li><a href="{% url example_max_forms %}">Limit allowed forms</a></li>
</ul>
</div>
</div>
Expand Down
Loading

0 comments on commit 1894c1b

Please sign in to comment.