Validate Django forms using AngularJS

Django’s forms.Form class offers many possibilities to validate a given form. This, for obvious reasons is done on the server. However, customers may not always accept to submit a form, just to find out that they missed to input some correct data into a field. Therefore, adding client side form validation is a good idea and very common. But since client side validation easily can be bypassed, the same validation has to occur a second time, when the server accepts the forms data for final processing.

This leads to code duplication and generally violates the DRY principle!

NgFormValidationMixin

A workaround to this problem is to use Django’s form declaration to automatically generate client side validation code, suitable for AngularJS. By adding a special mixin class to the form class, this can be achieved automatically and on the fly

from django import forms
from django.utils import six
from djng.forms import fields, NgDeclarativeFieldsMetaclass, NgFormValidationMixin

class MyValidatedForm(six.with_metaclass(NgDeclarativeFieldsMetaclass, NgFormValidationMixin, forms.Form)):
    form_name = 'my_valid_form'
    surname = fields.CharField(label='Surname', min_length=3, max_length=20)
    age = fields.DecimalField(min_value=18, max_value=99)

Note

Since django-angular-1.1, you must use the adopted field classes, instead of Django’s own fields.

In the majority of cases, the Form is derived from Django’s forms.Form, so the above example can be rewritten in a simpler way, by using the convenience class NgForm as a replacement:

from djng.forms import NgFormValidationMixin, NgForm

class MyValidatedForm(NgFormValidationMixin, NgForm):
    # members as above

If the Form shall inherit from Django’s forms.ModelForm, use the convenience class NgModelForm:

from djng.forms import NgFormValidationMixin, NgModelForm

class MyValidatedForm(NgFormValidationMixin, NgModelForm):
    class Meta:
         model = Article

    # fields as usual

Each page under control of AngularJS requires a unique form name, otherwise the AngularJS’s form validation engine shows undefined behavior. Therefore you must name each form inheriting from NgFormValidationMixin. If a form is used only once per page, the form’s name can be added to the class declaration, as shown above. If no form name is specified, it defaults to form, limiting the number of validated forms per page to one.

If a form inheriting from NgFormValidationMixin shall be instantiated more than once per page, each instance of that form must be instantiated with a different name. This then must be done in the constructor of the form, by passing in the argument form_name='my_form'.

In the view class, add the created form to the rendering context:

def get_context_data(self, **kwargs):
    context = super(MyRenderingView, self).get_context_data(**kwargs)
    context.update(form=MyValidatedForm())
    return context

or if the same form declaration shall be used more than once:

def get_context_data(self, **kwargs):
    context = super(MyRenderingView, self).get_context_data(**kwargs)
    context.update(form1=MyValidatedForm(form_name='my_valid_form1'),
                   form2=MyValidatedForm(form_name='my_valid_form2'))
    return context

Note

Do not use an empty label when declaring a form field, otherwise the class NgFormValidationMixin won’t be able to render AngularJS’s validation error elements. This also applies to auto_id, which if False, will not include <label> tags while rendering the form.

Render this form in a template

<form name="{{ form.form_name }}" novalidate>
  {{ form }}
  <input type="submit" value="Submit" />
</form>

Remember to add the entry name="{{ form.form_name }}" to the form element, otherwise AngularJS’s validation engine won’t work. Use the directive novalidate to disable the browser’s native form validation. If you just need AngularJS’s built in form validation mechanisms without customized checks on the forms data, there is no need to add an ng-controller onto a wrapping HTML element. The only measure to take, is to give each form on a unique name, otherwise the AngularJS form validation engine shows undefined behavior.

Forms which do not validate on the client, probably shall not be posted. This can simply be disabled by replacing the submit button with the following HTML code:

<input type="submit" class="btn" ng-disabled="{{ form.form_name }}.$invalid" value="Submit">

More granular output

If the form fields shall be explicitly rendered, the potential field validation errors can be rendered in templates using a special field tag. Say, the form contains

from django import forms
from djng.forms import fields, NgFormValidationMixin

class MyValidatedForm(NgFormValidationMixin, forms.Form):
        email = fields.EmailField(label='Email')

then access the potential validation errors in templates using {{ form.email.errors }}. This renders the form with an unsorted list of potential errors, which may occur during client side validation.

<ul class="djng-form-errors" ng-hide="subscribe_form.email.$pristine">
  <li ng-show="subscribe_form.email.$error.required" class="ng-hide">This field is required.</li>
  <li ng-show="subscribe_form.email.$error.email" class="">Enter a valid email address.</li>
</ul>

The AngularJS form validation engine, normally hides these potential errors. They only become visible, if the user enters an invalid email address.

Bound forms

If the form is bound and rendered, then errors detected by the server side’s validation code are rendered as unsorted list in addition to the list of potential errors. Both of these error lists are rendered using their own <ul> elements. The behavior for potential errors remains the same, but detected errors are hidden the moment, the user sets the form into a dirty state.

Note

AngularJS normally hides the content of bound forms, which means that <input> fields seem empty, even if their value attribute is set. In order to restore the content of those input fields to their default value, initialize your AngularJS application with angular.module('MyApp', ['djng.forms']);.

Combine NgFormValidationMixin with NgModelFormMixin

While it is possible to use NgFormValidationMixin on itself, it is perfectly legal to mix NgModelFormMixin with NgFormValidationMixin. However, a few precautions have to be taken.

On class declaration inherit first from NgModelFormMixin and afterward from NgFormValidationMixin. Valid example:

from django import forms
from djng.forms import NgFormValidationMixin, NgModelFormMixin

class MyValidatedForm(NgModelFormMixin, NgFormValidationMixin, forms.Form):
    # custom form fields

but don’t do this

class MyValidatedForm(NgFormValidationMixin, NgModelFormMixin, forms.Form):
    # custom form fields

Another precaution to take, is to use different names for the forms name and the scope_prefix. So, this is legal

form = MyValidatedForm(form_name='my_form', scope_prefix='my_model')

but this is not

form = MyValidatedForm(form_name='my_form', scope_prefix='my_form')

An implementation note

AngularJS names each input field to validate, by concatenating its forms name with its fields name. This object member then contains an error object, named my_form.field_name.$error filled by the AngularJS validation mechanism. The placeholder for the error object would clash with ng-model, if the form name is identical to the scope prefix. Therefore, just remember to use different names.

Customize detected and potential validation errors

If a form with AngularJS validation is rendered, each input field is prefixed with an unsorted list <ul> of potential validation errors. For each possible constraint violation, a list item <li> containing a descriptive message is added to that list.

If a client enters invalid data into that form, AngularJS unhides one of these prepared error messages, using ng-show. The displayed message text is exactly the same as would be shown if the server side code complains about invalid data during form validation. These prepared error messages can be customized during form field definition.

The default error list is rendered as <ul class="djng-form-errors">...</ul>. To each <li> of this error list, the attribute class="invalid" is added. The last list-item <li class="valid"></li> is somehow special, as it is only visible if the corresponding input field contains valid data. By using special style sheets, one can for instance add a green tick after a validated input field, to signal that everything is OK.

The styling of these validation elements must be done through CSS, for example with:

ul.djng-form-errors {
        margin-left: 0;
        display: inline-block;
        list-style-type: none;
}
ul.djng-form-errors li.invalid {
        color: #e9322d;
}
ul.djng-form-errors li.invalid:before {
        content: "\2716\20";  /* adds a red cross before the error message */
}
ul.djng-form-errors li.valid:before {
        color: #00c900;
        content: "\2714";  /* adds a green tick */
}

If you desire an alternative CSS class or an alternative way of rendering the list of errors, then initialize the form instance with

class MyErrorList(list):
    # rendering methods go here

# during form instantiation
my_form = MyForm(error_class=MyErrorList)

Refer to TupleErrorList on how to implement an alternative error list renderer. Currently this error list renderer, renders two <ul>-elements for each input field, one to be shown for pristine forms and one to be shown for dirty forms.

Adding an AngularJS directive for validating form fields

Sometimes it can be useful to add a generic field validator on the client side, which can be controlled by the form’s definition on the server. One such example is Django’s DateField:

from django import forms

class MyForm(forms.Form):
    # other fields
    date = forms.DateField(label='Date',
        widget=forms.widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}))

Since AngularJS can not validate dates, such a field requires a customized directive, which with the above definition, will be added as new attribute to the input element for date:

<input name="date" ng-model="my_form_data.birth_date" type="text" validate-date="^(\d{4})-(\d{1,2})-(\d{1,2})$" />

If your AngularJS application has been initialized with

angular.module('MyApp', ['djng.forms']);

then this new attribute is detected by the AngularJS directive validateDate, which in turn checks the date for valid input and shows the content of the errors fields, if not.

If you need to write a reusable component for customized form fields, refer to that directive as a starting point.