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.
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" …>
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="…" …>
Disable button for invalid form
By adding ng-disabled="isDisabled()"
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"
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())
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'))"
{"success_url": "/path/to/other/view"}
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>
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())"
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())"
,
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'}))"
Triggering Further Actions
By adding ng-click="do(update()).then(emit('name', {'foo': 'bar'}))"
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 namedmy_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.