Django & EmberJS Full Stack Basics: Connecting Frontend and Backend — Part 4
In Part 4 we shift our attention to the frontend and begin working with Ember. We’ll install the required software, setup a basic DOM, styles, create the book
model, and the books
route. We’ll also load up fake book data for demonstration purposes before we go on to access real data from the backend.
4.1 Install Software
Before we begin frontend development we need to install some software:
4.1.1 NodeJS and NPM
The most straightforward method of installing NodeJS and NPM is using the installation file from the official site.
Once installation is complete check that everything is installed:
node --version
npm --version
4.1.2 Ember CLI
Let’s use NPM to install the Ember CLI, ‘the official command line utility used to create, build, serve, and test Ember.js apps and addons’ :
# install Ember CLI
npm install -g ember-cli
# check that it's installed
ember --version
4.2 Ember Project
4.2.1 Create a new Ember project
Let’s create a frontend client called client
:
# cd into the main project folder
cd ~/desktop/my_library
# create a new app: client
ember new client
# cd into the directory
cd 'client'
# run the server
ember s
Head over to http://localhost:4200
and you should see this screen:
Shutdown the server with ctrl+C
.
4.2.2 Update .gitignore with Ember exclusions
Before we make any new commits, let’s update the .gitignore
file to exclude unwanted files from being pushed to the repo. Add on to the file below the Django section:
...### Ember ###
/client/dist
/client/tmp# dependencies
/client/node_modules
/client/bower_components# misc
/client/.sass-cache
/client/connect.lock
/client/coverage/*
/client/libpeerconnection.log
/client/npm-debug.log
/client/testem.log# ember-try
/client/.node_modules.ember-try/
/client/bower.json.ember-try
/client/package.json.ember-try
4.3 Display books data
4.3.1 Setup the DOM
Now that we have the software installed let’s set up a basic DOM and styles. I’m not doing anything fancy here, just the bare minimum to have our data displaying in a readable format.
Locate the file client/app/templates/application.hbs
. Get rid of {{welcome-page}}
and the comments .
Then create a div
with the class .nav
. Use Ember’s built-in {{#link-to}}
helper to create a link to the route books
(we will create it later):
<div class="nav">
{{#link-to 'books' class="nav-item"}}Home{{/link-to}}
</div>
Wrap everything including the{{outlet}}
in a div
with the .container
class. Each route template will be rendered inside {{outlet}}
:
<div class="container">
<div class="nav">
{{#link-to 'books' class="nav-item"}}Home{{/link-to}}
</div> {{outlet}}
</div>
Since this is the template for the parent level application
route any sub-routes like books
will be rendered inside the {{outlet}}
. This means that the nav
will always be visible on screen.
4.3.2 Create styles
I’m not going to get into the nitty gritty of the CSS. It’s pretty simple to figure out. Locate the file client/app/styles/app.css
and add the following styles:
Variables and Utilities
:root {
--color-white: #fff;
--color-black: #000;
--color-grey: #d2d2d2;
--color-purple: #6e6a85;
--color-red: #ff0000; --font-size-st: 16px;
--font-size-lg: 24px; --box-shadow: 0 10px 20px -12px rgba(0, 0, 0, 0.42),
0 3px 20px 0px rgba(0, 0, 0, 0.12),
0 8px 10px -5px rgba(0, 0, 0, 0.2);
}.u-justify-space-between {
justify-content: space-between !important;
}.u-text-danger {
color: var(--color-red) !important;
}
General
body {
margin: 0;
padding: 0;
font-family: Arial;
}.container {
display: grid;
grid-template-rows: 40px calc(100vh - 80px) 40px;
height: 100vh;
}
Navigation
.nav {
display: flex;
padding: 0 10px;
background-color: var(--color-purple);
box-shadow: var(--box-shadow);
z-index: 10;
}.nav-item {
padding: 10px;
font-size: var(--font-size-st);
color: var(--color-white);
text-decoration: none;
}.nav-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
Headings
.header {
padding: 10px 0;
font-size: var(--font-size-lg);
}
Books List
.book-list {
padding: 10px;
overflow-y: scroll;
}.book {
display: flex;
justify-content: space-between;
padding: 15px 10px;
font-size: var(--font-size-st);
color: var(--color-black);
text-decoration: none;
cursor: pointer;
}.book:hover {
background: var(--color-grey);
}
Buttons
button {
cursor: pointer;
}
Book Detail
.book.book--detail {
flex-direction: column;
justify-content: flex-start;
max-width: 500px;
background: var(--color-white);
cursor: default;
}.book-title {
font-size: var(--font-size-lg);
}.book-title,
.book-author,
.book-description {
padding: 10px;
}
Add/Edit Book Form
.form {
display: flex;
flex-direction: column;
padding: 10px 20px;
background: var(--color-white);
}input[type='text'],
textarea {
margin: 10px 0;
padding: 10px;
max-width: 500px;
font-size: var(--font-size-st);
border: none;
border-bottom: 1px solid var(--color-grey);
outline: 0;
}
Actions
.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 10px 20px;
background-color: var(--color-white);;
box-shadow: var(--box-shadow)
}
4.4 The books route
4.4.1 Create the books route
Now that we have our basic styles and container DOM in place let’s generate a new route that will display all of the books in our database:
ember g route books
The router file client/app/router.js
will now be updated with:
import EmberRouter from '@ember/routing/router';
import config from './config/environment';const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL
});Router.map(function() {
this.route('books');
});export default Router;
4.4.3.2 Load fake data in the model hook
Let’s edit the books route client/app/routes/books.js
to load all books from the database.
import Route from '@ember/routing/route';export default Route.extend({ model() {
return [
{
title: 'Monkey Adventure',
description: 'A tale of high seas monkey adventure.'
},
{
title: 'Island Strife',
description: 'Life in paradise takes a turn for Monkey.'
},
{
title: 'The Ball',
description: 'Monkey encounters a ball. What to do?'
},
{
title: 'Simple Pleasures of the South',
description: 'Monkey reminisces on a life well lived.'
},
{
title: 'Big City Monkey',
description: 'Toronto! Tall buildings and busy people!'
}
]
}});
The model hook is returning an array of objects. This is fake data for demonstration purposes. We will come back here later and load the actual data from the database using Ember Data when we are ready.
4.4.3.3 Update the books route template
Let’s edit the books route template client/app/templates/books.hbs
to display the books that are returned in the model.
<div class="book-list">
{{#each model as |book|}}
<div class="book">
{{book.title}}
</div>
{{/each}}
</div>
Ember uses the Handlebars Template Library. Here we use the each
helper to iterate through our array of books data in model
. We wrap each of the items in the array in a div
with the class book
and access and display it’s title information with book.title
.
4.4.4 Demonstration: books route loading and displaying fake data
Now that we have the DOM, book model, and books route setup with some fake data we can see this running in the browser. Visit localhost:4200/books
and you should see this:
4.4.5 Create application route for redirect
It’s kind of annoying to have to put a /books
to visit the books
route. Let’s generate the application
route and use the redirect
hook to redirect to the books route when we enter the base route.
ember g route application
If prompted to overwrite the application.hbs
template, say no. We don’t want to overwrite the template we already setup.
In client/app/routes/application.js
create the redirect
hook:
import Route from '@ember/routing/route';export default Route.extend({
redirect() {
this.transitionTo('books');
}
});
If you visit localhost:4200
it will now redirect to localhost:4200/books
.
4.5 Display books route with real data
4.5.1 Create an application adapter
We don’t want to use fake data forever so let’s connect to the backend using and adapter and start pulling the books data into the client. Think of adapters as an “object that receives requests from a store and translates them into the appropriate action to take against your persistence layer… usually an HTTP API.”
Generate a new application adapter:
ember g adapter application
Locate the file client/app/adapters/application.js
and update it:
import DS from 'ember-data';
import { computed } from '@ember/object';export default DS.RESTAdapter.extend({
host: computed(function(){
return 'http://localhost:8000';
}), namespace: 'api'
});
The JSONAPIAdapter is the ‘default adapter used by Ember Data’. It transforms the store’s requests into HTTP requests that follow the JSON API format. It plugs into the data management library called Ember Data. We use Ember Data to interface with the backend in a more efficient way. It can store and manage data in the frontend and make requests to the backend when required to update instead of making constant requests to the backend. This helps the user experience feel more fluid with generally faster loading times.
We will use it’s built-in store
service to easily access server
data without writing more complex ajax
or fetch
requests (though those are still necessary for more complex use cases).
Here the adapter is telling Ember Data that it’s host
may be found at http://localhost:8000
namespaced to api
. This means that any calls to the server
will be understood to start with http://localhost:8000/api/
.
4.5.2 Create the book model
Since we are working with Ember Data, we’ll have to generate a book
model so it understands what the data coming from the backend should map to:
ember g model book
Locate the file in client/models/book.js
and define the book
model:
import DS from 'ember-data';export default DS.Model.extend({
title: DS.attr(),
author: DS.attr(),
description: DS.attr()
});
The attributes are the same as those we’ve defined in the backend. We define them again so that Ember Data knows what to expect from the structured data.
4.5.3 Update the books
route
Let’s update the books route by importing the store
service and using it to request data.
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
store: service(),
model() {
const store = this.get('store');
return store.findAll('book');
}
});
4.5.4 Demonstration: books has a CORS issue
Now that we’ve created an application adapter and updated the books
route to query for all books in the data base, let’s see what we’re getting back.
Run both the Django and Ember servers then visit localhost:4200/books
and you should see this in the console:
There seems to be a problem with CORS.
4.5.5 Resolve the Cross-Origin Resource Sharing (CORS) issue
CORS ‘defines a way in which a browser and server can interact to determine whether or not it is safe to allow the cross-origin request’. To keep things simple we are making a request from localhost:4200
(our frontend) to the server
at localhost:8000/api/books
in order to capture our books data. The request is essentially blocked because our frontend is not an allowed origin from which to request the data from our backend endpoint. We can resolve this issue by allowing all requests to pass through during development.
Begin by installing an app that adds CORS headers to responses:
pip install django-cors-headers
Install it into server
's settings.py
file under the INSTALLED_APPS
array:
INSTALLED_APPS = [
...
'books',
'corsheaders'
]
Add it to the top of the MIDDLEWARE
array:
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
Finally, allow all requests to get through during development:
CORS_ORIGIN_ALLOW_ALL = DEBUG
4.5.6 Demonstration: CORS issue resolved, incompatible data format
Visit localhost:4200
and you should see this in the console:
Looks like we solved the CORS issue and we’re receiving a response from server
with the data that we expect:
[
{
"id": 1,
"title": "Conquistador",
"author": "Buddy Levy",
"description": "It was a moment unique in ..."
},
{
"id": 2,
"title": "East of Eden",
"author": "John Steinbeck",
"description": "In his journal, Nobel Prize ..."
}
]
We do get an array of objects in JSON format, unfortunately it’s still not in quite the shape we expect it to be. This is what Ember Data expects:
{
data: [
{
id: "1",
type: "book",
attributes: {
title: "Conquistador",
author: "Buddy Levy",
description: "It was a moment unique in ..."
}
},
{
id: "2",
type: "book",
attributes: {
title: "East of Eden",
author: "John Steinbeck",
description: "In his journal, Nobel Prize ..."
}
}
]
}
Close but not quite there yet.
4.6 Conclusion
We’ve completed the following steps:
- Installed NodeJS and NPM
- Installed the Ember CLI and created a new client project
- Basic DOM setup
- Created a
books
route and template to load and display books - Demonstrated the app running with fake data
- Created an application adapter to connect to the backend and receive data
- Created a
book
model and updated thebooks
route to capture backend data - Demonstrated that the data we are currently getting from the backend isn’t structured in the way that Ember Data expects it to be
In Part 5 we will go back to Django and structure the data (using the Django REST Framework JSON API library) in a way that works with Ember Data. We’ll also update the backend API to return book detail data so that we can look at a single instance of a book record.