Validated Model-Form Demo

Combine client-side form validation with server-side form validation

New in version 2.0  Here we drop support for application/x-www-form-urlencoded submissions and combine the validation from the two previous examples. This presumably is the best solution, when integrating Django forms with AngularJS.


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?

If a Form inherits from both mixin classes NgModelFormMixin and NgFormValidationMixin, then django-angular combines client-side validation with AngularJS's model scope.

Refer to the previous examples for a detailed explanation.

Adding Actions to Buttons

In the previous example we have seen, that we can chain asynchronous actions on the button's <button ng-click="on(…)" type="button" …> directive. For this we're using promise functions extensively, provided by AngularJS.

On success a promise calls the function passed into the …then(…)-clause. By chaining these clauses, we can invoke actions sequentially, each one after the previous action has finished.

This can be used to build a chain of independent actions for each button. If an action fails, the following action inside the …then(…) is skipped in favour of the function inside the next …catch(…) call. If an action shall be executed regardless of its previous success or failure state, use it inside a …finally(…)-clause.

Promise aware actions

django-angular is shipped with a special button directive, which, if used inside a <form djng-endpoint="…" …> directive, adds a bunch of prepared action functions to its (isolated) scope:

Disable button for invalid form

By adding ng-disabled="isDisabled()" to the button element, the button element is greyed out and disabled in case the client-side form validation fails. This shall be used to prevent users from submitting forms with missing fields or wrong values.

Our first promise

In order to chain our actions, we have to start with a promise-clause, which always resolves. This is why we always have to start our first action such as: ng-click="do(first_action).then(…)…".

Send a subset of the scope to the server

Forms with input elements bound to the scope, normally use a directive with such a pattern: ng-model="scope_prefix.field_name". Here the bound models are grouped into one or more subobjects of the scope. By invoking fetch(), create(), update() or delete(), we send that subset of data to the server, using the HTTP methods GET, POST, PUT or DELETE respectively.

Scroll to rejected fields

Forms sometimes extend over more than one screen height. If a form validation fails, the message near a rejected field may be outside the visible area. To improve the user experience, it therefore is good practice to point the user to the field(s), which have been rejected. This can by achieved by adding a target such as ng-click="do(…).then(…).catch(scrollToRejected()) to our promises chain. Whenever a form validation fails, the browser scrolls the page content, so that it shows up on top of the visible area.

Reload the current page

Specially after a successful login- or logout submission, we might want to reload the current page, in order to reset the cookie value and/or session states. For this purpose, use an action such as: ng-click="do(upload()).then(reloadPage())".

Proceed to another page

To proceed to another page after a successful submission, use an action such as: ng-click="do(upload()).then(redirectTo('/path/to/view'))". If the response from the server contains {"success_url": "/path/to/other/view"}, then the URL provided by redirectTo()is overridden.

Delay the submission

Sometimes we might want to delay a further action. If for instance we want to add a 500 miliseconds delay after a successful submission, we then would rewrite our action such as: ng-click="do(upload()).then(delay(500)).then(reloadPage())".

Giving feedback to the user

To improve the user's experience, it is a good idea to give feedback on an action, which succeeded or failed. Our button directive offers two such functions, one to display an OK tick on success, and one to display a cross to symbolize a failed operation. These symbols replace the buttons <i class="fontawesome or glyphicon"></i> icon element, if present.

By using the promises chain, we can easily integrate this into our actions flow: ng-click="do(update()).then(showOK()).then(delay(500)).then(reloadPage()).catch(showFail()).then(delay(2000)).finally(restore())". Here we use the catch(…)-clause, to run a different action function in case of a failed submission. The finally(restore()) is executed regardless of the submission success or failure, it restores the button internal icon to its original state.

Handle processing delays

Sometimes processing form data can take additional time. To improve the user experience, we can add some feedback to the submission button. By changing the submit action to ng-click="do(disable()).then(update()).then(redirectTo()).finally(restore())", the submit button is deactivated during the form submission and will be reactivated as soon as the server responded.

In case of potentially long lasting submissions this can be further extended, by replacing the button's internal icon with a rotating spinner wheel   . To do so, just replace the disable() function against spinner().

Passing Extra Data

Sometimes we might want to use more than one submit button. In order to distinguish which of those buttons has been pressed, pass an object to the form submission function, for instance ng-click="do(update({foo: 'bar'}))". That dictionary then is added to the submitted payload and can be extracted by the server's view function for further analysis.

Triggering Further Actions

By adding ng-click="do(update()).then(emit('name', {'foo': 'bar'}))" to our promises chain, we can emit an event upwards through the scope hierarchy, notifying registered listeners.

Fill Form with Data send by Server

The server-side endpoint can push data to the form, in order to fill it. Say, a form is named my_form, then sending an object, such as {"my_form": {"fieldname1": "value1", "fieldname2": "value2", "fieldname3": "value3"}}, in the response's payload, will set the named form fields with the given values.

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_password(value):
    # Just for demo. Do not validate passwords like this!
    if value != "secret":
        raise ValidationError("The password is wrong.")

class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, 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,
        required=True,
        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],
        min_length=6,
        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()

default_subscribe_data = {
    'first_name': "John",
    'last_name': "Doe",
    'sex': 'm',
    'email': 'john.doe@example.org',
    'phone': '+1 234 567 8900',
    'birth_date': '1975-06-01',
    'continent': 'eu',
    'height': 1.82,
    'weight': 81,
    'traveling': ['bike', 'train'],
    'notifyme': ['email', 'sms'],
    'annotation': "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
    'agree': True,
    'password': '',
}
import json
from django.http import JsonResponse
from django.core.urlresolvers import reverse_lazy
from django.views.generic.edit import FormView
from django.utils.encoding import force_text

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

    def get(self, request, **kwargs):
        if request.is_ajax():
            form = self.form_class(initial=default_subscribe_data)
            return JsonResponse({form.form_name: form.initial})
        return super(SubscribeView, self).get(request, **kwargs)

    def post(self, request, **kwargs):
        assert request.is_ajax()
        return self.ajax(request)

    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 }}" djng-endpoint="." ng-model-options="{allowInvalid: true}" novalidate>
  {{ form.as_div }}
  <button ng-click="do(update()).then(redirectTo()).catch(scrollToRejected())" 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>
  <button ng-click="do(spinner()).then(update({delay: true})).then(showOK()).then(delay(500)).then(redirectTo()).catch(showFail()).then(delay(1500)).finally(restore())" ng-disabled="isDisabled()" type="button">
    Delayed Submission <i class="some-icon"></i>
  </button>
  <button ng-click="do(fetch())" type="button">
    Fetch Defaults <i class="some-icon"></i>
  </button>
</form>

This configuration is the most flexible one. Use it on productive web-sites.
Note: The submit buttons are disabled, until the client-side Form validation has validated all the fields.

Fork me on GitHub