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.