Integrate a Django form with an AngularJS model¶
When deriving from Django’s forms.Form
class in an AngularJS environment, it can be useful to
enrich the rendered form output with an AngularJS HTML tag, such as:
ng-model="model_name"
where model_name corresponds to the named field from the declared form class.
Sample code¶
Assume to have a simple Django form class with a single input field. Enrich its functionality
by mixing in the djng class NgModelFormMixin
Note
Here the names NgModelForm... do not interrelate with Django’s forms.ModelForm
.
Instead that name reflects the HTML attribute ng-model
as used in <form>
-elements
under control of AngularJS.
from django import forms
from django.utils import six
from djng.forms import NgDeclarativeFieldsMetaclass, NgModelFormMixin
class ContactForm(six.with_metaclass(NgDeclarativeFieldsMetaclass, NgModelFormMixin, forms.Form)):
subject = forms.CharField()
# more fields ...
In the majority of cases, the Form is derived from Django’s forms.Form
, so the above example
can be rewritten in a simpler way, by using the convenience class NgForm
as a replacement:
from djng.forms import NgModelFormMixin, NgForm
class MyValidatedForm(NgModelFormMixin, NgForm):
# members as above
If the Form shall inherit from Django’s forms.ModelForm
, use the convenience class
NgModelForm
:
from djng.forms import NgModelFormMixin, NgModelForm
class MyValidatedForm(NgModelFormMixin, NgModelForm):
class Meta:
model = Article
# fields as usual
Now, each rendered form field gets an additional attribute ng-model
containing the field’s name.
For example, the input field named subject
now will be rendered as:
<input id="id_subject" type="text" name="subject" ng-model="subject" />
This means, that to a surrounding Angular controller, the field’s value is immediately added to its
$scope
.
Full working example¶
This demonstrates how to submit form data using an AngularJS controller. The Django view handling this unbound contact form class may look like
from django.views.generic import TemplateView
class ContactFormView(TemplateView):
template = 'contact.html'
def get_context_data(self, **kwargs):
context = super(ContactFormView, self).get_context_data(**kwargs)
context.update(contact_form=ContactForm())
return context
with a template named contact.html
:
<form ng-controller="MyFormCtrl" name="contact_form">
{{contact_form}}
<button ng-click="submit()">Submit</button>
</form>
and using some Javascript code to define the AngularJS controller:
my_app.controller('MyFormCtrl', function($scope, $http) {
$scope.submit = function() {
var in_data = { subject: $scope.subject };
$http.post('/url/of/your/contact_form_view', in_data)
.success(function(out_data) {
// do something
});
}
});
Note that the <form>
tag does not require any method
or action
attribute, since the
promise success
in the controller’s submit function will handle any further action.
The success handler, for instance could load a new page or complain about missing fields. It now
it is even possible to build forms without using the <form>
tag anymore. All what’s needed
from now on, is a working AngularJS controller.
As usual, the form view must handle the post data received through the POST (aka Ajax) request.
However, AngularJS does not send post data using multipart/form-data
or
application/x-www-form-urlencoded
encoding – rather, it uses plain JSON, which avoids an
additional decoding step.
Note
In real code, do not hard code the URL into an AngularJS controller as shown in this example. Instead inject an object containing the URL into the form controller as explained in manage Django URL’s for AngularJS
Add these methods to view class handling the contact form
import json
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponseBadRequest
class ContactFormView(TemplateView):
# use ‘get_context_data()’ from above
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(ContactFormView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not request.is_ajax():
return HttpResponseBadRequest('Expected an XMLHttpRequest')
in_data = json.loads(request.body)
bound_contact_form = CheckoutForm(data={'subject': in_data.get('subject')})
# now validate ‘bound_contact_form’ and use it as in normal Django
Warning
In real code, do not use the @csrf_exempt
decorator, as shown here for
simplicity. Please read on how
to protect your views from Cross Site Request Forgeries.
Prefixing the form fields¶
The problem with this implementation, is that one must remember to access each form field three
times. Once in the declaration of the form, once in the Ajax handler of the AngularJS controller,
and once in the post handler of the view. This make maintenance hard and is a violation of the DRY
principle. Therefore it makes sense to add a prefix to the model names. One possibility would be to
add the argument scope_prefix
on each form’s instantiation, ie.:
contact_form = ContactForm(scope_prefix='my_prefix')
This, however, has to be done across all instantiations of your form class. The better way is to hard code this prefix into the constructor of the form class
class ContactForm(NgModelFormMixin, forms.Form):
# declare form fields
def __init__(self, *args, **kwargs):
kwargs.update(scope_prefix='my_prefix')
super(ContactForm, self).__init__(*args, **kwargs)
Now, in the AngularJS controller, the scope for this form starts with an object named my_prefix
containing an entry for each form field. This means that an input field, the is rendered
as:
<input id="id_subject" type="text" name="subject" ng-model="my_prefix.subject" />
This also simplifies the Ajax submit function, because now all input fields are available as a
single Javascript object, which can be posted as $scope.my_prefix
to your Django view:
$http.post('/url/of/contact_form_view', $scope.my_prefix)
Working with nested forms¶
NgModelFormMixin is able to handle nested forms as well. Just remember to add the attribute
prefix='subform_name'
with the name of the sub-form, during the instantiation of your main form.
Now your associated AngularJS controller adds this additional model to the object
$scope.my_prefix
, keeping the whole form self-contained and accessible through one Javascript
object, aka $scope.my_prefix
.
The Django view responsible for handling the post request of this form, automatically handles the parsing of all bound form fields, even from the nested forms.
Note
Django, internally, handles the field names of nested forms by concatenating the prefix
with the field name using a dash ‘-
’. This behavior has been overridden in order to
use a dot ‘.
’, since this is the natural separator between Javascript objects.