Django CRUD and Angular.js

In the post on Simple Django Class based CRUD views the focus was on setting up simple views with ModelForms to build all the CRUD views for a Model. I have been playing around a bit with Angular.js and wanted to see if I could link the two up.

A word of caution. I really don’t have any idea what I am doing in Angular.js land so take this all with a grain of salt.

Angular seems like a kind of retro-future solution to the javascript application problem. Backbone.js seems to want to get away from the DOM as anything other than presentation. In Backbone the models exist in a pure JS land and the views render them out into the DOM as needed when things change by subscribing to events on the model. Angular in contrast, embraces the DOM more, turning <input> elements into models and providing updates via some dependency injection magic.

Angular also lets you essentially extend html by letting you add your own element and attribute types using directives. But we are getting ahead of ourselves.

Every Angular tutorial shows some example like this:

 <input type="text" ng-model="MyModel"/>
{{ MyModel }}

In this case angular will dynamically update the page by reprocessing the stuff inside {{}} each time the user types something new into the model. Neat.

Another thing Angular can do is provide validation of fields. This works well with html5 <input> types like number and email. If the input element has an ng-model attribute, angular will add some css classes ng-valid or ng-invalid in response to user input.

Which got me thinking. Can I link up Django ModelForms on the server side with all this Angular stuff on the client? The idea is adding some functionality into the Django forms so that they render in way that angular will take advantage of.

If you build the forms by hand in Django you can basically set the widget attributes to whatever you like, but if you want this to happen more automatically when using ModelForms you are probably going to want to check out formfield_callback. In each model form you can add a new formfield_callback function that will determine how django converts the fields in the model to fields in the form. Unfortunately, it doesn’t seem like you can inherit this function from your own ModelForm base class. You have to add it into each class definition. Cry.

class MyForm(forms.ModelForm):
    formfield_callback = angular_formfield_callback
    class Meta:
        model = MyModel

Where angular_formfield_callback is the function we are going to write.

Lets think first about the <input> types. By default django doesn’t seem to do much here. Even if your model has a field of type PositiveIntegerField, the element django gives by default is just type=’text’. So lets change this by defining our own widget for positive integers.
Here we set the input type and also force the min attribute to be zero since this is a PositiveNumber field:

class PositiveIntWidget(TextInput):
    input_type = 'number'
    def __init__(self, *args, **kwargs):
        if not 'attrs' in kwargs:
            kwargs['attrs'] = dict(min=0)
        else:
            kwargs['attrs'].setdefault('min', 0)
        super(PositiveIntWidget, self).__init__(*args, **kwargs)

Then we would define our form field callback something like:
def angular_formfield_callback(f, **kwargs):
    if isinstance(f, PositiveIntegerField):
        field = f.formfield(widget = PositiveIntWidget(),**kwargs)
    else:
        # We don't have a special one just let django pick for us
        field = f.formfield(**kwargs)
    if field: # guard against the case where there is no field...
        if 'ng-model' not in field.widget.attrs:
            # set up a model for this so ng can do stuff with it
            # the dj_ prefix helps avoid name collision
            field.widget.attrs['ng-model'] = 'dj_%s' % f.name

Now when we render the form from django in our django template using {{form}} it will be decorated with the ng-model tag with its value set to the field name of the django model field.

But there is still a problem. Angular really wants the initial values of the fields to be stored in the $scope object. When you use the technique above by itself and you are trying to have it work for updating existing data, the values which django sets as the value attritbute in the <input> element don’t seem to get copied into Angular’s scope.

I tried a few ways around this but ended up writing my own directive for this. The directive just copies the value attribute into the scope using the element’s model name.

var app = angular.module('dj_app', []);
app.directive('djInit', function() {
            return function(scope, element) {
                initial_value = element.val();
                if(element[0].type == 'number')
                {
                    initial_value = parseInt(initial_value);
                }
                scope[element[0].attributes['ng-model'].value] = 
                      initial_value;
            }

That junk with type==’number’ makes it so the scope value is actually a numeric instead of string value.

Now we just need to add the dj-init attribute to all the field objects in the form field callback at the same spot where we set the model. For fun we can also copy over some info from Validators on the original model field and we get:

def angular_formfield_callback(f, **kwargs):
    """
    Applies our own assignment of widgets when using model forms
    """
    if isinstance(f, PositiveIntegerField):
        field = f.formfield(widget = PositiveIntWidget(),**kwargs)
    else:
        # We don't have a special one just let django pick for us
        field = f.formfield(**kwargs)

    if field:  # guard against missing field
        for v in f.validators: # Copy any model validation into the widget
            if isinstance(v, MaxValueValidator):
                field.widget.attrs['max'] = v.limit_value
            elif isinstance(v, MinValueValidator):
                field.widget.attrs['min'] = v.limit_value

        if 'ng-model' not in field.widget.attrs:
            # set up a model for this so ng can do stuff with it
            field.widget.attrs['ng-model'] = 'dj_%s' % f.name
            # set the model name as an attribute of the 
            # init directive in case we want to know it later
            field.widget.attrs['dj_init'] = 
                           field.__class__.__name__.lower()

    return field

Its easy enough to play the same trick with other data types like email or url.

Things to watch out for

Both django and Angular.js use {{ for macro expansion. {% verbatim %} {% endverbatim %} is your friend.

An Angular directive called fooBar is referenced in the markup as foo-bar.

2 thoughts on “Django CRUD and Angular.js

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>