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.
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"
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
…_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">
action="…"
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
{"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())"
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())"
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.