JavaScript Templates with Django Pipelines

In a modern world where building apps based on components with a lot of JavaScript in the middle, sometimes we end shipping a more code that the end Users will ever use.

Do you remember, back in the 00's when we use AJAX to bring "real time" to the website, this post is about that. Before we had React or Vue, we used template engines like {{ mustache }} or Handlebars in order to bring our dynamic model data to the view.

Django is one of those frameworks that doesn't plays very well with the modern FrontEnd technologies, causing you have to use workarounds in order to get a first class setup.

Let's suppose the following User interaction: You have a blog named "Surf and Turf Experiences". In your blog, you have a sidebar that shows, the most commented or newest posts, this QuerySet could consume a few seconds and in order to reduce the database load and QuerySet complexity you remove this from the main post list, then you move it to a view that returns an HTML template, the view is like this:

def ajax_sidebar_posts(request):
    user_posts = get_all_user_posts()
    fields = ['author', 'title', 'body']
    if 'hot' in request.GET:
        order = ['-number_of_replies', '-submit_date']
        fields += ['number_of_replies']
    else:
        order = [F('newest_activity').desc(nulls_last=True)]
        fields += ['newest_activity', 'submit_date']
    comments = user_posts.only(*fields).order_by(*order)[:5]
    return render(request, sidebar_comments.html, {
        'comments': comments,
        'is_hot_comments': 'hot' in request.GET,
        })

And we ship the following JavaScript, that does a request to the View described above and then append the response into an specific container:

function getSidebarComments(container, req_params) {
  $.get('/comment/ajax-sidebar-comments', req_params)
    .done(function (response) {
      container.children().remove();
      container.append(response).fadeIn('slow');
    })
    .fail(function (response) {
      $.showNotification('error', 'Oh snap!', 'No comments');
      console.error('error', response);
    });
};

This works, but when you need high availability and don't want to relay on a network request, this might not be the best option.

Django-Pipelines

Django Pipelines which provides CSS and JavaScript concatenation and compression, allows you to use JavaScript templates based on a variant of Micro Templating by John Resig along with your JavaScript views. To use javascript templates, add them to the JAVASCRIPT group in the Django settings file:

PIPELINE = {
    'JAVASCRIPT': {
        'application': {
            'source_filenames': (
                'js/app.js',
              'js/jquery.js',
            ),
            'output_filename': 'js/application.js',
        },
        'templates': {
            'source_filenames': (
                'js/templates/**/*.jst',
            ),
            'output_filename': 'js/templates.js',
        },
  }
}
static/js
├── app.js
├── jquery.js
├── templates
│   ├── photo_detail.jst
│   ├── sidebar_posts.jst
│   └── post_comment.jst
└── typpy.core.js

For example, if you have the following template js/templates/photo_detail.jst

<div class="photo">
 <img src="<%= src %>" />
 <div class="caption">
  <%= caption %>
 </div>
</div>

It will be available from your JavaScript code via window.JST

JST.photo_detail({
    src:"images/baby-panda.jpg",
    caption:"A baby panda is born"
});

Updating the codebase

Since now we don't need an HTML template we can change our view to return a simple JSON:

@login_required
def ajax_sidebar_comments(request):
    response = {
        'items': Model.objects.filter(*qs_filters).values(*fields),
        'template': 'sidebar_comments',
    }
    return JsonResponse(response)

And addapt the JavaScript function to use JST:

function getSidebarComments(container, req_params) {
  $.get('/comment/ajax-sidebar-comments', req_params)
    .done(function (response) {
      container.html(
          JST[response.template](response.items)
          ).fadeIn('slow');
    })
    .fail(function (response) {
      $.showNotification('error', 'Oh snap!', 'No comments');
      console.error('error', response);
    });
};

And this is the JST template:

<div class="sidebar-comments">
    <ul>
      <% for ( var i = 0; i < items.length; i++ ) { %>
      <li>
            <h5 class="mt-0 mb-1 text-truncate">
              <span>
                  <% if (!is_hot) { %>
                  <i class="far fa-fire"></i>
                  <% } else { %>
                    <i class="far fa-comment"></i>
                  <% } %>
              </span>
                <%= items[i].title %>
            </h5>
            <p class="m-0 text-truncate"><%= items[i].comment %></p>
        </li>
      <% } %>
  </ul>
</div>

Consideration

  • You can't use the Django template tags in a JST template neither your custom or the builtins.
  • Changing a view response from HTML to JSON sometimes will not make a big change, unless you're returning a big HTML response.
  • The JST templates are rendered on-time not on-demand.

Extra

There's a handy Django template tag in the builtins named json-script which safely outputs a Python object as JSON, wrapped in a <script> tag, ready for use with JavaScript. Combining this with the JST templates could be a good combination.