RkBlog

Hardware, programming and astronomy tutorials and reviews.

Example Ember.js + Django + Django Rest Framework single-page application

How a single page applications written in ember look and work and how they integrate with Django.

Ember.js is one of JavaScript frameworks for creating interactive single page web applications. Pages may change but the browser doesn't reload the page. This framework has some similarities with Django and can be good pick to start with such frontend applications for Django developers with some JavaScript knowledge.

In this article I'll show you an example Django + Django Rest Framework + Ember.js application. Classical posts and categories done in Ember and in Django for comparison.

Ember.js + DRF + Django Application

If you are new to Ember.js you should read my previous article which describes the basics (and shows an ember application with Tastypie backend). In this article I'll go staright to the application code.

So now I'll go through a simple application that has posts assiged to a category. Whole django project with the ember code can be found on github. Grab it, run it, modify it as you see fit. There are two applications: blog with the classical Django implementation and eblog with ember frontend, DRF API backend and Django.

Single page applications need a single page so in Django we just need a basic view that renders a given template:

from django.views.generic import TemplateView


class EmberView(TemplateView):
    template_name = 'eblog/blog.html'

ember_view = EmberView.as_view()

That blog.html template has some vendor and our JS files hooked:

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="{% static 'vendor/handlebars-v1.3.0.js' %}"></script>
<script src="{% static 'vendor/ember.js' %}"></script>
<script src="{% static 'vendor/ember-data.js' %}"></script>
<script src="{% static 'vendor/ember-data-django-rest-adapter.js' %}"></script>
<script src="{% static 'vendor/moment-with-locales.js' %}"></script>

<script src="{% static 'application/app.js' %}"></script>
<script src="{% static 'application/helpers.js' %}"></script>
<script src="{% static 'application/router.js' %}"></script>
<script src="{% static 'application/routes.js' %}"></script>
<script src="{% static 'application/controllers.js' %}"></script>
<script src="{% static 'application/models.js' %}"></script>
<script src="{% static 'application/views.js' %}"></script>

We have jquery, handlebars templates library, ember and django rest framework adapter for ember as well as our application files. Ember applications divide to parts so I places each in separate file:

Let us start with standard app initialization in app.js (can have more options and code):

window.Blog = Ember.Application.create();

Where Blog is the application name used in every other file.

Now we can write models for our Django models which have posts and categories:

(function(Blog, $, undefined ) {
    Blog.ApplicationAdapter = DS.DjangoRESTAdapter.extend({
        namespace: "api"
    });
    Blog.Category = DS.Model.extend({
        name: DS.attr('string'),
        slug: DS.attr('string')
    });
    Blog.Post = DS.Model.extend({
        title: DS.attr('string'),
        slug: DS.attr('string'),
        text: DS.attr('string'),
        category: DS.belongsTo('category', {async: true}),
        posted_date: DS.attr('date')
    });
}(window.Blog, jQuery));

ApplicationAdapter is the DRF adapter configuration for ember. Usually every REST generation backend needs an adapter for ember so that all features are supported (like filtering, relation handling, batch fetching etc.). In case of DRF we have ember-django-adapter.

Blog.Category and Blog.Post are ember models that match Django models exposed via the REST API by DRF. DS.attr can take one argument stating field type. It's optional, but for non string or not null fields it's required for correct handling.

We have models that will call the DRF API, which is made by serializers and two views: CategorySetView and PostSetView.

So now we can start with base ember template. In the blog.html template there are JS inserts of handlebars templates used by ember. body.handlebars is the base template used by ember and the other templates are for our pages:

<script type="text/x-handlebars">
    {% include "eblog/body.handlebars" %}
</script>
<script type="text/x-handlebars" data-template-name="posts">
    {% include "eblog/posts.handlebars" %}
</script>
<script type="text/x-handlebars" data-template-name="categoryPosts">
    {% include "eblog/posts.handlebars" %}
</script>
<script type="text/x-handlebars" data-template-name="post">
    {% include "eblog/post.handlebars" %}
</script>

Template name passed in data-template-name is used to populate names fo controllers, views, route and so on. For posts we will get PostsController, PostsView etc. A page within ember application doesn't have to have custom views or controllers as ember will use built in defaults if nothing custom is provided.

So let us look at the body.handlebars:

{% verbatim %}
<div class="blog-app">
    <h1>{{#link-to "posts"}}Example Blog{{/link-to}}</h1>
    <nav>
        {{#each category in categories itemController="category"}}
            {{#link-to "categoryPosts" category.id classNameBindings="controller.isActive:current"}}{{category.name}}{{/link-to}}
        {{/each}}
    </nav>
    {{outlet}}
</div>
{% endverbatim %}
<footer>
    <a href="{% url 'posts' %}">Classic version</a>
</footer>

Handlebars use similar syntax to Django templates so we have to use verbatim tag to prevent Django from parsing handlebars templates. The link-to tag is used to make links to pages within ember. The outlet tag defines a place in which child templates will be inserted - so in our case all other three. There is also category list iteration and linking every category to a page (that should show posts only from that category).

The each iterator has an extra itemController which allows us to assign a custom controller for every iterated element. From our controller we have a isActive property which will set CSS class to current if true (STATEMENT:ON_TRUE:ON_FALSE).

Now let check our router - URLs within our application:

(function(Blog, $, undefined) {
    Blog.Router.map(function() {
        this.route('posts');
        this.route('categoryPosts', {path: 'category/:id'});
        this.route('post', {path: 'posts/:id'});
    });
    if (window.history && window.history.pushState) {
        Blog.Router.reopen({
          location: 'history'
        });
    }
}(window.Blog, jQuery));

We have three pages linked/defied with this.route. In case of categoryPosts which uses a category ID we pass that as a dynamic part of the URL, where id is our variable name. Similar for post. Ember also supports nesting pages and some other implementation of this example could have post as a child of categoryPost.

There is also Blog.Router.reopen snipped that changes how URLs are generated. If the browser supports push state the links will look normally like /posts/. If not default mode will be used that is based on hash #/posts/. You can copy/past this snippet in your app too if you want better looking urls for most browsers (except older IE).

In routes we have page business logic:

(function(Blog, $, undefined ) {
    Blog.IndexRoute = Ember.Route.extend({
        redirect: function() {
            this.transitionTo('posts');
        }
    });
    Blog.PostsRoute = Ember.Route.extend({
        model: function() {
            return this.get('store').find('post');
        }
    });
    Blog.CategoryPostsRoute = Ember.Route.extend({
        model: function(params) {
            return this.get('store').find('post', {'category': params.id });
        }
    });
    Blog.PostRoute = Ember.Route.extend({
        model: function(params) {
            return this.get('store').find('post', params.id);
        }
    });
}(window.Blog, jQuery));

IndexRoute is the rout for the root of the application and can be used to direct to one of our pages that will serve as the first page. Other routes are for our pages. In PostsRoute we fetch all posts while for CategoryPostsRoute we filter posts by category id. params.id is the variable name from the URL mapping. And in the end for PostRoute we fetch a post by given ID.

The posts page should list all posts so in posts.handlebars we iterate fetched content:

{% verbatim %}
{{#each post in content}}
    <h2>{{#link-to "post" post.id}}{{post.title}}{{/link-to}}</h2>
    <blockquote>
        {{{post.text}}}
        <p>{{date post.posted_date}}, {{post.category.name}}</p>
    </blockquote>
{{else}}
    <p>No posts.</p>
{{/each}}
{% endverbatim %}

Data fetched by route will be under content. Note that ember is asynchronous. The page gets displayed while the data is being fetched and when it's fetched it gets displayed. In this simple example it doesn't change much over Django version but in more advanced applications it will be clearly visible and ember applications will be build differently than django ones.

The date tag is a custom handlebars helper that formats date with moment.js library.

So let us look at the controllers:

(function(Blog, $, undefined ) {
    Blog.ApplicationController = Ember.ObjectController.extend({
        categories: function() {
            return this.get('store').find('category');
        }.property(),
        makeCurrentPathGlobal: function() {
            Blog.set('currentPath', this.get('currentPath'));
        }.observes('currentPath')
    });
    Blog.PostController = Ember.ObjectController.extend({
        isPython: function() {
            var title = this.get('content.title').toLowerCase();
            var category = this.get('content.category.name');
            if (category) {
                category = category.toLowerCase();
                return title.indexOf('python') != -1 || category.indexOf('python') != -1;
            }
        }.property('content.title', 'content.category.name')
    });
    Blog.CategoryController = Ember.ObjectController.extend({
        needs: ["post"],
        isActive: function() {
            var path = Blog.get('currentPath');
            return path == 'post' && this.get('content.id') == this.get('controllers.post.content.category.id');
        }.property('content.id', 'controllers.post.content.category.id', 'Blog.currentPath')
    });
}(window.Blog, jQuery));

ApplicationController is the root controller which is visible in the base template. We fetch categories in it so that the template may list them. There is also a small hack assigning current path to a global application variable. Sometimes is needed to have that path in an IF statement but it should be avoided if possible - to avoid high number of value recalculations.

categories are marked as property and such properties are key element of ember applications. A property will calculate its value only when it is used and when variable on which it depends change. In case of category list there is no dependencies so it will never recalculate, but as it is used in a template it will calculate once and return the result.

The observes type of functions are reactions to an event. They calculate every time a variable on which their depend change. They don't return anything, they just react. Observers may recalculate a lot and in general should be avoided unless needed - like in views to handle external JS widgets or animations. Ember way of doing things is by properties and by template bindings (using things in templates which acts just like a property).

In PostController I've created a property that has some dependencies - post title and category name. Will return true if there is python somewhere within those two strings. CategoryController has isActive property that is used to highlight category name on the category list. Such properties handle asynchronous behavior of such applications. They will get a promise of a category or post with no value under given field. But when the object is fetched the promis will set it values and the property will be recalculated. You don't have to handle that nor do any DOM operation, jQuery event handling etc.

In the end

I've showcased a working basic ember application that uses Django and DRF as it data provider. Some aspects of ember applications are similar to django, but the asynchronous behavior makes at some point using ember much harder (for the first time). Even so single page applications can be quite handy to use for users where they don't have to wait for page to load and so on. Ember application can work as an Facebook application (like a contest) or rich content, interactive widget on a bigger web page and more.

So if you are interested in ember get the code from github, check ember manual, try to change the application a bit or try creating something simple on your own. A lot of help is available on stackoverlow or also some on the ember IRC channel.

RkBlog

4 January 2015;

Comment article