Django & EmberJS Full Stack Basics: Connecting Frontend and Backend — Part 5

Michael X
11 min readAug 28, 2018

--

In Part 5 we’ll use the Django REST Framework JSON API to structure the data in a way that Ember Data can work with. We’ll also update the books API to return book a single instance of a book record. We’ll also add the functionality to add, edit, and create books. Then we’re done with our application!

P1, P2, P3, P4, P5

5.1 Install the Django REST Framework JSON API

First we use pip to install the Django REST Framework JSON API. It will transform regular Django REST Framework (DRF) responses into an identity model in JSON API format.

With the virtual environment enabled:

# install the Django REST Framework JSON API
pip install djangorestframework-jsonapi

Next, update DRF settings in server/server/settings.py:

REST_FRAMEWORK = {
'PAGE_SIZE': 100,

'EXCEPTION_HANDLER':
'rest_framework_json_api.exceptions.exception_handler',

'DEFAULT_PAGINATION_CLASS': 'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata', 'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.OrderingFilter',
),
'ORDERING_PARAM': 'sort',

'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),

'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
}

These override the default settings for DRF with defaults from the JSON API. I increased the PAGE_SIZE so we can get up to 100 books back in a response.

5.2 Retrieving a single book record along with READ, UPDATE, DELETE methods

5.2.1 Create a view

Let’s also update our books API so that we can retrieve single instances of a book record.

Create a new view calledbookRudView in server/books/api/views.py:

class bookRudView(generics.RetrieveUpdateDestroyAPIView):
resource_name = 'books'
lookup_field = 'id'
serializer_class = bookSerializer
def get_queryset(self):
return Book.objects.all()

This view uses the id lookup_field to retrieve a single instance of a book with the RetrieveUpdateDestroyAPIView. It also provides us with the basic get, put, patch and delete method handlers to create, update, and delete individual book data.

5.2.2 Update the book API URLs

We’ll need to create a new URL pattern that delivers data through the bookRudView.

from .views import bookAPIView, bookRudView
from django.conf.urls import url
urlpatterns = [
url(r'^$', bookAPIView.as_view(), name='book-create'),
url(r'^(?P<id>\d+)', bookRudView.as_view(), name='book-rud')
]

Import bookRudView, match it to the pattern r'^(?P<id>\d+)', and give it the name book-rud.

5.2.3 Update the server URLs

Finally, update the books API URL pattern in server/server/urls.py so that it can match to patterns which begin after books/:

...urlpatterns = [
...
url(r'^api/books/?', include('books.api.urls', namespace='api-books')),
]

5.2.4 Demonstration: Access a single book record

Now if you visit localhost:8000/api/books/1 it should display a single book record that matches to a book’s id:

Notice that we have access to the DELETE, PUT, PATCH and other methods that come with RetrieveUpdateDestroyAPIView.

5.2.5 Demonstration: Successfully capturing and displaying data from the backend in the correct format

With the JSONAPI installed our frontend application adapter should be happy with the structure of the backend responses. Run both servers and visit localhost:4200/books. We should get back real data from the backend and have the route correctly displayed it. Success!

Successfully capturing and displaying data from the backend

Take a look at the response coming through. It’s in the valid JSONAPI format that Ember Data works with.

5.3 The book Route

We can now view the list of books from our database in the books route. Next, let’s create a route in our frontend client to display each individual book in detail with title, author, and description data.

5.3.1 Create the book route

Generate a new route for the individual book page:

ember g route book

In router.js update the newly created route with the path ‘books/:book_id’. This overrides the default path and takes in a book_id parameter.

...Router.map(function() {
this.route('books');
this.route('book', { path: 'books/:book_id' });
});
...

Next update the book route client/app/routes/book.js to retrieve a single book record from the database:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
store: service(),
model(book) {
return this.get('store').peekRecord('book', book.book_id);
}
});

As outlined in router.js the book route takes in the book_id parameter. The parameter is passed into the route’s model hook and we use it to retrieve the book with the Ember Data store.

5.3.2 Update the book template

Our client/app/templates/book.hbs template should display the book data retrieved from the store. Get rid of {{outlet}} and update it:

<div class="book book--detail">
<div class="book-title">
{{model.title}}
</div>
<div class="book-author">
{{model.author}}
</div>
<div class="book-description">
{{model.description}}
</div>
</div>

Like in the books template we access the model’s attributes using dot notation.

5.3.3 Update the books template

Finally, let’s update the books template to link to each individual book page as displayed in the book route we just created:

<div class="book-list">
{{#each model as |book|}}
{{#link-to 'book' book.id class="book"}}
{{book.title}}
{{/link-to}}
{{/each}}
</div>

Wrap the book.title with the link-to helper. It creates a link to the book route, takes in the book.id as the parameter, and takes class to style the <a> tag that is generated in the DOM.

5.3.4 Demonstration: Select book to view detailed information

Now check out localhost:4200/books. We can click on our books to get a detailed view. Sweet!

5.4 Adding a new book to the database

We can now view all the books from the database and view a single instance of a book record in detail. It’s time to build the functionality to add a new book to the database as follows:

  • The create-book route will handle the process of of creating a new book and adding it to the database
  • The create-book template will have a form with two inputs and a text area to take in a title, author, and description
  • The create-book controller will hold on to the form values

5.4.1 Create the create-book route and controller

Generate the create-book route to handle new book creation:

ember g route create-book

Create a controller of the same name to hold form data:

ember g controller create-book

5.4.2 Setup the create-book controller

In client/app/controllers/create-book.js create a computed property called form that returns an object with our book data attributes. This is where the new book data entered in by the user will be captured. It is empty by default.

import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({ form: computed(function() {
return {
title: '',
author: '',
description: ''
}
})
});

5.4.3 Setup the create-book route

In client/app/routes/create-book.js we create actions to confirm creation of a new book, cancel the creation process, and use a route hook to clear the form data upon entering the route:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
store: service(),
setupController(controller, model) {
this._super(controller, model);
this.controller.set('form.title', '');
this.controller.set('form.author', '');
this.controller.set('form.description', '');
},
actions: { create() {
const form = this.controller.get('form');
const store = this.get('store');
const newBook = store.createRecord('book', {
title: form.title,
author: form.author,
description: form.description
});
newBook.save()
.then(() => {
this.transitionTo('books');
});
},
cancel() {
this.transitionTo('books');
}
}
});

The setupController hook allows us to reset the form’s values so that they don’t persist when we go back and forth through pages. We don’t want to click away to another page without having completed the create book process and come back to see the unused data still sitting in our form.

The create() action will take the form data and create a new record with the Ember Data store which will then persist it to the Django backend. Once successfully complete it will transition the user back to the books route.

The cancel button simply transitions the user back to the books route.

5.4.4 Setup the create-book template

Next, in client/app/template/create-book.hbs we build the form:

<form class="form">  <div class="header">
Add a new book
</div>
{{input
value=form.title
name="title"
placeholder="Title"
autocomplete='off'
}}
{{input
value=form.author
name="author"
placeholder="Author"
autocomplete='off'
}}
{{textarea
value=form.description
name="description"
placeholder="Description"
rows=10
}}
</form>
<div class="actions">
<div>
<button {{action 'create'}}>
Create
</button>
<button {{action 'cancel'}}>
Cancel
</button>
</div>
</div>

The form uses the built-in {{input}} helpers to take in values, display placeholders, and to turn autocomplete off. The {{text}} area helper works in a similar way, though the number of rows are defined as well.

The actions div contains the two buttons to create and cancel. Each button is tied to it’s namesake action using the {{action}} helper.

5.4.5 Update the books route template

The final piece of the create book puzzle is to add a button in the books route to enter the create-book route and begin creating a new book.

Add on to the bottom of client/app/templates/books.hbs:

...{{#link-to 'create-book' class='btn btn-addBook'}}
Add Book
{{/link-to}}

5.4.6 Demonstration: Can successfully add a new book

Now if we go back and try to create a new book again we’ll find success. Click into the book to see a more detailed view:

5.5 Delete an existing book from the database

Now that we can add books to the database we should be able to delete them too.

5.5.1 Update the book route template

First update the book route’s template. Add on under book book--detail:

...<div class="actions {{if confirmingDelete
'u-justify-space-between'}}">
{{#if confirmingDelete}} <div class="u-text-danger">
Are you sure you want to delete this book?
</div>
<div>
<button {{action 'delete' model}}>Delete</button>
<button {{action (mut confirmingDelete)false}}>
Cancel
</button>
</div>
{{else}} <div>
<button {{action (mut confirmingDelete) true}}>Delete</button>
</div>
{{/if}}
</div>

The actions div contains the buttons and text for the book deletion process.

We have a bool called confirmingDelete which will be set on the route’s controller. confirmingDelete is used to add the .u-justify-space-between utility class on actions when it is true. When it is true, it also displays a prompt with the utility class .u-text-danger that prompts the user to confirm deletion of the book. It also shows two buttons, one to run delete action in our route, and the other which uses the mut helper to flip confirmingDelete to false.

When confirmingDelete is false (the default state) a single delete button is displayed used to flip it to true, which then displays the prompt and the other two buttons.

5.5.2 Update the book route

Next update the book route. Under the model hook add:

setupController(controller, model) {
this._super(controller, model);
this.controller.set('confirmingDelete', false);
},

In setupController we call this._super() so that the controller goes through its usual motions. Then we set confirmingDelete to false. We do this because if we had started to delete a book, but left the page without either cancelling the action or deleting the book when we go to any book page confirmingDelete would be set to true as a leftover.

Next let’s create an actions object that will hold our route actions:

actions: {  delete(book) {
book.deleteRecord();
book.save().then(() => {
this.transitionTo('books');
});
}
}

The delete action as referenced in our template takes in a book. We run deleteRecord on the book and then save to persist the change. Once that promise completes successfully we use transitionTo to leave the route and enter the books route (our list view).

5.5.3 Demonstration: Can successfully delete an existing book

Let’s check this out in action. Run the servers and select a book you want to delete.

Delete button visible when confirmingDelete is false
Prompt to confirm deletion of book when confirmingDelete is true

When you delete the book you should be redirected to the books route.

5.6 Edit an existing book in the database

Last but not least we’ll add the functionality to edit an existing books information.

5.6.1 Update the book route template

Open up the book template and add a form to update book data:

{{#if isEditing}}  <form class="form">
<div class="header">Edit</div>
{{input
value=form.title
placeholder="Title"
autocomplete='off'
}}
{{input
value=form.author
placeholder="Author"
autocomplete='off'
}}
{{textarea
value=form.description
placeholder="Description"
rows=10
}}
</form>
<div class="actions">
<div>
<button {{action 'update' model}}>Update</button>
<button {{action (mut isEditing) false}}>Cancel</button>
</div>
</div>
{{else}}...
<div>
<button {{action (mut isEditing) true}}>Edit</button>
<button {{action (mut confirmingDelete) true}}>Delete</button>
</div>
...
{{/if}}

First let’s wrap the entire template in an if statement corresponding to the isEditing property which by default will be false.

Notice that the form is very almost identical to our create book form. The only real difference is that the actions update runs the update action in the book route, and the cancel button flips the isEditing property to false.

Everything we had before gets nested inside the else and we add the Edit button to flip isEditing to true and display the form.

5.6.2 Create a book controller to handle form values

Remember the create-book controller? We used it to hold the values that will be sent to the server to create a new book. We will use a similar idea to get and display the book data in our isEditing form so that it is pre-populated with the current book data. Generate a book controller:

ember g controller book

Open client/app/controllers/book.js and create a form computed property like before. Unlike before we’ll use the model to pre-populate our form with the current book data:

import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({form: computed(function() {
const model = this.get('model');
return {
title: model.get('title'),
author: model.get('author'),
description: model.get('description')
}
})
});

5.6.3 Update the book route

We’ll have to update our route again:

setupController(controller, model) {
...
this.controller.set('isEditing', false);
this.controller.set('form.title', model.get('title'));
this.controller.set('form.author', model.get('author'));
this.controller.set('form.description', model.get('description'));

},

Adding on to the setupController hook we’ll set isEditing to false and reset all the form values to their defaults.

Next let’s create the update action:

actions: {  ...  update(book) {
const form = this.controller.get('form');
book.set('title', form.title);
book.set('author', form.author);
book.set('description', form.description);
book.save().then(() => {
this.controller.set('isEditing', false);
});
}

}

It’s pretty straightforward. We get the form values, set those values on the book and persist with save. Once successful we flip isEditing back to false.

5.6.4 Demonstration: Can successfully edit information of an existing book

Edit button to toggle the isEditing controller property
Form pre-populated with book’s current information
Book updated with new information

5.7 Conclusion

We’ve completed the following steps:

  • Identified the problem with the data coming from Django
  • Installed JSON API into Django
  • Updated the Books Route Template
  • Created the book detail route and template
  • Successfully viewing, editing, and deleting records from the db from EmberJS client

That’s it. We’ve done it! We built a very basic full stack application using Django and Ember.

What’s Next

Software development is complicated and what we’ve accomplished so far is a series of baby steps. There are really no shortcuts to building your knowledge. You have to take it step by step and break things down. I hope you found this tutorial useful and it serves as a jump off point for you to learn more about Django, Ember and full stack development.

--

--

Michael X

Software Developer at Carb Manager | lookininward.github.io | carbmanager.com | Hiring Now! | @mxbeyondborders | /in/vinothmichaelxavier