Server-Side Form Validation

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

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"
     
    MyFormCtrl's scope:
    subscribe_data = {{ subscribe_data | json }}

    How does it work?

    When working with Django Forms and AngularJS, it is a common use case to access the form's model from within an Angular controller. If the directive ng-model is added to an input field, then the controller's $scope object, always contains the actual field's content for further disposal.

    If a form inherits from the mixin class NgModelFormMixin, then Django renders each Field with the directive ng-model="fieldname". To prevent the pollution of 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. Otherwise 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, using the form's name and this must be different from the scope's content.

    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.

    Form data submitted by Ajax, is validated by the server using the same functionality. Errors detected during such a validation, are sent back to the client using a JSON object. In JavaScript, the controller submitting the form, shall use the returned data and pass it to the special function djangoForm.setErrors(), so that all validation errors can be displayed as usual.

    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') == 'John' and self.cleaned_data.get('last_name') == 'Doe':
                raise ValidationError('The full name "John Doe" is rejected by the server.')
            return super(SubscribeForm, self).clean()
    
    import json
    from django.http import HttpResponse
    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):
            form = self.form_class(data=json.loads(request.body))
            response_data = {'errors': form.errors, 'success_url': force_text(self.success_url)}
            return HttpResponse(json.dumps(response_data), content_type="application/json")
    
    <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="." validate ng-controller="MyFormCtrl">
        {% csrf_token %}
        {{ form.as_div }}
        <button type="submit" class="btn btn-primary">Submit via Post</button>
        <button type="button" ng-click="submit()" class="btn btn-primary">Submit via Ajax</button>
    </form>
    
    angular.module('djangular-demo').controller('MyFormCtrl', ['$scope', '$http', '$window', 'djangoForm',
                                                function($scope, $http, $window, djangoForm) {
    	$scope.submit = function() {
    		if ($scope.subscribe_data) {
    			$http.post(".", $scope.subscribe_data).then(function(response) {
    				if (!djangoForm.setErrors($scope.my_form, response.data.errors)) {
    					// on successful post, redirect onto success page
    					$window.location.href = response.data.success_url;
    				}
    			}, function() {
    				console.error('An error occured during submission');
    			});
    		}
    		return false;
    	};
    }]);
    

    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