Django & EmberJS Full Stack Basics: Connecting Frontend and Backend — Part 5
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!
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 urlurlpatterns = [
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!
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 atitle
,author
, anddescription
- 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.
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
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.