Client-Side Form Validation

Form validation with AngularJS using only a Django Form

This example shows how to let AngularJS validate input fields from a Django Form in a DRY manner.


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"

Submission using a POST request:

Test submission of invalid POST data:


How does it work?

The Django forms.Form class offers many possibilities to validate a given form. This for obvious reasons is done on the server. However, while typing, it is unacceptable to send the form's content to the server for continuous validation. Therefore, adding client side form validation is a good idea and commonly used. But since this validation easily can be bypassed by a malicious client, the same validation has to occur a second time, when the server receives the form's data for final processing.

With django-angular, we can handle this without having to re-implement any client side form validation. Apart from initializing the AngularJS application, no JavaScript is required to enable this useful feature.

from django.forms import widgets
from django.core.exceptions import ValidationError
from djng.forms import fields, NgFormValidationMixin
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(NgFormValidationMixin, Bootstrap3Form):
    use_required_attribute = False

    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')
from django.core.urlresolvers import reverse_lazy
from django.views.generic.edit import FormView

class SubscribeView(FormView):
    template_name = 'client-validation.html'
    form_class = SubscribeForm
    success_url = reverse_lazy('form_data_valid')
<script type="text/javascript">
    angular.module('djangular-demo', ['djng.forms']);
</script>

<form name="{{ form.form_name }}" method="post" action="." novalidate>
    {% csrf_token %}
    {{ form.as_div }}
    <button type="submit" ng-disabled="{{ form.form_name }}.$invalid" class="btn btn-primary">Subscribe</button>
</form>

Here the differences to the Classic Subscription example is, that the Form now additionally must inherit from the mixin class NgFormValidationMixin. Furthermore, the browsers internal Form validation must be disabled. This is achieved by adding the property novalidate to the Form's HTML element.

Fork me on GitHub