EmberJS Tutorial
In this article, we'll show you how to create a neat little web app that runs on the EmberJS framework and uses an API built on Node.js. Read on for more!
Join the DZone community and get the full member experience.
Join For FreeEmberJS was developed by Yehuda Katz. It was initially released in December 2011. EmberJS was also formerly known as SproutCore MVC framework. New applications now run on EmberJS 2 which was released in August 2015. EmberJS 2.0 introduced new APIs and removed deprecated ones from Ember 1. The goal of Ember 2 is to remove badly designed and unnecessarily complicated code from Ember 1. And apps that run on Ember 1.13 without any deprecation warnings should run without issues on Ember 2.0. Currently, many popular products use EmberJS to build their user interfaces. Such platforms include LinkedIn, Yahoo, Zendesk, Square, PlayStation Now, Apple Music, Heroku Dashboard, Twitch, Discourse, IndieHackers and more. There is a comprehensive list of projects using Emberjs on builtwithember.io. EmberJS documentation is very detailed, and there is a vibrant community of users.
Understanding Core Concepts in EmberJS
If you have experience with frameworks like VueJS, Angular, and React, then you'll understand how EmberJS works in a split second. Developers coming from the jQuery world might find it difficult to comprehend at first glance. But if you are familiar with frameworks like Laravel and Rails, then you'll discover a pattern that'll make fall in love with EmberJS.
I'll give a basic overview of these concepts to nourish your understanding of EmberJS. They are:
- Routing
- Templates
- Models
- Components
- Controllers
Routing
Let's take a good look at how a typical EmberJS app works. Our fictitious app is a Student Management System and the URL is https://studember.ng
. One of the URLs in this app is https://studember.ng/students
. This route simply returns the details of all the students registered on this app. Now, check out what happens when the user loads the app for the first time.
Ember router maps the URL to a route handler. The route handler then renders a template and loads a model that is available to the template.
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('students');
});
export default Router;
router
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return [
'David Ajimobi',
'Olorigbeske Ojuyobo',
'Orieja Michael'
]
}
});
Route Handler
You can easily create a route using the Ember CLI's generate
command like so:
ember generate route route-name
This creates a route file at app/routes/route-name.js
, a template for the route at app/templates/route-name.hbs
, and a unit test file at tests/unit/routes/route-name-test.js
. It also adds the route to the router.
Templates
Templates are used to organize the HTML layout of the application. By default, EmberJS uses Handlebars templates. Templates can display properties provided to them from a controller or a component. The screen rendered to the user is composed of handlebars templates. A typical example is this:
<h3> {{title}} </h3>
<ul>
{{#each people as |person| }}
<li>{{person}}</li>
{{/each}}
</ul>
template.hbs
The template extension is .hbs.
Model
Models are objects that represent the underlying data that your application presents to the user. The structure and scope of your app will determine the types and number of models that will present in it.
A typical example is this:
Our student management app might have a Student
model to represent a particular student. Models are also used to persist data. Typically, most models fetch and persist data to a store. The store could be a database on a server or simply a JSON file.
import DS from 'ember-data';
export default DS.Model.extend({
first_name: DS.attr(),
last_name: DS.attr(),
city: DS.attr(),
age: DS.attr(),
});
app/models/student.js
Ember comes with a data management library called Ember Data to help deal with persistent application data. The library requires you to define the structure of the data you wish to provide to your application by extending DS.Model
. At first, using Ember Data may feel different than the way you're used to writing JavaScript applications. Many developers are familiar with using AJAX to fetch raw JSON data from an endpoint, which may appear easy at first. Over time, however, complexity leaks out into your application code, making it hard to maintain. Ember Data helps you manage your models in a simple way as your application grows.
Ember Data gives you a single store that is the central repository of models in your application. Components and routes can ask the store for models, and the store is responsible for knowing how to fetch them.
You can easily create a model using the Ember CLI's generate
command like so:
ember generate model student
This creates a model at app/models/student.js
file:
import DS from 'ember-data';
export default DS.Model.extend({
});
Components
Ember Components consist basically of two parts: a Handlebars template, and a JavaScript file that defines the component's behavior.
Components must have at least one dash in their name, e.g active-list
. This helps Ember differentiate it from native HTML tags. Components control how the user interface behaves. Components are represented in the view with the curly brace rather than the angle tag like this:
{{active-list}}
Ember provides some methods that are triggered at various points from creating a component up until the component is destroyed. This is called the Component's Lifecycle. You can declare methods to hook into the component's lifecycle to control the behavior of components in your app.
On Initial Render, we have:
- init
- didReceiveAttrs
- willRender
- didInsertElement
- didRender
On Re-render, we have:
- didUpdateAttrs
- didReceiveAttrs
- willUpdate
- willRender
- didUpdate
- didRender
On Component Destroy, we have:
- willDestroyElement
- willClearRender
- didDestroyElement
A typical example of a component is this:
import Ember from 'ember';
export default Ember.Component.extend({
init() {
this._super(...arguments);
this.errors = [];
},
didUpdateAttrs() {
this._super(...arguments);
this.set('errors', []);
},
actions: {
required(event) {
if (!event.target.value) {
this.get('errors').pushObject({ message: `${event.target.name} is required`});
}
}
}
});
You can easily create a component using the Ember CLI's generate
command like so:
ember generate component student-list
This creates a component file at app/components/student-list.js
, a template for the component at app/templates/components/student-list.hbs
, and an integration test file at tests/integration/components/student-list-test.js
.
Controllers
Ember Controllers are routable objects meant to decorate a model with display logic. They sit between the template and model to deal with logic and properties that do not belong to the view or the model.
When you have a property that needs to be in the template but doesn't exist in the model, you can place it the controller:
import Ember from 'ember';
export default Ember.Controller.extend({
canDelete: true
});
This property can now be accessed in the template:
{{#if canDelete}}
// Go ahead and delete the student
{{/if}}
The controller can also be used to make model data more readable to the user. An example is returning the full name of the user:
import Ember from 'ember';
export default Ember.Controller.extend({
getFullName() {
return `${this.get('model.firstName')} - ${this.get('model.lastName')}`
}
});
We can just call getFullName
in the template:
<span>{{ getFullName }} is the senior prefect.</span>
You can easily create a controller using the Ember CLI's generate
command like so:
ember generate controller students
This creates a controller file at app/controllers/students.js
, and a unit test file at tests/unit/controllers/students-test.js
.
Next, let's build an application with Emberjs 2.
Our App: Whistle Blower
The app we will build today is called Whistle Blower
. A Whistle Blower is a person who exposes any kind of information or activity that is deemed illegal, unethical, or not correct within an organization that is either private or public. The Whistle Blower app does the following:
- It gives information about whistle blowing activities in your region.
- It's a small community of whistle blowers.
- A guest user on the Whistle Blower app will only have access to basic information about the whistle blowing activities on the landing page.
- An authenticated user will have access to whistle blowers and their profiles.
- An authenticated user will have access to whistle blower meetups/gatherings.
Build The Backend
Let's build an API for our app. We'll quickly build the API with Node.js. The API is simple. This is what we need:
- An endpoint to serve the latest whistle blowing activities around the world -
/api/activities
. - An endpoint to serve whistle blowers and their profiles -
/api/whistleblowers
. - An endpoint to serve whistle blower meetups -
/api/meetups
. - Securing the endpoint that serves whistle blowers profiles and meetups, so that it can only be accessed by registered users.
Go ahead and fetch the Node.js backend from GitHub.
Your server.js
should look like this:
'use strict';
const express = require('express');
const app = express();
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const cors = require('cors');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
const authCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://{YOUR-AUTH0-DOMAIN}/.well-known/jwks.json"
}),
// This is the identifier we set when we created the API
audience: '{YOUR-API-AUDIENCE-ATTRIBUTE}',
issuer: "{YOUR-AUTH0-DOMAIN}",
algorithms: ['RS256']
});
app.get('/api/activities', (req, res) => {
let whistleBlowerActivities = [
// An array of whistleblowing activities
];
res.json(whistleBlowerActivities);
})
app.get('/api/whistleblowers', (req,res) => {
let whistleBlowers = [
// An aray of whistle blowers
];
res.json(whistleBlowers);
})
app.get('/api/meetups', (req,res) => {
let meetups = [
// An array of meetups
];
res.json(meetups);
})
app.listen(3333);
console.log('Listening on localhost:3333');
Check out the full server.js file here.
Your package.json
file should look like this:
{
"name": "whistleblower",
"version": "0.0.1",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"author": "Auth0",
"license": "MIT",
"dependencies": {
"body-parser": "^1.15.2",
"cors": "^2.8.1",
"express": "^4.14.0",
"express-jwt": "^3.4.0",
"jwks-rsa": "^1.1.1"
}
}
Note: Make sure you have
nodemon
installed globally.
Once you have cloned the project, run an npm install
, then use postman to serve your routes like so:
API serving meetups
API serving whistle blowers
API serving whistle blowing activities
The public whistle blowing activities endpoint should be http://localhost:3333/api/activities
.
The private meet up endpoint should be http://localhost:3333/api/meetups
.
The private whistle blower endpoint should be http://localhost:3333/api/whistleblowers
.
Don't worry about the middleware in charge of securing our endpoint for now. We'll deal with that later. Now, let's build our frontend with EmberJS 2.
Build the Front-End With Emberjs 2
EmberJS has a very nice tool for scaffolding your apps. It's called the ember-cli. It's being maintained by the Ember team.
Go ahead and install the ember-cli tool globally like so:
npm install -g ember-cli
After installing globally, go ahead and scaffold a new EmberJS 2 app like so:
ember new whistleblower
Move into the new directory, whistleblower
and run ember serve
to start up your app.
Let's check out the structure of our newly scaffolded app.
whistleblower/
app/ - All the controllers, components, routes and templates reside here
config/ - All environment config files are here
node_modules/ - All the packages required for the emberjs app resides here
public/
tests/ - All the tests file resides here
vendor/
.editorconfig
.ember-cli
.eslintrc.js
.gitignore
.travis.yml
.watchmanconfig
ember-cli-build.js
package.json - File that contains the names of all the packages residing in node_modules folder
README.md
testem.js
Note: We are not writing any tests for this application. It's out of the scope of this tutorial.
We need to remove the default page content that Ember presents in our app. Open theapp/templates/application.hbs
file and remove this:
{!-- The following component displays Ember's default welcome message. --}}
{{welcome-page}}
{{!-- Feel free to remove this! --}}
Now, our app will show a blank screen. Sweet! Let's get started.
Style With Bootstrap
Go ahead and open the app/index.html
file. Here, we will add the link to the bootstrap CSS and JS file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Whistleblower</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link rel="stylesheet" href="{{rootURL}}assets/whistleblower.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
{{content-for "head-footer"}}
</head>
<body>
{{content-for "body"}}
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/whistleblower.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js">
{{content-for "body-footer"}}
</body>
</html>
Ember already provides a directory and a stylesheet file for your custom CSS. So, open app/styles/app.css
and add the CSS here.
That's all about our styling. Next, let's create our routes.
Create the Routes
We need users to be able to access a URL that:
- provides details about whistle blower meetups.
- provides details about whistle blowers.
We also need a callback URL. I'll tell you why we need that later in the tutorial. Let's create these routes ASAP. The ember-cli provides generator commands that make this easy. So go ahead and run the following commands in your terminal:
ember generate route whistle-blowers
ember generate route meetups
ember generate route callback
The route files are generated together with their respective template files. The EmberJS route handler renders a template when a route is invoked.
Building the Nav Component
Let's build the Nav Component. This component will be shared amongst all the pages. Generate the component using the ember-cli:
ember generate component app-nav
This command generates a component and a template for the app nav. Open up app/templates/components/app-nav.hbs
and add this to it:
<nav class="navbar navbar-default">
<div class="navbar-header">
Whistle Blower
</div>
<ul class="nav navbar-nav navbar-right">
<li>
<button class="btn btn-danger log">Log out</button>
<button class="btn btn-info log">Log In</button>
</li>
</ul>
</nav>
Now, next, we need to create a utility file for authentication and fetching API data for our routes from the backend. Well, Ember allows us to create utilities, but this is better suited for services. An Ember service is a long lived Ember object that can be made available in different parts of your application. This is exactly what we need.
Creating Services
We need to create two services, the auth and API service. The former for everything related to user authentication and the latter for fetching API data from our server. Go ahead and create both services using the ember-cli:
ember generate service auth
ember generate service whistleblowerapi
app/services/auth.js
and app/services/whistleblowerapi.js
will be created. Now, open the auth service and add this to it:
app/services/auth.js
import Ember from 'ember';
import decode from 'npm:jwt-decode';
import auth0 from 'npm:auth0-js';
const ID_TOKEN_KEY = 'id_token';
const ACCESS_TOKEN_KEY = 'access_token';
const CLIENT_ID = '{AUTH0_CLIENT_ID}';
const CLIENT_DOMAIN = '{AUTH0_DOMAIN}';
const REDIRECT = '{CALLBACK_URL}';
const SCOPE = '{SCOPE}';
const AUDIENCE = '{API IDENTIFIER}';
export default Ember.Service.extend({
auth: new auth0.WebAuth({
clientID: CLIENT_ID,
domain: CLIENT_DOMAIN
}),
login() {
this.get('auth').authorize({
responseType: 'token id_token',
redirectUri: REDIRECT,
audience: AUDIENCE,
scope: SCOPE
});
},
logout() {
this.clearIdToken();
this.clearAccessToken();
window.location.href = "/";
},
getIdToken() {
return localStorage.getItem(ID_TOKEN_KEY);
},
getAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_KEY);
},
clearIdToken() {
localStorage.removeItem(ID_TOKEN_KEY);
},
clearAccessToken() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
},
// Helper function that will allow us to extract the access_token and id_token
getParameterByName(name) {
let match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
},
// Get and store access_token in local storage
setAccessToken() {
let accessToken = this.getParameterByName('access_token');
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
},
// Get and store id_token in local storage
setIdToken() {
let idToken = this.getParameterByName('id_token');
localStorage.setItem(ID_TOKEN_KEY, idToken);
},
isLoggedIn() {
const idToken = this.getIdToken();
return !!idToken && !this.isTokenExpired(idToken);
},
getTokenExpirationDate(encodedToken) {
const token = decode(encodedToken);
if (!token.exp) { return null; }
const date = new Date(0);
date.setUTCSeconds(token.exp);
return date;
},
isTokenExpired(token) {
const expirationDate = this.getTokenExpirationDate(token);
return expirationDate < new Date();
}
});
Go ahead and install the auth0-js
and jwt-decode
packages from the terminal:
npm install auth0-js jwt-decode --save
Our auth service contains different functions for authenticating using Auth0 hosted lock, saving/extracting tokens, checking expiry date, and checking if a user is logged in or not.
Note: You can fetch a property in a service using the this.get('<name-of-property>')
syntax.
Now, you might have noticed that we are importing them, using this syntax import module from npm:package
. It turns out that CommonJS(Node) module doesn't play nice with ES6 import statements in Ember. It throws an error indicating that the module can't be found. As usual, we got a work around. To get our npm CommonJS version of our node modules to work with our ES6 import, all we have to do is:
ember install ember-browserify
And append npm
to the module name like we did in the code snippet for the auth service above.
Open the whistleblower API service and add this to it:
app/services/whistleblowerapi.js
import Ember from 'ember';
import axios from 'npm:axios';
const ACCESS_TOKEN_KEY = 'access_token';
const BASE_URL = 'http://localhost:3333';
export default Ember.Service.extend({
getMeetups() {
const url = `${BASE_URL}/api/meetups`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
getWhistleBlowers() {
const url = `${BASE_URL}/api/whistleblowers`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
getActivities() {
const url = `${BASE_URL}/api/activities`;
return axios.get(url).then(response => response.data);
},
getAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
});
Install the axios
module via your terminal:
npm install axios --save
Here, we fetched the meetups, whistleblowers, and activities from the API. Ember already provides jQuery by default. So, an alternative is to use
Ember.$.get(url)
instead of axios. I personally love using axios, hence the reason I chose to use it here.
Ember Services are injectable. You can inject them into different parts of your application as the need arises.
Build the Routes
We created our routes earlier. Now, we need to pass data to the templates of these routes. Once a user hits a URL, they should be able to get data presented to them.
Ember provides a model
method in routes that allows us to fetch data and pass it down to the route template. So, we'll add the model method into our routes, inject the API service and call the service methods to provide data to the model hook so that it can be passed down to the templates.
Open app/routes/meetups.js
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getMeetups();
}
});
app/routes/meetups.js
Open app/routes/whistle-blowers.js
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getMeetups();
}
});
app/routes/whistle-blowers.js
Now, we need to create a route for our index page, which is the landing page.
ember generate route index
Open app/routes/index
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getActivities();
}
});
app/routes/index.js
Next, we need to display data in their respective templates.
Bring Templates to Life
Open app/templates/index.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blowing Updates From Across The World</h3>
</div>
<br/>
{{#each model as |update|}}
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"> {{ update.title }} </h3>
</div>
<div class="panel-body">
<p><span class="badge alert-danger"> Location: </span><strong> {{ update.location }} </strong></p>
</div>
</div>
</div>
{{/each}}
We imported the app-nav component into this template to provide navigation menu for our app.
We also looped through the model data coming from the index route using the #each
helper method.
Open app/templates/meetups.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blower Meetups Across The World </h3>
</div>
<br/>
{{#each model as |meetup|}}
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"> {{ meetup.name }} </h3>
</div>
<div class="panel-body">
<p><span class="badge alert-danger"> Location: </span><strong> {{ meetup.date }} </strong></p>
</div>
</div>
</div>
{{/each}}
{{outlet}}
Open app/templates/whistle-blowers.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blowers Across The World </h3>
</div>
<br/>
{{#each model as |whistleblower|}}
<div class="col-lg-2 col-sm-4">
<div class="card hovercard">
<div class="cardheader">
</div>
<div class="avatar">
<img alt="" src="{{whistleblower.avatar}}">
</div>
<div class="info">
<div class="title">
<a target="_blank" href="http://scripteden.com/">{{ whistleblower.name }}</a>
</div>
<div class="desc">
<p><strong>{{whistleblower.level}}</strong></p></div>
<div class="desc">
<p><span class="badge alert-danger"> Uncovered Spoils: </span> {{whistleblower.uncoveredSpoils}} </p></div>
</div>
</div>
</div>
{{/each}}
{{outlet}}
Go to your browser and check out all the routes. They should be displaying the right data:
Landing page
Meetups Route
Whistleblowers route
Published at DZone with permission of Prosper Otemuyiwa, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments