Django's Forms Set

New in 2.0 How to validate a Set of Forms

This example shows how to validate multiple Forms inside an AngularJS application.

Subscribe Form
Must consist of a first- and a last name
Address Form
djng-forms-set's scope:
subscribe_data = {{ subscribe_data | json }}
address_data = {{ address_data | json }}

How does it work?

In component based web development, it is quite common to arrange more than one form on the same page. As opposed to form submissions via application/x-www-form-urlencoded or multipart/form-data, we can, thanks to Ajax, submit the content of more than one form using a single HTTP-request. This requires to dispatch the submitted data on the server to each form class, but if we prefix them with unique identifiers (using scope_prefix), that's a no-brainer.

Directive djng-forms-set

To achieve this, we can reuse the same Form mixin classes as we have used in the previous examples. The main difference is that we must wrap the set of forms into the AngularJS directive, <djng-forms-set endpoint="/some/endpoint">...</djng-forms-set>. Inside this directive, we render the forms as usual, using {⁠{ form.as_div }⁠}.

Forms Submission

The submit button(s) now can be placed outside of the <form>...</form> element. This allows us to submit the content from multiple forms altogether. However, we must specify the common endpoint to accept our form submissions; this is, as you might have expected, the attribute endpoint="/some/endpoint" in our forms wrapping directive. To send the form's content to the server, add ng-click="do(update())" to the submission button. We have to start this expression with do(...), in order to emulate the first promise (see below for a longer explanation).

By itself, this however would not invoke any further action on the client. We therefore must tell our directive, what we want to do next. For this, the django-angular's button directive offers a few prepared targets, which can be chained. If we change the above to ng-click="do(update()).then(reloadPage())", then after a successful submission the current page is reloaded.

For further options on how to chain actions, please refer to the previous chapter.

Forms Validation

All forms wrapped inside our djng-forms-set directive, are validated. This shall be used to prevent submitting data, if at least one of the forms does not validate. For this, just add ng-disabled="isDisabled()" to the submission button.

import re
from django.core.exceptions import ValidationError
from django.forms import widgets
from djng.forms import fields, NgModelFormMixin, NgFormValidationMixin
from djng.styling.bootstrap3.forms import Bootstrap3Form

def validate_full_name(value):
    pattern = re.compile(r'^\S+\s+\S+')
    if not pattern.match(value):
        raise ValidationError("Please enter a first-, followed by a last name.")

class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form):
    scope_prefix = 'subscribe_data'
    form_name = 'subscribe_form'
    use_required_attribute = False

    sex = fields.ChoiceField(
        choices=[('m', 'Male'), ('f', 'Female')],
        widget=widgets.RadioSelect,
        error_messages={'invalid_choice': 'Please select your sex'},
    )

    full_name = fields.CharField(
        label='Full name',
        validators=[validate_full_name],
        help_text='Must consist of a first- and a last name',
    )

    def clean(self):
        if self.cleaned_data.get('full_name', '').lower() == 'john doe':
            raise ValidationError('The full name "John Doe" is rejected by the server.')
        return super(SubscribeForm, self).clean()

class AddressForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form):
    scope_prefix = 'address_data'
    form_name = 'address_form'
    use_required_attribute = False

    street_name = fields.CharField(label='Street name')
import json
from django.http import JsonResponse
from django.views.generic import TemplateView
from django.core.urlresolvers import reverse_lazy

class SubscribeView(TemplateView):
    template_name = 'forms-set.html'
    success_url = reverse_lazy('form_data_valid')

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        context['subscribe_form'] = SubscribeForm()
        context['address_form'] = AddressForm()
        return self.render_to_response(context)

    def put(self, request, *args, **kwargs):
        request_data = json.loads(request.body)
        subscribe_form = SubscribeForm(data=request_data.get(SubscribeForm.scope_prefix, {}))
        address_form = AddressForm(data=request_data.get(AddressForm.scope_prefix, {}))
        response_data = {}

        if subscribe_form.is_valid() and address_form.is_valid():
            response_data.update({'success_url': self.success_url})
            return JsonResponse(response_data)

        # otherwise report form validation errors
        response_data.update({
            subscribe_form.form_name: subscribe_form.errors,
            address_form.form_name: address_form.errors,
        })
        return JsonResponse(response_data, status=422)
<script type="text/javascript">
  angular.module('djangular-demo', ['djng.forms']).config(function($httpProvider) {
    $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
    $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token }}';
  });
</script>

<djng-forms-set endpoint="/form_sets/">

  <form name="subscribe_form" novalidate>
    <fieldset>
      <legend>Subscribe Form</legend>
      {{ subscribe_form.as_div }}
    </fieldset>
  </form>

  <form name="address_form" novalidate>
    <fieldset>
      <legend>Address Form</legend>
      {{ address_form.as_div }}
    </fieldset>
  </form>

  <button ng-click="do(update()).then(reloadPage())" type="button">
    Forced Submission <i class="some-icon"></i>
  </button>

  <button ng-click="do(update()).then(redirectTo())" ng-disabled="isDisabled()" type="button">
    Validated Submission <i class="some-icon"></i>
  </button>

</djng-forms-set>
Fork me on GitHub