Server-Side Form Validation

Django Form's two-way data-binding with an AngularJS controller

New in version 2.0 This example shows how to reflect Django's form data into an AngularJS controller.


Please enter a valid email address
Allowed date format: yyyy-mm-dd
Choose one or more carriers
Must choose at least one type of notification
The password is "secret"
 
Scope:
subscribe_data = {{ subscribe_data | json }}

How does it work?

When working with Django Forms and AngularJS, it is a common use case to upload the form's model data to an endpoint on the server. If the directive ng-model is added to an input field, then, thanks to AngularJS's two-way databinding, the scope object contains a copy of the actual input field's content.

Auto-adding ng-model to forms

If a form inherits from the mixin class NgModelFormMixin, then django-angular renders each field with the directive ng-model="fieldname". To prevent polluting the scope's namespace, it is common practice to encapsulate all the form fields into a separate JavaScript object. The name of this encapsulating object can be set during the form definition, using the class member scope_prefix. Furthermore, set the class member form_name to a different name. If omitted, the mixin class will invent a unique form name for you. The form_name must be different from scope_prefix, because AngularJS's internal form controller adds its own object to the scope, and this objects is named after the form. To prevent confusion, name your forms ending in something such as …_form, whereas the form's models shall be kept inside an object ending in …_data.

In this example, an additional server-side validations has been added to the form: The method clean() rejects the combination of “John” and “Doe” for the first- and last name respectively. Errors for a failed form validation are send back to the client and displayed on top of the form.

Using directive djng-endpoint

We must inform the client where we want the form data to be sent. For this purpose, django-angular offers the attribute directive <form djng-endpoint="/path/to/endpoint">. This is the Ajax's counterpart of the action="…" form attribute.

Error responses

Form data submitted by Ajax, is validated by the server using the same functionality as if it would have been submitted using the classic application/x-www-form-urlencoded POST submission. Errors detected during such a validation, are sent back to the client using a JSON object. This object for example, is structured such as: {"formname": {"errors": {"fieldname1": ["This field is required."]}, "__all__": ["The combination of username and password is not correct."]}}}. Just as in Django, the __all__ is used for form errors not associated with a certain field. Such an error, typically is rendered on top of the form.

Populating the form

Sometimes it is desirable to use an Ajax request to prefill all or a subset of the form fields with values. Whenever the Django endpoint adds an object such as {"formname": {"models": {"fieldname1": "Some value", "fieldname2": "Other value"}}} to the response object, then the django-angular form-controller adds those values to the form's input fields.

Forms Submission

The submit button(s) must be placed anywhere inside the <form>…</form> element. 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.

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

from django.core.exceptions import ValidationError
from django.forms import widgets

from djng.forms import fields, NgModelFormMixin
from djng.styling.bootstrap3.forms import Bootstrap3Form

def validate_password(value):
    # Just for demo. Do not validate passwords like this!
    if value != 'secret':
        raise ValidationError('The password is wrong.')

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

    CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'),
                         ('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')]
    TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'),
                     ('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')]
    NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')]

    first_name = fields.CharField(label='First name', min_length=3, max_length=20)

    last_name = fields.RegexField(
        r'^[A-Z][a-z -]?',
        label='Last name',
        error_messages={'invalid': 'Last names shall start in upper case'})

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

    email = fields.EmailField(
        label='E-Mail',
        required=True,
        help_text='Please enter a valid email address')

    subscribe = fields.BooleanField(
        label='Subscribe Newsletter',
        initial=False, required=False)

    phone = fields.RegexField(
        r'^\+?[0-9 .-]{4,25}$',
        label='Phone number',
        error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'})

    birth_date = fields.DateField(
        label='Date of birth',
        widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}),
        help_text='Allowed date format: yyyy-mm-dd')

    continent = fields.ChoiceField(
        label='Living on continent',
        choices=CONTINENT_CHOICES,
        error_messages={'invalid_choice': 'Please select your continent'})

    weight = fields.IntegerField(
        label='Weight in kg',
        min_value=42,
        max_value=95,
        error_messages={'min_value': 'You are too lightweight'})

    height = fields.FloatField(
        label='Height in meters',
        min_value=1.48,
        max_value=1.95,
        step=0.05,
        error_messages={'max_value': 'You are too tall'})

    traveling = fields.MultipleChoiceField(
        label='Traveling by',
        choices=TRAVELLING_BY,
        help_text='Choose one or more carriers',
        required=True)

    notifyme = fields.MultipleChoiceField(
        label='Notify by',
        choices=NOTIFY_BY,
        widget=widgets.CheckboxSelectMultiple, required=True,
        help_text='Must choose at least one type of notification')

    annotation = fields.CharField(
        label='Annotation',
        required=True,
        widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'}))

    agree = fields.BooleanField(
        label='Agree with our terms and conditions',
        initial=False,
        required=True)

    password = fields.CharField(
        label='Password',
        widget=widgets.PasswordInput,
        validators=[validate_password],
        help_text='The password is "secret"')

    confirmation_key = fields.CharField(
        max_length=40,
        required=True,
        widget=widgets.HiddenInput(),
        initial='hidden value')

    def clean(self):
        if self.cleaned_data.get('first_name', '').lower() == 'john' \
            and self.cleaned_data.get('last_name', '').lower() == 'doe':
            raise ValidationError('The full name "John Doe" is rejected by the server.')
        return super(SubscribeForm, self).clean()
import json
from django.http import JsonResponse
from django.core.urlresolvers import reverse_lazy
from django.utils.encoding import force_text
from django.views.generic.edit import FormView

class SubscribeView(FormView):
    template_name = 'model-scope.html'
    form_class = SubscribeForm
    success_url = reverse_lazy('form_data_valid')

    def post(self, request, **kwargs):
        if request.is_ajax():
            return self.ajax(request)
        return super(SubscribeView, self).post(request, **kwargs)

    def ajax(self, request):
        request_data = json.loads(request.body)
        form = self.form_class(data=request_data.get(self.form_class.scope_prefix, {}))
        if form.is_valid():
            return JsonResponse({'success_url': force_text(self.success_url)})
        else:
            return JsonResponse({form.form_name: form.errors}, 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>

<form name="{{ form.form_name }}" method="post" action="." djng-endpoint="." ng-model-options="{allowInvalid: true}" novalidate>
    {% csrf_token %}
    {{ form.as_div }}
    <button type="submit" class="btn btn-primary">Submit via Post</button>
    <button type="button" ng-click="do(update()).then(redirectTo())" class="btn btn-primary">Submit via Ajax</button>
</form>

Note: The AngularJS app is configured inside Django's HTML template code, since template tags, such as {​{ csrf_token }​} can't be expanded in pure JavaScript.

Fork me on GitHub