Dynamically Add Items to Django Inline Formset Using Underscore.js

by Nick Lang -- July 19, 2012, 9:47 a.m.

So periodically you're confronted with having to used inline formsets from django. These in practice aren't very hard to use or implement. There are plenty of blogposts out there describing in intricate detail how to use use them and implement them in practice. The problem with most of these posts is that's where it ends. What if you want to dynamically add items to your inline formset on the front end?

Well turns out it's pretty simple if you use Underscore.js (or any other JS template library). I'm gonna go over the process of setting up some models, a CBV to access a form and the inline formset and a template with minimal Jquery required to get the inline formset to dynamically add items.

First lets declare some models

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author)
    title = models.CharField(max_length=100)

Next we need to declare some forms

from django import forms
from django.forms.models import inlineformset_factory
from .models import Author, Book

class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author

class BookForm(forms.ModelForm):
    class Meta:
        model = Book

BookFormSet = inlineformset_factory(Author, Book)

Now we're going to use a CBV to display a form for adding an Author and a formset to add books to this Author as well.

from django.views.generic import CreateView
from django.shortcuts import redirect

from .models import Author
from .forms import AuthorForm, BookFormSet

class AddAuthorView(CreateView):
    template_name = 'create_author.html'
    form_class = AuthorForm

    def get_context_data(self, **kwargs):
        context = super(AddAuthorView, self).get_context_data(**kwargs)
        if self.request.POST:
            context['formset'] = BookFormSet(self.request.POST)
        else:
            context['formset'] = BookFormSet()
        return context

    def form_valid(self, form):
        context = self.get_context_data()
        formset = context['formset']
        if formset.is_valid():
            self.object = form.save()
            formset.instance = self.object
            formset.save()
            return redirect(self.object.get_absolute_url())  # assuming your model has ``get_absolute_url`` defined.
        else:
            return self.render_to_response(self.get_context_data(form=form))

And finally our template to render

<h1>Add an Author and Books</h1>

<form class="form-horizontal form-inline" method="post" >
  {% csrf_token %}
  <legend>Author Details</legend>
  {{ form }}
  {{ formset.management_form }}
  <legend>Books</legend>
  <div class="books">
  {% for book_form in formset %}
    <div id="book-{{ forloop.counter0 }}">
      {{ book_form }}
    </div>
  {% endfor %}
 </div>
 <div class="form-actions">
   <a href="#" class="btn btn-info add-book">Add Book</a>
   <button type="submit" class="btn btn-primary">Create Event</button>
 </div>
</form>

All of this up until now has been pretty generic Django. Aside from the view for adding in the formset to the CreateView. If any of this is unclear let me know and I'll try to clear it up in the comments.

Now comes the tricky part. We've got our template setup so that our form renders and everything is good, but what happend when we want to add additional books to the page. When using the Django admin there is the option to dynamically add more items to the inline. Well that's where underscore.js comes in.

First we need to add JQuery and Underscore.js to our template. Then we need to create the underscore template which is easily done by rendering the template once and looking at how the HTML is rendered under the <div id="book-0"> element. In this example I copied the first blank form, and substutided all the 0 with <%= id %>. This is underscore's syntax for specifying variables in templates. The id is context variable I chose to use for this example.

<script type="text/html" id="book-template">
  <div id="book-<%= id %>">
    <label for="id_books-<%= id %>-title">Book Title:</label>
    <input id="id_books-<%= id %>-title" type="text" name="books-<%= id %>-title" maxlength="100">
    <input type="hidden" name="books-<%= id %>-author" id="id_books-<%= id %>-author">
    <input type="hidden" name="books-<%= id %>-id" id="id_books-<%= id %>-id">
  </div>
</script>

Now that we have our template we need some javascript to make the new book forms show up each time you click the "Add Book" button. How this will work, is we fire a function each time the "Add Book" button is clicked. This function will first find out how many book forms have been rendered so far. Next it will grab the template, and then render the template with the context data (our number of Book forms) and then append the rendered html to the page. The next part is important cause it is updating the management_form details to increase the number of forms included with the formset. So when Django goes to process this form it will not throw any errors about the management form.

$('.add-book').click(function(ev){
  ev.preventDefault();
  var count = $('.books').children().length;
  var tmplMarkup = $('#book-template').html();
  var compiledTmpl = _.template(tmplMarkup, { id : count });
  $('div.books').append(compiledTmpl);
  // update form count
  $('#id_books-TOTAL_FORMS').attr('value', count+1);
});

Putting it all together we get our updated template

<script src="{{ STATIC_URL }}/js/jquery.js" ></script>
<script src="{{ STATIC_URL }}/js/underscore.js" ></script>

<script>
  $('.add-book').click(function(ev){
    ev.preventDefault();
    var count = $('.books').children().length;
    var tmplMarkup = $('#book-template').html();
    var compiledTmpl = _.template(tmplMarkup, { id : count });
    $('div.books').append(compiledTmpl);
    // update form count
    $('#id_books-TOTAL_FORMS').attr('value', count+1);
  });
</script>

<h1>Add an Author and Books</h1>

<form class="form-horizontal form-inline" method="post">
  {% csrf_token %}
  <legend>Author Details</legend>
  {{ form }}
  {{ formset.management_form }}
  <legend>Books</legend>
  <div class="books">
    {% for book_form in formset %}
      <div id="book-{{ forloop.counter0 }}">
        {{ book_form }}
      </div>
    {% endfor %}
  </div>
  <div class="form-actions">
     <a href="#" class="btn btn-info add-book">Add Book</a>
     <button type="submit" class="btn btn-primary">Create Event</button>
   </div>
 </form>

<script type="text/html" id="book-template">
  <div id="book-<%= id %>">
    <label for="id_books-<%= id %>-title">Book Title:</label>
    <input id="id_books-<%= id %>-title" type="text" name="books-<%= id %>-title" maxlength="100">
    <input type="hidden" name="books-<%= id %>-author" id="id_books-<%= id %>-author">
    <input type="hidden" name="books-<%= id %>-id" id="id_books-<%= id %>-id">
  </div>
</script>