Table of Contents
-
- What you need to know about this book
- Why Ember?
- Anatomy
- Conventions
- Getting started
-
Hands-on
- Adding a friend resource
- Connecting with a Backend
- A word on Adapters
- Listing our friends
- Adding a new friend
- Viewing a friend profile
- Updating a friend profile
- Deleting friends
- Mockups
- Installing Dependencies
- Articles Resource
- Defining relationships.
- Nested Loans Index
- Lending new articles
- Computed Property Macros
- Using components to mark a loan as returned.
- Implementing auto save.
- Route hooks
- Working with JavaScript plugins
- Components and Addons
- Ember Data
- Computed Properties and Observers
- Driving our application state through the URL
- Testing Ember.js applications
- PODS
- Deploying Ember.js applications
- Updating your project to the latest version of ember-cli
- Notes
What you need to know about this book
This book is a beginners guide to the JavaScript framework Ember.
Audience: Programmers with basic JavaScript knowledge.
In order to understand this book, you should have some basic JavaScript knowledge. If you don’t, check out the book “Speaking JavaScript”1 by Axel Rauschmayer, it’s free online.
Why should I read this book?
This book covers the basic features to help you build Ember applications. It is written in an informal style and tries to show you common pitfalls to then teach you how to solve those problems and why they are bad ideas.
How to read this book
The book should be read sequentially since every chapter depends on features built or introduced in previous chapters.
Can I learn Ember 2.0 with this book?
Yes, this book will help you learn the current version of Ember and will be updated to reflect future changes (without extra cost), just make sure you enable email notifications from leanpub.
But you are still talking about controllers!
If you are following Ember development, you might know controllers are
going away being replaced by something called Routable
Components
. This book talks about controllers but just the necessary
stuff so you can use features that still depend on them, once
Routable Components
are official, the book will be updated to
reflect those changes.
Ember in 2016.
This book is being updated with features from Ember and best practices in 2016. Is still a work in progress, but if you enable notifications from Leanpub or follow me on twitter @abuiles, you’ll get the notification when a new version is release.
You can follow the changes in the following pull request https://github.com/abuiles/ember-101/pull/65.
The upcoming version will use JSON API following the example from JSON API By Example, remove PODS, talk about the upcoming structure for projects and give more importance to the use of components and how to avoid controllers.
Demo code on GitHub
The repository borrowers-2016 contains the code for the application built in the book.
Reporting Errata - Suggesting Edits.
This book is licensed under creative commons, you can report errors or suggest edits in the book’s repository ember-101.
I hope you enjoy the book!
<3 Adolfo Builes (a.k.a @abuiles).
Why Ember?
Ember is an evolving JavaScript framework for creating “ambitious web applications”, it tries to maximize developers’ productivity using a set of conventions in a way that they don’t need to think about common idioms when building web applications.
Using Ember in conjuntion with Ember CLI, allows you to reduce big amounts of glue code, giving you the opportunity to focus on what is most important for you: building your application.
Glue code refers to those things that are not related to your application but that every project requires. For example, you need to test your code, compile your assets, serve your files in the browser, interact with a back-end API, perhaps use third party libraries, and so on. All those things can be automated and, as it is done in other frameworks, some conventions can provide a common ground to begin building your applications.
Having a tool that does that for you not only eases the process of writing your app but also saves you time and money (you don’t have to think about problems that are already solved).
Ember CLI
aims to be exactly that tool. Thanks to Broccoli
2,
waiting time is reduced while your assets compile. QUnit
3 allows
you to write tests, which can then be run with Testem
4.
If you need to deploy your build to production, you’ll get
fingerprint, compression, and some other features for free.
Ember CLI
also encourages the use of ES6(ECMAScript 6)5. It
provides built-in support for modules and integrates easily with other
plugins, allowing you to write your applications using other ES6
features.
Next time you consider wasting your day wiring up all those things we
just mentioned, consider Ember CLI
. It will make your life easier
and you will get support from a lot of smart people who are already
using this tool.
Anatomy
In this chapter we will learn about the main components of Ember CLI.
Ember CLI is a Node.js command line application that sits on top of other libraries.
Its main component is Broccoli, a builder designed to keep builds as fast as possible.
When we run ember server
, Broccoli compiles our project and puts it
in a directory where it can be served using Express.js6, a Node.js library. Express not only serves
files but also extends Ember CLI functionality using its
middlewares. An example of this is http-proxy, which supports
the --proxy
option that allows us to develop against our development
backend.
Out of the box, testing is powered by QUnit and Testem. By navigating to
http:/localhost:4200/tests, our tests run automatically.
We can also run Testem in CI or --development
mode with the ember
test command. We can also use other testing frameworks like Mocha thanks to the ember-cli-mocha
addon.
Ember CLI allows us to write our application using ES6 Modules. The code is then transpiled (compiled)7 to AMD8 and finally loaded with the minimalist AMD loader, loader.js.
We can use CoffeeScript if we want, but it is recommended to use plain JS and ES6 modules where possible. In subsequent chapters, we’ll explore its syntax and features. Ember CLI ships with Babel9 support, which allows us to use next generation JavaScript without extra work.
Finally, we need to cover plugins that enhance the functionality of Broccoli. Each transformation your files go through is done with a Broccoli plugin, e.g. transpiling, minifying, finger-printing, uglifying. You can have your own Broccoli plugins and plug them wherever you like throughout the build process.
Conventions
We will explore some of the basic conventions and best practices both
in Ember
and Ember CLI
.
- Use
camelCase
even if you are writingCoffeeScript
. - Avoid globals as much as possible:
Ember CLI
supportsES6 Modules
out of the box so you can write your app in a modular way. - Create custom
shims
for apps that are not distributed inAMD
format: we will cover this in a subsequent chapter, but the basic idea is to treat libraries that are not written withES6 Modules
as if they were. - Create reusable code: if there is a functionality you are using in several different places, remember that
Ember
offers an Ember.Mixin class that you can then reuse in different parts. If you think other people can benefit from this, create an add-on. - Name your files using kebab-case: Use hyphens instead of underscores to separate words in a file name. For example, if you have a model called
InvoiceItem
,Ember CLI
expects this model to be underapp/models/invoice-item.js
.
Getting started
With this book, we’ll create an app to keep track of items we lend to our friends. It’s a very simple app, but it will allow us to learn Ember. At the same time, we’ll learn how to use Ember CLI generators, work with third party libraries, and write Ember CLI add-ons.
Requirements
- Install
Node.js
. The easiest way is to download the installer from http://nodejs.org/. - Install the ember-inspector. Click here for Chrome or here for Firefox.
- Install watchman for fast watching. We can start it with
watchman watch ~/path-to-dir
. - Make sure you are not required to run
npm
(Node’s package manager) with sudo. To test this, run the following command
If you were prompted to install as sudo, make sure you can run
npm
without it. Tyler Wendlandt wrote an excellent tutorial for
installing npm
without sudo:
http://www.wenincode.com/installing-node-jsnpm-without-sudo.
It’s very important that you are not required to run npm
as sudo,
otherwise you will have problems when running Ember CLI.
All set? Now let’s create our first Ember app.
ember new
Like other command line tools, Ember CLI
comes with a bunch of useful
commands. The first one we will explore is new
, which creates a
new project.
The new command will create a directory with the following structure:
Change directory into your project.
We will cover all the components as we move through this text, but the following are the most important.
-
app
is where the app code is located: models, routes, templates, components and styles. -
tests
is where test code is located. -
bower.json
helps us manageJavaScript
plugins viaBower
. -
package.json
helps us withJavaScript
dependencies vianpm
.
If everything is fine, we can do ember server
and navigate to http://localhost:4200
where we should see a Welcome to Ember
message.
Hands-on
In the following sections we will add models to our application, define the interactions between them, and create an interface to add friends, articles and allow us to keep track of the things we loaned them.
Adding a friend resource
The main model of our application will be called Friend. It represents the people who will borrow articles from us.
Let’s add it with the resource generator.
If we open app/models/friend.js or app/routes/friends.js, we will see that they have a similar structure.
What is that? ES6 Modules! As mentioned previously, ember CLI
expects us to write our code using ES6 Modules. import Foo from
'foo'
consumes the default export from the package foo
and assigns
it to the variable Foo
. We use export default Foo.extend...
to
define what our module will expose. In this case we will export a
single value, which will be a subclass of Foo
.
Now let’s look at the model and route.
In a future version of ember we might be able to be more explicit about the things we want to use from every module. Instead of writing import Ember from ‘ember’, we could have import { Route } from ‘ember/route’ or import { Model } from ‘ember-data/model’. This is currently possible in ES6 using Named Imports and Exports.
What about tests? If we open the test files, we’ll see that they are also written in ES6. We’ll talk about that in a later chapter. Now let’s connect to a backend and display some data.
Connecting with a Backend
We need to consume and store our data from somewhere. In this case, we created a public API (which follows JSON API) http://api.ember-101.com with Ruby on Rails. The following are the API end-points.
Verb | URI Pattern | |
---|---|---|
GET | /friends/:friend_id/relationships/loans(.:format) | |
POST | /friends/:friend_id/relationships/loans(.:format) | |
PUT | PATCH | /friends/:friend_id/relationships/loans(.:format) |
DELETE | /friends/:friend_id/relationships/loans(.:format) | |
GET | /friends/:friend_id/loans(.:format) | |
GET | /friends(.:format) | |
POST | /friends(.:format) | |
GET | /friends/:id(.:format) | |
PATCH | /friends/:id(.:format) | |
PUT | /friends/:id(.:format) | |
DELETE | /friends/:id(.:format) | |
GET | /articles/:article_id/relationships/loans(.:format) | |
POST | /articles/:article_id/relationships/loans(.:format) | |
PUT | PATCH | /articles/:article_id/relationships/loans(.:format) |
DELETE | /articles/:article_id/relationships/loans(.:format) | |
GET | /articles/:article_id/loans(.:format) | |
GET | /articles(.:format) | |
POST | /articles(.:format) | |
GET | /articles/:id(.:format) | |
PATCH | /articles/:id(.:format) | |
PUT | /articles/:id(.:format) | |
DELETE | /articles/:id(.:format) | |
GET | /loans/:lend_id/relationships/article(.:format) | |
PUT | PATCH | /loans/:lend_id/relationships/article(.:format) |
DELETE | /loans/:lend_id/relationships/article(.:format) | |
GET | /loans/:lend_id/article(.:format) | |
GET | /loans/:lend_id/relationships/friend(.:format) | |
PUT | PATCH | /loans/:lend_id/relationships/friend(.:format) |
DELETE | /loans/:lend_id/relationships/friend(.:format) | |
GET | /loans/:lend_id/friend(.:format) | |
GET | /loans(.:format) | |
POST | /loans(.:format) | |
GET | /loans/:id(.:format) | |
PATCH | /loans/:id(.:format) | |
PUT | /loans/:id(.:format) | |
DELETE | /loans/:id(.:format) |
If we do a GET request to http://api.ember-101.com/friends, we will get a list of all our friends.
The previous payload follows JSON API
and returns a list of resource
objects which will be used to populate ember data store.
A word on Adapters
Ember data has two mechanism to translate request to the server and transform incoming or outgoing data, such mechanisms are called an adapter and serializer. By default, ember data uses the DS.JSONAPIAdapter10, which expects your API to follow http://jsonapi.org/, a specification for building APIs in JSON. In our example, we’ll be using this adapter since our API is written using JSON API.
Ember data doesn’t force us to use this adapter, we can work with others or create our own. One of those adapters is the active model adapter, built for people using or following API similars to the ones created with active model serializer, which uses a different convention for keys and naming. Everything is in snake_case and objects are linked in different ways.
There are a bunch of different adapters for different projects and frameworks.
Some of them are:
We can find a longer list of adapters if we search GitHub for ember-data adapters.
Playing with the resolver
Before going deep into our app, let’s talk about something which will be very useful while we build Ember applications and that thing is the resolver.
The resolver is the system in charge of returning whatever Ember requires at different stages, so if we need to load a template, route or service it all goes through the resolver. Normally people don’t need to interact with it directly when building applications, but knowing that it is there and being able to idenfity whatever it is trying to do might probably come handy while debugging our applications.
To see it in action, let’s play with the console and examine how ember tries to resolve things.
First we need to go to config/environment.js
and uncomment ENV.APP.LOG_RESOLVER
. It should look like:
That line will log whatever ember tries to “find” to the browser’s console. If we go to http://localhost:4200 and open the console, we’ll see something like the output below:
That’s the ember resolver trying to find things. We don’t need to worry about understanding all of it right now.
Coming back to the adapter, if we open the ember-inspector and grab the instance of the application route
With the application route instance at hand, let’s have some fun.
Let’s examine what happens if we try to find all our friends:
First, the resolver tries to find an adapter at the model level:
We can use this if we want to change the default behavior of ember data. For example, changing the way an URL is generated for a resource.
Second, if no adapter is specified for the model, then the resolver checks if we specified an application adapter. As we can see, it returns undefined, which means we didn’t specify one:
Third, if no model or application adapter is found, then ember data falls back to the default adapter, the JSONAPIAdapter. We can check the implementation for this directly in the adapterFor function in ember data.
Once ember data has resolved the adapter it tries to follow the logic to fetch all objects for a given resource. In ours we’ll find an error like the following.
The requests failed because we aren’t connected to any backend.
We need to stop the ember server and start again, but this time let’s specify that we want all our API requests to be proxy to http://api.ember-101.com. To do so we use the option –proxy:
Let’s go back to the console and load all our friends, but this time logging something with the response:
If we see ‘Hi from’ followed by a name, we have successfully connected to the backend. The output might be different every time we run it since the API’s data is changing.
Listing our friends
Now that we have successfully made a request to our API, let’s display our friends.
By convention, the entering point for rendering a list of any kind of resource in web applications is called the Index. This normally matches to the Root URL of our resource. With our friends example, we do so on the backend through the following end-point http://api.ember-101.com/friends. If we visit that URL, we will see a JSON list with all our friends.
In our ember application, we need to specify somehow that every time we go to URL /friends, then all our users should be loaded and displayed in the browser. To do this we need to specify a Route.
Routes are one of the main parts of ember. They are in charge of everything related to setting up state, bootstrapping objects, specifying which template to render, etc. In our case, we need a Route that will load all our friends from the API and then make them available to be rendered in the browser.
Creating our first Route.
First, if we go to app/router.js, we will notice that the resource generator added this.route(‘friends’);.
We specify the URLs we want in our application inside the function passed to Router.map. There, we can declare new routes calling this.route.
Let’s check the Routes that we have currently defined. To do so, open the ember-inspector and click on Routes.
By default, ember creates 4 routes:
- ApplicationRoute
- IndexRoute
- LoadingRoute
- ErrorRoute
We also see that the friends route was added with this.route(‘friends’). if we pass a function as second or third argument, ember will create an Index, Loading, and Error Route.
We can modify our route passing an empty function
And then if we check the inspector, the children routes are automatically generated for the friends route.
Since we just added a friends index route, visiting http://localhost:4200/friends should be enough to list all our friends. But if we actually go there, the only thing we will see is a message with Welcome to Ember.
Let’s go to app/templates/friends.hbs and change it to look like the following:
For people familiar with Ruby on Rails, {{outlet}} is very similar to the word yield in templates. Basically it allows us to put content into it. If we check the application templates (app/templates/application.hbs), we’ll find the following:
When ember starts, it will render the application template as the main template. Inside {{outlet}}, it will render the template associated with the Route we are visiting. Then, inside those templates, we can have more {{outlet}} to keep rendering content.
In our friends scenario, app/templates/friends.hbs will get rendered into the application’s template {{outlet}}, and then it will render the friends index template into app/templates/friends.hbs {{outlet}}.
To connect everything, let’s create an index template and list all our friends. Let’s run the route generator ember g route friends/index and put the following content inside app/templates/friends/index.hbs:
Next, we need to specify in the friends index route the data we want to load in this route. The part in charge of loading the data related to a route is called the model hook. Let’s add one to app/routes/friends/index.js as follows:
If we visit http://localhost:4200/friends we will see something like the following along with a list of our friends:
We played previously with store.findAll to load all our friends from the API and that’s what we are doing in the model hook. ember waits for this call to be completed. When the data is loaded, it automatically creates a friends index controller (or we can define a controller explicitly) and sets the property model with the content returned from the API.
We can also use store.findRecord
or store.queryRecord
if we want
to load a record by a given id or appending query parameters to the
request URL, such as this.store.findRecord(‘friend’, 1) or
this.store.query(‘friend’, {active: true}), which creates
the following requests to the API /api/friends/1 or
/api/friends?active=true.
When we do {{#each model as |friend|}}, ember takes every element of the collection and set it as friend, the collection which is what the model hook returned is referenced as model.
If we want to display the total number of friends and the id for every friend, then we just need to reference model.length in the template and inside the each use friend.id:
Again, because our model is a collection and it has the property length, we can just reference it in the template as model.length.
Adding a new friend
We are now able to list our friends, but we don’t have a way to add new friends. The next step is to build support for adding a new friend.
To do this we’ll need a friends new route under the route friends, which will handle the URL http://localhost:4200/friends/new.
To add the new route, let’s run the route generator with the parameters friends/new:
If we go to app/router.js we’ll see that the new route was nested under the route friends:
Let’s add the following content on the new template:
And then navigate to http://localhost:4200/friends/new:
Notice how the friends new route got rendered in the {{outlet}} inside app/templates/friends.hbs.
We got our route and template wired up, but we can’t add friends yet. We need to set a new friend instance as the model of the friends new route, and then create a form that will bind to the friend’s attributes, and save the new friend in our backend.
Following the logic we used in the friends index route, we need to return the model that will be the context of the friends new route.
We need to edit app/routes/friends/new.js and add the following model hook:
We have been using the this.store without knowing what it is. The Store is an ember data class in charge of managing everything related to our model’s data. It knows about all the records we currently have loaded in our application and it has some functions that will help us to find, create, update, and delete records. During the whole application life cycle there is a unique instance of the Store, and it is injected as a property into every Route, Controller, Serializer, and adapter under the key store. That’s why we have been calling .store in our Routes and Controllers.
The method we are using on the model hook store.createRecord
creates a new record in our application store, but it doesn’t save
it to the backend. What we will do with this record is set it as the
model of our friends new route. Then, once we have filled the
first and last names, we can save it to our backend calling the method
#save()
in the model.
Since we will be using the same form for adding a new friend and
editing, let’s create an
Ember component
to contain the form, we can generate the component with the generator,
ember g component friends/edit-form
and add the following content:
Then we should modify the template app/templates/friends/new.hbs to include the component:
Now if we visit http://localhost:4200/friends/new, the form should be displayed.
There are some new concepts in what we just did. Let’s talk about them.
Components
In app/templates/friends/new.hbs we used
This is how components are rendered, we’ll have a whole section to talk about components, but for now let’s say that they are isolated “templates”, they don’t know anything about the context surrounding them, so we need to pass down all the necessary data for it to display correctly. In our example, the component required a property called “model” to work, so we are assigning our “current context” model to the component’s model.
The friend form is a perfect candidate for a component since we will be using the same form to create and edit a new friend. The only difference will be how “save” and “cancel” will behave under both scenarios.
{{action}}
The {{action}} helper is one of the most useful features in ember. It allows us to bind an action in the template to an action in the component, controller or route. By default it is bound to the click action, but it can be bound to other actions.
The following button will call the action cancel when we click it.
And <form {{action “save” on=”submit”}}> will call the action save when the onsubmit event is fired; that is, when we click Save.
If we go to the browser http://localhost:4200/friends/new, open the console, and click Save and Cancel, we’ll see two errors. The first says Nothing handled the action ‘save’ and the second Nothing handled the action ‘cancel’.
Ember expects us to define our action handlers inside the property actions in the component, controller or route. When the action is called, ember looks for the definition in the current context, so if we are inside the component, it will look at the component.
Let’s go to the component and add the actions save and cancel.
If we go to http://localhost:4200/friends/new and click save, we’ll see in the browser’s console “save action in edit-form component”.
This action is running on the context of the component so if we do
this.get('model')
we’ll get the record created on the model’s hook
because we passed it down as an argument when rendering the component.
A component not only receives objects but we can also pass it actions, by default the actions need to be specified in the context where we are calling it, and to do so we use the action helper too.
Let’s edit our friends/new
template to add the save
and cancel action, it should look like the following now:
After adding the actions, we’ll see the following error in the console:
The issue here is that we didn’t specify an action in the
friends/new
controller. As mentioned previously controllers will be
replaced eventually but for now if we want to connect a component with
its surrounding context then we need to use controllers too.
We can create a controller using the controller generator like ember
g controller friends/new
and then let’s add the save
and cancel
actions:
Now the route renders again but we won’t see the actions in the controller being called yet, the reason is that we need to call the passed action from the component’s action. To do so, let’s change our component as follows:
How is this related to creating a new friend in our API? We’ll discover that after we cover the next helper. On the save action in the component, we’ll validate our model, and if it is valid call the action in the controller which will take care of calling .save(), which saves it to the API, and finally transition to a route where we can add new articles.
The input helper
Last we have the input helper. It allows us to automatically bind an html input field to a property in our model. With the following {{input value=firstName}}, changing the value changes the property firstName.
Let’s modify our component’s template to include the following before the form:
And then go to the browser, we’ll see that every time we change the first or last name field, this will change the description in Friend details.
We can also use the input helper to render other types of input such as a checkbox. To do so, simply specify type=’checkbox’.
If we click the checkbox, the attribute trusted will be true. Otherwise, it will be false.
Save it!
We learned about actions, {{component}}, and {{input}}. Now let’s save our friend to the backend.
To do so, we are going to validate the presence of all the required
fields. If they are present, call the action save
which will call .save() on the model.
Otherwise, we’ll see an error message on the form.
First we’ll modify app/templates/components/friends/edit-form.hbs to include a field {{errorMessage}}.
We will see the error every time we try to save a record without first filling in all the fields.
Then we’ll implement a naive validation in app/components/friends/edit-form.js by adding a computed property called isValid:
Ember.computed? That’s new! ember allows us to create functions that will be treated as properties. These are called computed properties. In our example, isValid is a computed property that depends on the properties model.email, model.firstName, model.lastName, and model.twitter. When any of those properties changes, the function that we passed-in is called and the value of our property is updated with the returned value.
In our example, we are manually checking that all the fields are not empty by using the isEmpty helper.
With our naive validation in place, we can now modify our save and cancel actions:
When the action save is called, we are first checking if isValid is true, then we get the model and call .save(). The return of save() is a promise, which allows us to write asynchronous code in a sync manner. The function .then receives a function that will be called when the model has been saved successfully to the server. When this happens, it returns an instance of our friend and then we can call the save action specified in the controller, in this function we’ll put the logic to transiton to the route friends show where we can see our friend’s profile.
If we click save and have filled all the required fields, we’ll see
that nothing happens after saving a new friend, but the action save
was called in the controller. Let’s change the controller as follows
to include the transition logic:
If we save a friend once more, we’ll get an error: The route
friends/show was not found
. This is because we haven’t defined a
friends show route. We’ll do that in the next chapter.
Whenever we want to access a property of an ember object, we need to use this.get(‘propertyName’). It’s almost the same as doing object.propertyName, but it adds extra features like handling computed properties. If we want to change the property of an object, we use this.set(‘propertyName’, ‘newvalue’). Again, it’s almost equivalent to doing this.propertyName = ‘newValue’, but it adds support so the observers and computed properties that depend on the property are updated accordingly.
Viewing a friend profile
Let’s start by creating a friends show route
If we open app/router.js, we’ll see the route show nested under friends.
We have talked previously about path but not about dynamic segments. path: ‘:friend_id’ is specifying a dynamic segment. This means that our route will start with /friends/ followed by an id that will be something like /friends/12 or /friends/ned-stark. Whatever we pass to the URL, it will be available on the model hook under params, so we can reference it like params.friend_id. This will help us to load a specific friend by visiting the URL /friends/:friend_id. A route can have any number of dynamic segments (e.g., path: ‘/friends/:group_id/:friend_id’.)
Now that we have a friends show route, let’s start first by editing the template in app/templates/friends/show.hbs:
According to what we have covered, the next logical step would be to add a model hook on the friends show route by calling this.store.findRecord(‘friend’, params.friend_id). However, if we go to http://localhost:4200/friends/new and add a new friend, we’ll be redirected to the friends show route and our friend will be loaded without requiring us to write a model hook.
Why? As we have said previously, ember is based on convention over configuration. The pattern of having dynamic segments like model_name_id is so common that if the dynamic segment ends with _id, then the model hook is generated automatically and it calls this.store(‘model_name’, params.model_name_id).
Visiting a friend profile
We can navigate to http://localhost:4200/friends to see all of our friends, but we don’t have a way to navigate to their profiles!
Fear not, ember has a helper for that as well, and it is called {{link-to}}.
Let’s rewrite the content on app/templates/friends/index.hbs to use the helper:
When we pass our intended route and an instance of a friend to link-to, it maps the property id to the parameter friend_id(we could also pass friend.id). Then, inside the block, we render the content of our link tag, which would be the first and last name of our friend.
One important item to mention is that if we pass an
instance of a friend to link-to, then the model hook in the
friends show route won’t be called. If we want the hook to be
called, instead of doing {{#link-to 'friends.show' friend}}
, we’ll
have to do {{#link-to 'friends.show' friend.id}}
.
The resulting HTML will look like the following
If our friend model had a property called fullName, we could have
written the helper with this other form of link-to
which doesn’t
include the block:
We already talked about computed properties, so let’s add one called fullName to app/models/friend.js
The computed property depends on firstName and lastName. Any time either of those properties changes, so will the value of fullName.
Once we have the computed property, we can rewrite link-to without the block as follows:
Now we’ll be able to visit any of our friends! Next, let’s add support to edit a friend.
Quick Task
- Add a link so we can move back and forth between a friend’s profile and the friends index.
- Add a link so we can move from app/templates/index.hbs to the list of friends (might need to generate the missing template).
Updating a friend profile
By now it should be clear what we need to update a friend:
- Create a route with the ember generator.
- Update the template.
- Add Controller and actions.
To create the friends edit route we should run:
The nested route edit should looks as follows under the the resource friends:
Then we should modify the template app/templates/friends/edit.hbs to render the edit friend component:
With that in place, let’s go to a friend’s profile and then append /edit in the browser (e.g., http://localhost:4200/friends/2/edit.)
Thanks to the component, we have the same form as in the new
template without writing anything extra. If we open the browser’s
console and click on Save and Cancel, we’ll see error because
we didn’t pass down the save
and cancel
actions which our
component depends on.
Let’s create a friends.edit
controller and implement those
actions. The save action will behave exactly as the one in
new. The action cancel will be different; instead of
redirecting to the friends index route, we’ll redirect back to the
profile page.
We’ll create the controller using ember g controller.
And then we can add the save and cancel actions:
After adding the actions, if we go to the edit template and click
cancel or save we’ll still see an error. The problem is that we didn’t
specify the actions when rendering the component. Let’s change the
edit form to pass the actions save
and cancel
.
If we refresh our browser, edit the profile, and click save, we’ll see
our changes applied successfully. But if we click cancel it won’t
work as expected. The reason is that cancel
need to receive the
model as parameter.
We can fix it by going to the friends/edit-form
component and call
cancel with the model.
To transition from a controller, we have been using this.transitionToRoute. It’s a helper that behaves similarly to the {{link-to}} helper but from within a controller. If we were in a Route, we could have used this.transitionTo.
Visiting the edit page.
We can edit a friend now, but we need a way to reach the edit screen from the user profile page. To do that, we should add a {{link-to}} in our app/templates/friends/show.hbs.
If we go to a friend’s profile and click Edit info, we’ll be taken to the edit screen page.
Deleting friends
We have decided not to lend anything to a couple of friends ever again after they took our beloved The Dark Side of the Moon vinyl and returned it with scratches.
It’s time to add support to delete some friends from our application. We want to be able to delete them directly within their profile page or when looking at the index.
By now it should be clear how we will do this. Let’s use actions.
Our destroy actions will call model#destroyRecord() and then this.transitionTo to the friends index route.
Let’s replace our app/templates/friends/index.hbs so it includes the delete action:
And then add the action *delete to the controller.
To support deletion on friends show route, we just need to add the same link with the action delete and implement the action in the controller.
With that we can now create, update, edit, and delete any of our friends!
Next, let’s add some styling to our project. We don’t want to show this to our friends as it is right now.
Mockups
Before changing our templates, we’ll review a couple of mockups to have an idea of how our pages are going to look.
Friends Index
We’ll have a header that will take us to a dashboard, the friends index page, and about page. Additionally, we can insert some content depending on which route we are visiting. In the Friends Index Route we’ll see a search box to filter users.
Then we’ll have a table that can be ordered alphabetically or by number of items.
friend profile
Our friend profile will show us the user’s data with an avatar that we might pull from Gravatar.
We have links to add new articles, edit the user’s info, or delete the user’s profile.
At the bottom we’ll have the list of all the articles the user has borrowed with options to mark them as returned or to send a reminder.
If we are careful, we’ll also notice that the URL looks a little different from what we currently have. After the friend id, we see /articles (..com/friends/1/articlesloans). Whenever we visit the user profile, the nested resource articles will be rendered by default. We haven’t talked about it yet, but basically we are rendering a resource under our friends show route that will defer all responsibility of managing state, handling actions, etc. to a different Controller and Route.
Installing Dependencies
To save time, we’ll be using Basscss as our base CSS and fontello for icons.
Including Basscss
Basscss is distributed through npm
or downloading directly from a CDN, but for our example we want to compile it ourselves and put it into our vendor.css
. To do, let’s download the source file and put it in the directory vendor
.
The fact that a file is in the vendor directory doesn’t mean that
they’ll be included in our assets. We still need to tell ember CLI
that we want to import those assets into our application. To do
so, we need to add the following line to our ember-cli-build.js before
return app.toTree();
app.import is a helper function that tells ember CLI to append vendor/basscss.min.css into our assets. By default it will put any **CSS file we import into ****/vendor.css** and any JavaScript file into /vendor.js.
If we check app/index.html, we’ll see 2 CSS files included:
The first one contains all the imported (vendor) CSS files and the second one contains the CSS files defined under app/styles.
After modifying our ember-cli-build.js
we need to stop and start the
server again so the changes are applied. Once we have done that, we
can refresh our browser and go to
http://localhost:4200/assets/vendor.css, we’ll see that the code
for Basscss is there.
Including fontello
Because fontello doesn’t have a custom distribution either, we can’t download it with bower, we’ll download a bundle of icons and fonts that we can manage manually by putting it under vendor/fontello.
We can download a bundle from the following URL https://www.dropbox.com/s/bo2gi770ydxjc9v/fontello.zip?dl=0 and then put the content under vendor/, which will give us the directory vendor/fontello.
In order to tell ember CLI that we want to include fontello’s CSS and fonts, we need to modify our Brocfile as follows:
We are already familiar with the line to import fontello.css, but the following ones are new to us since we have never passed any option to import.
The option destDir tells ember CLI that we want to put those files under a directory called assets/font. If we save and refresh our browser, vendor.css should now include fontello.css.
With that, we know the basics of including vendor files. Now that we have our basic dependencies on hand, let’s improve the appearance of our templates.
The header
We’ll use components to simplify our templates. In this case, our
component contains the code for the navigation bar. Let’s create a component call nav-bar
and add the following content:
The header should always be visible in our application. In ember, the right receptacle for that content would be the application template since it will contain any other template inside its {{outlet}}.
Modify app/templates/application.hbs as follows:
We will render the header and wrap the outlet in a row using basscss classes.
If we refresh, the header should display nicely.
Friends Index
First, let’s remove the <h1> from app/templates/friends.hbs so it only contains {{outlet}}. Next, clean up app/templates/friends/index.hbs so it adds the class primary to the table:
Now if we visit http://localhost:4200/friends, our friends and navigation bar should look nicer.
New Friend And Friend profile template
Let’s edit app.css
with the following:
And then edit the friends/edit-form
component:
And let’s center friends/new
with the following:
And finally, change app/templates/friends/show.hbs.
The Dashboard
By default, we’ll use the application index route as the dashboard. For now, we are going to create the file app/templates/index.hbs and write a simple title:
Let’s move on with more functionality.
Articles Resource
With our Friends CRUD ready, let’s create a similar interface to add articles to our system.
Let’s create an articles resource:
Let’s check the model:
We have defined our Articles model successfully, but we need to create a full interface so we can add or remove articles to our system. We’ll add two more tabs to our navigation header, one to list all articles and the other one to add new ones.
After adding this, we’ll see an error in the console because we haven’t created yet the route for new articles.
Let’s create the routes for creating, editing and showing articles. They will be very similar to what we did for friends.
And then we can wire the functionality following a similar pattern to
the one we did for friends. Add a model hook in the index and then
render every article. Create a edit-form
component for articles. Use
the edit-form
in the new
and edit
route.
The articles index
route should look like the following:
And for the template we can reuse the one from friends:
Next we need to create the component for the article form:
And add the following to the component’s template:
Next, let’s modify the component so it does the validation and saves the model:
Then, we need to change the articles new template to render the component:
If we go to http://localhost:4200/articles/new we’ll see an error in the conole saying: An action could not be made for
save in <borrowers@controller:articles/new
.
If we look again at the template above, we’ll notice that the syntax to call the action is slighty different to the one we use in friends new. Instead of passing the name of the action as a string, we are calling it as if it were a property in the controller.
This is another way of using actions, and they are called closure
actions. Introduced in the RFC
0050-improved-actions.md,
these allow us to pass functions directly as actions. This means we
don’t need to define the action in the action
object and we can
just bind any function defined in the controller.
To make save
and cancel
work, let’s create the the articles new
controller running ember g controller articles/new
and then add the
following content:
Once we have defined the actions in the controller then the template should render. Next, if we try to save a new article, we’ll see that it doesn’t work. The reason is that we didn’t create a model hook on the articles new route. Let’s do that next:
We will leave as a task the rest of the routes, templates and actions. We still need to support edit, delete and show for articles. Also, use closure actions.
Defining relationships.
We’ll be using a join model to keep track of who borrowed what. The name for this join model will be “loan”. A loan belongs to one friend and one article. Friends and articles can have many loans.
In other frameworks this is known as hasMany and belongsTo relationships, and so are they in ember data.
If we want to add a hasMany relationship to our models, we write:
Or we want a belongsTo:
Let’s run the resource generator to create the loan model:
If we open the loan model, it will look something like the following:
Next, using the relationship types, we can modify our Article model:
And our Friend model:
With those two lines, we have added a relationship between our models. Now let’s work on the loans resource.
Nested Loans Index
In our friend profile, we specified that we wanted to render the list of articles as a nested route inside the friend profile.
To handle this scenario. We need to make sure that loans is specified as a nested resource inside friends show. Let’s go to our app/router.js and change it to reflect this:
Now let’s open the ember-inspector and check our newly defined routes.
We can identify the routes and controllers that ember expects us to define for the new resource.
Next we need to add an {{outlet }} to app/templates/friends/show.hbs, which is where the nested routes will render:
Any nested route will be rendered by default into its parent’s {{outlet}}.
Rendering the index.
Let’s create a new file called app/templates/loans/index.hbs and write the following:
If we visit a friend profile, we won’t see anything related with the loans index route. Why? Well, we are not visiting that route, that’s why. To get to the loans index route, we need to modify the link-to in app/templates/friends/index.hbs to reference the route loans instead of friends.show. We’ll still pass the friend as an argument since the route loans is nested under friends.show and it has the dynamic segment :friend_id.
Now, with the previous change, if we go to the friends index and visit any profile, we’ll see Loans Index at the bottom.
If we open the ember-inspector and filter by *Current Route
only**, we’ll see loans.index
at the last route.
Routes are resolved from top to bottom, so when we navigate to /friends/1/loans it will go first to the application route, then move to friends show route to fetch our friend. Once it is loaded, it will move to loans index route.
Next we need to define the model hook for the loans index route.
Fetching our friend loans.
Let’s add the loans index route to the generator and reply ‘no’ when it asks us if we want to overwrite the template.
In app/routes/loans/index.js, load the data using the model hook:
In the model hook, we are using a new function this.modelFor that helps us grab the model for any parent route. In this scenario, parent routes are all the ones appearing on top of articles index route in the ember-inspector.
Once we get the model for friends show route, we simply ask for its loans. And that’s what we are returning.
We need to modify the app/templates/loans/index.hbs so it displays the loans:
If our friend hasn’t borrowed any article yet, we can use the ember-inspector to create a loan manually.
Let’s open the ember-inspector and select the model in the route friends.show
.
Once we have the instance of a friend assigned to the variable $E, let’s run the following on the browser’s console:
We will see that our loans list updates automatically with the created records.
So far we are only putting records into the store, but they are not being saved to the backend. To do that we’ll need to call save() on every instance. Let’s try to call save:
We will notice that a POST is attempted to our backend, but it gets rejected because the model is not valid:
And if we look at the response in the network tab, we’ll see that
there is an error with the message article - can't be blank
. Let’s
grab an article and then create a loan including the article.
In the previous snippet, we are using two methods from ember data’s
store, the first makes a request to download all the available
articles in the API and puts them in the store, and the second one is
“fetching” all the records from the store (without firing a HTTP
request) and we call firstObject
with returns the first record in the
list.
Then, with that record we are creating a new loan.
Let’s add the route loans new and the template so we can lend new articles to our friends.
Lending new articles
Let’s start by adding the route. We’ve done it with the generator up to this point, but now we’ll do it manually.
We need to add the nested route new under the resource loans:
Then let’s create the route app/routes/loans/new.js with the model hook:
In the model hook we use this.store.createRecord, which creates a new instance of a model in the store. It takes the name of the model we’re creating and its properties.
We pass the property friend, which will make sure that the loan is linked with our friend. For the article we still need to add something like a select where the user can choose an article.
Ember data allows us to specify a defaultValue for our attributes. We can use that to set notes as an empty string. In app/models/loan.js, let’s replace the definition of notes so it looks as follows:
Next we need to add the new template. Since we might want to reuse the form, let’s add it as a component and then include it in the template.
We can follow a similar pattern to the one used with articles and friends. Let’s create a component called loans/edit-form and then pass the necessary data for it to function properly.
Let’s run the following command: ember g component loans/edit-form
and then edit the template with the form.
Then include it in app/templates/loans/new.hbs:
We almost have the basics ready. We have set up the route and template, but we still haven’t added a link to navigate to the articles new route. Let’s add link-to to articles.new in app/templates/friends/show.hbs:
We are creating the link with {{link-to "Lend article"
"loans.new"}}
. Since we’re already in the context of a
friend, we don’t need to specify the dynamic segment. If we want to add
the same link in the friends index route, we’ll need to
pass the parameter as {{link-to “Lend article” “loans.new”
friend}} where friend is an instance of a friend.
Completing the form
We have the route, template and component for new loans but the select
is not showing the list of articles that we can loan to a
friend. Let’s build that next, and then connect the cancel
and
save
actions.
Instead of building the select ourselves, we are going to use an addon called ember-power-select which give us a nice select component.
Ember addons offer us an easy way to share code and augment ember-cli, we’ll be building one in a subsequent chapter, but for now, let’s start consuming them.
We can install the addon running the ember install command, we need to stop the ember-cli server and then run the following:
Once our addon has been installed, we can edit the loans/edit-form
to use it:
As we can see, the addon takes as options the current selected option,
which in our example would be model.article
. Then it takes the list
of options which will be used to populate the select and also has an
action which gets called every time the selection changes.
In the action we are using the mut
helper which give us a special
action to update a property. The following onchange=(action (mut
model.article))
can be read as “On change, change the property
model.article
which the selected valued”. We are not seeing the
parameter, but the addon passes it explicitly.
We could have also written a function in the component like the following:
And then pass it as the onchange
action: onchange=(action changeArticle)
.
If we start the server and go to the form for new loans, we’ll see that the select is being displayed but the articles are not listed, the reason for that is that we haven’t populated the articles attribute.
To do so, we’ll be creating a computed property in the component and then calling store.findAll
, let’s go to loans/edit-form
component an add the following:
Now if we refresh again, we’ll see that the list of articles are now in the select.
Next, we need to bind the new
and cancel
actions.
After successfully making a new loan or clicking cancel, we want to
navigate back to the loans.index
for the friend. To do so, let’s
write an action in the controller that we can use in both scenarios.
Let’s create the controller using the generator: ember g controller
loans/new
and then add the following content:
Then in the loans.new
template, pass down the action:
And finally we need to connect the back
action in the component and
change the way we call the save
and cancel
actions:
To finish, we need to change the save
and cancel
actions so we use
the closure action format instead of “quoted” format.
If we go to a friend profile and click “Lend article”, we’ll be able to create a new loan.
Computed Property Macros
In app/components/friends/edit-form.js, we define the computed property isValid with the following code:
Although the previous code does what we expect, it is not the most pleasant to read, especially with all those nested &&’s. As it turns out, Ember has a set of helper functions that will allow us to write the previous code in a more idiomatic way using something called computed property macros.
Computed property macros are a set of functions living under Ember.computed. that allow us to create computed properties in an easier, more readable and clean way.
As an example, let’s take two computed property macros and write our isValid on terms of them:
This is certainly much cleaner and less error-prone than original implementation.
We can see the full list of computed properties under the Ember.computed namespace.
Using components to mark a loan as returned.
We previously lent our favorite bike to one of our friends and they just returned it. We need to mark the item as returned.
We’ll add a select in the loans index, so we can mark an item as returned or borrowed. Whenever that loan has pending changes, we’ll see a save button.
Using components we’ll encapsulate the behavior per row into its own class, removing responsibility from the model and delegating it to a class. This class will handle how every row should look and additionally when it should fire a save depending on the state of every loan.
We’ll create an loans/loan-row component which will wrap every element. We’ll pass the necessary data to render the list of possible states and also the loan.
Let’s create the loans/loans-row using the components generator.
Let’s modify the component so it looks as follows:
We are specifying that the html tag for this component is going to be a
tr
meaning that whatever content we put in the template, it will be
wrapped in table row using the HTML tag tr, by default it is a
div. Also we defined two properties loan
and loanStates
with value null
and the comment: “passed-in”. It will help people
consuming the component to identify which data they should pass-in.
We need to add the the markup for the component as follows:
In the template we are defining the cells for every loan row and
reading the value from the “passed-in” property loan, also we are
calling the action save
, which we are passing in.
We are also using the properties loan.isSaving and loan.hasDirtyAttributes, which belong to the loan we passed-in.
The previous properties are part of DS.Model and they help us to know things about a model. In the previous scenario, loan.hasDirtyAttributes becomes true if there is a change to the model and loan.isSaving is true if the model tries to persist any changes to the backend.
Before using our components, let’s create the loans.index
controller
and add a function called save
which we’ll use to save changes in a
loan.
Let’s add the following content after running ember g controller loans/index
:
Now let’s use our component in the loans index template:
We are iterating over every loan in the model and then rendering an loans-row component for each of them, we are passing as attributes the loan, bounding the save action to another action which is also called save.
If we open the ember-inspector, open the view tree and then select components, we will notice that every component is displayed independently.
If we go to the browser and try what we just created, everything should work.
Implementing auto save.
Instead of clicking the save button every time we change the state of the model, we want it to save automatically.
We’ll rewrite our template so the save button is not included and then use the input helper action to call save.
We are doing something new here, we are passing the argument to the
action in the action itself like action save loan
, this is one of
the cool things we can do with closure actions.
Now, every time we change the state of the loan, the save
action will be called.
Route hooks
If we go to http://localhost:4200/friends/new and click cancel without entering anything, or we write something and then click cancel, we’ll still see the unsaved record in our friends index. It only goes away if we refresh the app.
The same happens with an article. If we try to create one but we click cancel, it will appear in the index anyway.
The ember data store not only keeps all the data we load from the server, but it also keeps the one we create on the client. We were actually pushing a new record to the store when we did the following on the friends new route:
Such records will live in the store with the state new. We can call save on it, which will persist it to the backend and make it move to a different state, or we can remove it and our backend will never know about it.
We might ask ourselves: but aren’t we doing a store.findAll on the friends index route, which loads our data again from the server? And shouldn’t that remove the unsaved records?
That’s partially true. It is correct that when we do this.store.findAll(‘friend’), a GET request is made to the server. When we load our existing records again, instead of throwing out all the records in the store, ember data merges the results, updating existing records and leaving untouched the ones that the server doesn’t know about. That’s why we see the new but unsaved record in the index.
To mitigate this situation, if we are leaving the friends new route and the model was not saved, we need to remove the record from the store. How do we do that?
Ember.Route has a set of hooks that are called at different times during the route lifetime. For instance, we can use activate to do something when we enter a route, deactivate when we leave it or resetController to reset values on some actions.
Let’s try them in app/routes/friends/new.js:
And then visit http://localhost:4200/friends/new and click cancel or friends.
We should see something like the following in our browser’s console:
Coming back to our original problem of the unsaved record in the store, we can use the resetController hook to clean up our state.
Let’s rewrite app/routes/friends/new.js so the resetController hook does what we expect:
Another scenario where it is common to use the resetController hook
involves the edit routes. For example, if we try to edit a friend
and don’t save the changes but click cancel, the friend profile will
show whatever change we leave unsaved. To solve this problem we’ll use
the resetController hook, but instead of checking if the model
isNew, we’ll call model.rollback(). This will return the
attributes to their initial state if the model hasDirtyAttributes
.
Working with JavaScript plugins
In this chapter we’ll learn how to write Ember helpers that can be consumed in our templates. To do so, we’ll write a helper called format-date that will show the date when an article was borrowed. Instead of showing Sun Sep 28 2014 04:58:30 GMT-0500, we’ll see September 28, 2014.
We’ll implement format-date using Momentjs, a library that facilitates working with dates in JavaScript.
Installing moment
Remember that ember-cli uses Bower to manage frontend dependencies. Here we’ll use the same pattern used to install picnicss: we’ll add moment to Bower and then use app.import in our ember-cli-build.js.
First, we install moment:
The option --save
adds the dependency to our bower.json. We
should find something similar to “moment”: “~2.13.0” (the version
might be different).
Next, let’s import moment. To find out which file to import, let’s go to bower_components/moment/. We’ll see that it contains a moment.js file that is the non-minified version of the library. We can also point to any of the versions under the directory min/. For now, let’s use the non-minified.
Let’s add the following to our ember-cli-build.js:
Next, if we navigate to http://localhost:4200, open the console, and type “moment” we should have access to the moment object.
We have successfully included our first JavaScript plugin, but we need to be aware of some gotchas.
It’s a global!
At the beginning of the book, we mentioned that one of the things ember-cli gives you is support to work with ES6 Modules rather than globals. It feels like taking a step backward if we add a library and then use it through its global, right?
The sad news is that not all libraries are written in such a way that they can be consumed easily via a modules loader. Even so, if there is an AMD definition included in the library, not all of them are compatible with the module loader used by ember-cli.
For example, moment includes an AMD version:
Unfortunately, the module loader ember-cli is using doesn’t support that yet.
Other libraries do the following:
This is known as an anonymous module. Although its syntax is valid, the loader doesn’t support this either because it expects named modules.
This issue is not entirely the fault of ember-cli, but in fact results from everyone building their libraries in different formats, making it difficult for consumers to use.
What can we do about it?
Wrapping globals
Instead of consuming globals directly, let’s wrap them in a helper module that will allow us to foster the use of modules and to easily update or replace moment once we have a way to load it via the module loader.
First, let’s create a utils file called date-helpers:
Replace app/utils/date-helpers.js with the following:
Here we are wrapping the call to moment#format in the function formatDate, which we can consume doing import { formatDate } from ‘utils/date-helpers’;. With this, we are back to our idea of using modules. We’ll also have the facility to easily update moment when our loader is ready to load it.
If we decide to stop using moment and replace it with any other similar library, we won’t need to change our consuming code since it doesn’t care how format-date is implemented.
Writing an Ember helper: format-date.
Helpers are pieces of code that help us augment our templates. In this case, we want to write a helper to create a date as a formatted string.
ember-cli includes a generator for helpers. Let’s create format-date with the command ember g helper format-date, and then modify app/helpers/format-date.js so it consumes our format function.
Once we have our helper defined, we can use it in the component app/templates/components/loans/loan-row.hbs:
Now, when we visit any of our friends’ profiles, we should see the dates in a more attractive format.
Working with libraries with named AMD distributions.
Before the addons system existed, the easiest way to distribute JavaScript libraries to be consumed in ember-cli was to have a build with a named AMD version, importing the library using app.import, and whitelisting the library’s exports.
Let’s study ic-ajax, an “Ember-friendly jQuery.ajax wrapper.” If we navigate to the lib/main.js, we’ll notice that the source of the application is written with ES6 syntax, but it is distributed in different formats. This allows us to consume it in either global or module formats.
As mentioned previously, loader.js doesn’t work with anonymous AMD distributions. If we want to include ic-ajax, we need to use the named AMD output. Let’s try ic-ajax in our project for a first sketch of the dashboard.
Let’s add the library to bower with bower
install ic-ajax --save
. If we are asked to select a version of ember,
then we need to select the one which is higher. Once it’s installed,
let’s import it into our ember-cli-build.js as follows:
ic-ajax’s default export is the request function, which allows us to make petitions and manage them as if they were promises. Let’s use this to create a “dashboard” object.
We’ll present dashboard as the home page of our application, so when
we navigate to the root url we’ll see the reports. We already have the
template, but let’s create the route to load the required data. Create
app/routes/index.js
with the following content:
And then replace app/templates/index.hbs
so it uses
friendsCount:
Next, if we run ember server, we’ll see that everything works. We can see the friends count in our dashboard by visiting http://localhost:4200/.
ember-ajax
We installed ic-ajax
only for demonstration purposes but this library
is not longer the recommended way to wrap ajax request in our ember
applications. Instead, there is now a library called
ember-ajax which is the one
officially supported by the community.
The library ember-ajax
is consumed a bit differently than ic-ajax
since it requires us to use a service.
Now that we understand how importing named AMD libraries works, we can
remove the import for ic-ajax from the ember-cli-build.js and use ember-ajax
. Let’s run the following commands and then stop and
start the server.
If we navigate to the dashboard, we’ll see an error like Uncaught
Error: Could not find module
ic-ajax imported from
borrowers/routes/index. We need to change the route to consume
ember-ajax` instead, it should look like the following:
After changing the route with the code above, our application should work fine again.
A temporary replacement for moment.js
Let’s consume a simple named AMD library that takes a date and returns its value after calling .toDateString(). This will be a simple example just to practice another module for importing named AMD.
The name of the library is borrowers-dates and it is located in https://github.com/abuiles/borrowers-dates.
The following is the content of the library:
The library exports a function called format. Let’s consume it via bower:
And then import it through our ember-cli-build.js:
With the library included, let’s consume it in app/utils/date-helpers.js instead of moment:
Now when we visit the profile for any our friends with articles, we’ll see the dates rendered differently. This is because we are no longer using moment.
ember-browserify
Browserify is a Node library which allows us
to consume other Node libraries in the Browser using CommonJS (which
is Node’s module system), what this means is that we can install
libraries like MomentJS using npm and then consume them in the browser
via browserify. But wait, to use Browserify we actually need to
install the library and create a “bundle” with our
dependencies, normally we’ll run something like browserify main.js -o
bundle.js
and then use bundle.js
via a script tag <script
src="bundle.js"></script>
.
As we can imagine this can get tricky and hard to manage in our ember-cli application, but thanks to Edward Faulkner there is addon which allow us to consume libraries from npm with browserify without needing us to worry about the bundling process, it is called ember-browserify.
Using ember-browserify
First we need to install the addon, which we can do running ember
install
:
Once the addon has been installed, we are going to use it to consume
MomentJS from npm in our date-helpers
file.
Before doing that, let’s remove moment with bower uninstall moment
--save
and also remove
app.import('bower_components/moment/moment.js');
from
ember-cli-build.js
.
Next, let’s install moment via npm, which we can do with npm install
moment --save-dev
.
Once it has been installed we can consume it from npm thanks to
ember-browserify just doing import moment from 'npm:moment';
.
Let’s use it in our date-helpers
so formatDate
uses moment
.
And that’s it, we are now consuming MomentJS via browserify just as if it was other module in our application.
Wrapping up
In this chapter we have covered how to work with JavaScript plugins both as globals, consuming named AMD plugins and via ember-browserify.
We didn’t cover how to write reusable plugins to be consumed with ember-cli. This is what addons are used for, and we’ll talk about them in the next chapter.
Components and Addons
Web Components
Web Components are a new mechanism that allows us to extend the DOM with our own elements rather than limit ourselves to traditional tags. We can define our own tags, wrapping up all the display logic in a single bundle, and reuse it between different applications. We’ll use the component as any other tag and the browser will understand how to render it based on its definition.
Let’s examine how a Share on Twitter button works. Currently, we need to include some JavaScript and then create an anchor tag that will be transformed by the JavaScript snippet:
The previous code will be the same regardless of the application we are developing. Sounds like a good candidate for a component since it’s a chunk of code that can be reused across applications. A possible twitter-button component could look something like the following:
All the implementation details are hidden in its definition. As consumers, we are only interested in the final product and we needn’t worry about how it is accomplished.
Web Components are a great tool that we can use to write more expressive applications and to avoid code repetition within projects. Unfortunately, it is not yet supported in all browsers.
To tackle this problem, ember introduced the concept of Components. This is an API that allows us to write components today by following the W3C specification as closely as possible. The day components become widely available, we’ll be able to switch without hiccups.
ember-cli addons
ember-cli has a built-in mechanism that allows us to augment ember-cli’s functionality and share code easily between different applications. This mechanism is known as addons.
Using addons, we can easily write ember components and share them with others using npm. Let’s create our first component, which will help us grab an image to use as placeholder in our friends’ profiles.
ember-cli-fill-murray
http://www.fillmurray.com is a service we can use to get random images of Bill Murray to use as placeholders. Let’s write an addon so that we can do something like the following in any of our templates:
First we need to create the addon. ember-cli has a command for this. Outside of our borrowers directory, let’s run the following:
The addon command creates a directory very similar to the one created by new, but the former is done in a way that allows it to be distributed as an addon.
If we go to the directory ember-cli-fill-murray, it will look like the following:
If we open package.json, we’ll see the following section:
That’s how ember-cli detects the presence of an addon. When we include the library in an ember-cli project, it will transverse the dependencies and identify as addons the items with the keyword ember-addon. We’ll also use package.json to specify any dependency our library might have.
Next we have index.js, which is the entry point for loading our addon. If we need to add any extra configuration for our addon, we’ll specify it in this file. For now let’s work with the basic one, which looks like this:
Next we have the directories app and addon. This is where the code for our addon will live.
Whatever we put into app will be merged into our application’s namespace, meaning we’ll consume it just as if it were inside our ember-cli project.
For example, if we had an app/model/friend-base.js file in our addon, we could consume it in any of the models in our borrowers app thusly:
If we had put friend-base.js into the addon directory, instead of getting merged into the consuming application namespace, it would be kept under the addon namespace with the previous example. If our addon was called borrowers-base and we had addon/models/friend-base.js, then we would have consumed it like this:
Going back to our ember-cli-fill-murray addon, we have the directory in place and we want to distribute a component called fill-murray. Inside the directory, we can also use ember-cli generators. We’ll do that in order to create the component:
In addon/components/fill-murray.js, we can specify the properties for our component:
Next we need to specify the body of our component in addon/templates/components/fill-murray.hbs:
When rendering the component, it inserts an img tag that reads the source from the computed property we specified.
Our addon is now ready. The next step is to distribute it via npm. First let’s change the name in package.json because ember-cli-fill-murray is already taken. We’ll use ember-cli-fill-murray-your-github-nickname and set version to 0.1.0. It will look something like this:
Since our addon ships with templates, we need to include
ember-cli-htmlbars
in the dependencies, which we can do running npm
i ember-cli-htmlbars --save
.
With the previous in place, let’s do npm publish. Our addon is now ready to be consumed.
Consuming fill-murray in borrowers
Once our package is in npm, we can add it to our application by running the following command:
Once it is installed, we can consume the component in any of our templates as follows:
Let’s modify our app/templates/friends/show.hbs to look like the following:
After editing the template, the Bill Murray placeholder will appear when we visit a friend’s profile.
With that we have created and published our first addon.
Ember Data
In this chapter we’ll cover some of the public methods from the DS.Store and learn how to load relationships asynchronously.
DS.Store Public API
The store is the main interface we’ll use to interact with our records as well as the backend. When we create, load, or delete a record, it is managed and saved in the store. The store then takes care of replicating any change to the backend.
We won’t cover all of the functions, but we’ll go over the more common ones and their gotchas.
peekAll
store.peekAll is similar to store.findAll, but instead of making a request to the backend it returns all the records already loaded in the store. The result of this method is a live array, which means it will update its content if more records are loaded into the store for the given type.
Let’s study this with the inspector by navigating to http://localhost:4200 and clicking refresh. Next we’ll grab an instance of the application route and run the following commands in the console:
We stored the result in a variable called friends, which is a collection with zero elements. This makes sense because we haven’t loaded any friends yet. If we click on the friends link and run the following:
We’ll see that the result is no longer zero. When we navigated to the friends route, a request to the backend was made and some records were loaded into the store. As we mentioned, the result from peekAll is a live array. That’s why our friends variable was updated without requiring any additional steps.
findAll
If we call findAll with a model name, then it will make a request to load a list of records of that type. The following is an example:
If we want to send query parameters with the request, then we should
use store.query
, it receives the name of the model and an object
as second argument, every key on the object will be included as
parameter:
In the previous request we asked query to load all the articles, sending as parameters the key sort.
Like peekAll and filter, the result from findAll is a live array. When called, it makes a request to the server and the collection is updated when more records are added to or removed from the store.
findRecord: Loading a single record
If we want to load a single record then we should use store.findRecord . To do that, we use the name of the model and the record’s id as second argument:
peekRecord
We can use store.peekRecord(‘friend’, 15) to fetch a user directly
from the store. Unlike findRecord, query or findAll, the behavior of
this function is synchronous. It will return the record if it is
available, or null
otherwise.
createRecord
We are already familiar with createRecord, which is used when we want to create a new record for a given type. For example:
We can also use createRecord via a relationship. Suppose we are in the context of a friend and we know they have an articles property that represents all the articles belonging to another friend. If we want to add a new article, we can do it using the following syntax:
This won’t work if the relationship is async.
Loading relationships
We already covered how to create relationships between models. If we are defining a relationship of type “has many”, then we use the keyword hasMany. If we want a “belongs to”, we use DS.belongsTo.
There are two ways to work with relationships. The first is working with records pre-loaded into the store, and the second is to load them on demand.
As we know, our API follows JSON API, which gives us different strategies to load the records associated with a relationship. In our API, we use the links strategy. Ember data follows those links to fill up the association automatically.
If we inspect the payload for friends, the results look something like the following:
This includes the model’s attributes and a key called relationships
which includes the links where the related attributes can be fetched.
Ember data then follows that link to get the list of loans for a friend.
We can see how this works if we visit http://localhost:4200/friends
and then click on a friend, we’ll see that there are several requests
after that one to fetch the related loans and then more requests to
fetch the related articles.
We can avoid all those requests sideloading the related records,
JSONAPI give us a way to do so, which is using the parameter include
and then a path for the things we want to include.
Let’s modify the index route so it includes the query parameter include
with the value loans
:
Now if we go to to index, we’ll see that the request for the “loans” relationship is not happening, but there are request for articles.
We can sideload the articles too, but we need to ask for that in the
include
query params. Let’s modify our route once more so it looks
like the following:
If we go once more to friends and then click on one of them, we won’t see extra requests happening.
We can see the difference in the payload visiting the following page
http://api.ember-101.com/friends?include=loans
and then adding or removing things to the query param include
.
What to use?
So many options. What should we use? It depends on our scenarios and how we want to load our data. Side-loading works perfectly when we are not fetching many records, but it can make your API really slow if you are returning a lot of relationships and a lot of records.
Async helps us alleviate the issue when we have a lot of records. This can help us keep our end-points lighter, but it might add some overhead when getting all the ids in a relationship.
The faster option from an API point of view would be to use links. This won’t require the parent to know anything about its children, but then we lose other benefits.
For example, when using ids, ember data will only load records from the server that are not yet available in the store. However, if some of the records are loaded, it won’t make that request. With links, you lose that benefit because ember data doesn’t have any information. It will make the request and load data that you might already have available.
Again, it’s a matter of weighing risks and benefits and finding what works best for us. We need to measure and experiment with different strategies before choosing the one that gives us the best performance.
Computed Properties and Observers
We already covered computed properties, which we use in different parts of our applications. One of these uses occurs on the friend model:
With the code above, we created a new property on the model called fullName that depends on firstName and lastName. The computed properties are called once at the beginning and the result is cached until any of the dependent properties change.
Next we’ll talk about a couple of features and things to keep in mind when defining computed properties.
Computed Property function signature
The functions we’ve used to declare a computed property have looked like the following:
We can also add support for setting the value of a computed property and handling how it should behave. The following example shows how:
Why didn’t we mention that we can use a computed property as setter? This is a very uncommon scenario that tends to cause a lot of confusion for people. Ideally, we use computed properties as Read-Only.
Computed Properties gotchas
Computed properties and observers are normally fired whenever we call
this.set()
on the property they depend on. The downside of this is that they
will be recalculated even if the value is the same.
Fortunately for us, Gavin Joyce wrote an ember-cli-addon called ember-computed-change-gate that offers an alternative function to define computed properties and that fixes observers such that they are only called if the property they depend on has changed.
We can install the addon with npm i ember-computed-change-gate
--save-dev
and use it in our friends model like so:
Now our computed property capitalizedFirstName
will be called
only when the value of the dependent key has changed to a
different value.
Observers
Ember has a built-in implementation of the Observer pattern, which allows us to keep track of changes in any property or computed property.
Let’s use an observer on the low-row component to do something every time the state changes, let’s add the following:
We define an observer calling Ember.observer
which receives any
number of properties to observe and the function to call when any of
the properties change.
We can also create an observer using addObserver
from
Ember.Observable.
We could define the stateChanged
observer like this:
Here we are using init
which is the first function called when the
component is started, and then we call _super()
to call the
function on the object’s parent. Calling super is not always required
but if you are using mixins or inheriting from a class that does
something on init
then we need to call it.
The ember guides have a great section on the component life cycle, we recommend to give that section a read to expand the knowledge in this area https://guides.emberjs.com/v2.6.0/components/the-component-lifecycle/
Observing collections
Ember adds two convenient properties to collections. We can use them if we want to observe changes to any of the members’ properties, or if we want to do something every time an element is added or removed.
The first property is .[], which is just a special handler that changes every time the collection content changes.
The second one is @each, which allows us to observe properties on each of the items in the collection.
We can use the previous function in our loans index to call a function when we add a new loan, and then the other one when we change the state of a loan:
If we visit any of our friends’ profiles and change the state for any loan or add a new one, we’ll see the relevant messages in the browser’s console.
Driving our application state through the URL
In JSConf EU 2013, Tom Dale gave a talk called Stop Breaking the Web.
Tom talks about the importance of the URL and how we should give it a higher priority in our applications. Ideally, the URL should be able to reflect our application state in such a way that we can easily reference it, bookmark it, or share it with others.
Some of us have probably experienced some frustration when visiting a website that has search functionality but loses our selections between page reloads, or that doesn’t allow us to easily share what we see with others.
Airline websites offer an example of this issue. The following image shows Delta’s website after searching for flights to the next EmberConf.
The URL after the search is http://www.delta.com/air-shopping/findFlights.action, which doesn’t really tell us anything about the screen we are visiting. If we copy and paste the URL in another browser, we’ll get a bunch of errors and not the search we originally performed.
Now let’s do a search on hipmunk. This website places greater value on the functionality of the URL.
The search above results in the following: flights/MDE-to-PDX#!dates=Aug23,Aug31&pax=1. Isn’t that beautiful? Just by reading the URL, we know our destination and the dates of our trip. Clicking the URL takes us to the original search we see in the image. Suppose we want someone to buy the ticket for us; we can simply share the URL and be done with it.
Ember also appreciates the beauty of a functional URL. In fact, our applications are driven by URLs that we specify in app/router.js. This doesn’t mean we are immune from building bad applications that don’t respect the URL, but at least it gives us the tools to avoid these issues and invites us to think better about our URLs.
Sorting friends.
When visiting the friends index, we want to be able to sort them by clicking on the Name or Articles column and then toggle ascending or descending between clicks.
We’ll add 2 properties to our friends index controller: sortBy and sortAscending.
To change our sort field dynamically, we will create a function
setSortBy
that will receive as parameter the field we want to sort
our properties by.
We’ll also toggle the property sortAscending
every time we call the
action setSortBy
. For example, if it’s true
then it becomes
false
and vice versa.
Now we need to call the setSortBy
action in the
app/templates/friends/index.hbs
Let us add add some CSS so we have a cursor on the name column:
Now if we go to http://localhost:4200/friends and click on Name, we’ll see that our action is being fired and something like the following logged to the browser’s console:
But our list is not changing and we don’t see the URL changing either,
we need to refresh our model every time those values change and also
the URL. To achieve this we’ll use a useful feature called Query
Parameters that
allows us to persist application state in the URL as parameters,
generating URLs like /friends?sort=first-name
.
Query Parameters
To use query parameters we need to specify a property called
queryParams
in the controller associated with this route, and then
list every property that should persist as query parameter.
In our scenario we’ll modify the controller as follows:
If we visit http://localhost:4200/friends the URL won’t have any
query parameters, but as soon as we click any of the headers the query
parameters will change. Query parameters are only included when
the default value for the property changes. In our case, that would be
when sortAscending
changes to something different from true
and
sortBy
to something different from first-name
.
Now we can refresh the browser or copy the URL into a new tab and we’ll see the same query parameters, but the data is still not changing, we’ll see how to fix that shortly.
We can also use query params with the link-to
helper. If we want
a link to the friends index sorted by first-name
, we can write it
like this: {{#link-to 'friends' (query-params
sortBy="first-name")}}Friends{{/link-to}}
Refreshing the model when query parameters changes
By default the model hook won’t be called if any of the query parameters change, but there are scenarios where this can be the desired behavior. For example, when we are using pagination, or if we want to do server side sorting, under that scenario we’ll ask the API for the users sorted by a given field in ascending or descending order.
Let’s use the query parameters to change our friends order, since our
JSONAPI supports sort
, we can have the route make a
full transition when any of the queryParams
change. To do this,
we’ll need to specify a property in the route called queryParams
where we explicitly mark the parameters that we want to cause a full
transition.
Now every time we change sortBy
or sortAscending
, the model hook
for app/routes/friends/index.js will be called, making a request
to the API similar to the following and our friends list will be
updated accordingly:
Further Reading
Query parameters is one of the best documented features on Ember. We recommend the official guide for more information: http://emberjs.com/guides/routing/query-params/.
Testing Ember.js applications
In this chapter we’ll cover the basics of unit and acceptance testing in Ember.js applications and recommend a couple of resources that can help us expand our knowledge in this area.
Unit Testing
When we run the generators, they create unit test files by default.
We can view all the generated unit tests if we go to tests/unit
:
Tests are automatically grouped by type. If we open the unit test for our friend model, we’ll see the following:
At the beginning of the test we import a set of helpers from ember-qunit, which is a library that wraps a bunch of functions to facilitate testing with QUnit.
moduleForModel
received the name of the model we are testing, a
description, and some options. In our scenario, we specify
that the tests need a model called article because of the existing
relationship between them.
Next, the test includes a basic assertion that the model exists.
this.subject()
would be an instance of a friend
.
We have two ways of running tests. The first one is via the browser while
we run the development server. We can navigate to
http://localhost:4200/tests and our
tests will be run. The second method is using a tests runner. At the
moment ember-cli has built-in support for Testem with
PhantomJS, which we can use to run our tests on
a CI server. To run tests in this mode, we only need to do ember
test
.
Let’s write two more tests for our friend model. We want to check that
the computed property fullName
behaves as expected and that the
relationship articles is properly set.
We can run our tests by going directly to the following URL: http://localhost:4200/tests?module=Friend.
The first test verifies that fullName
is calculated
correctly. We have to wrap model.set('firstName', 'Geddy');
in
Ember.run
because it has an asynchronous behavior. If we modify the
implementation for fullName
such that it doesn’t return first and last
names, the tests will fail.
The second test checks that we have set up the proper
relationship to articles
. Something similar could go in the articles
model tests. If we call constructor
on an instance to a model, that
will give us access to the class of which it is an instance.
Let’s add other unit test for app/utils/date-helpers
:
We import the function we want to test and then check that it returns the date as a readable string. We can run the test by going to http://localhost:4200/tests?module=Unit%20%7C%20Utility%20%7C%20date%20helpers.
Acceptance Tests
With acceptance tests we can verify workflows in our application. For example, making sure that we can add a new friend, that if we visit the friend index a list is rendered, etc. An acceptance test basically emulates a real user’s experience of our application.
Ember has a set of helpers to simplify writing these kinds of tests. There are synchronous and asynchronous helpers. We use the former for tests that don’t have any kind of side-effect, such as checking if an element is present on a page, and the latter for tests that fire some kind of side-effect. For example, clicking a link or saving a model.
Let’s write an acceptance test to verify that we can add new friends to our application. We can generate an acceptance test with the generator acceptance-test.
If we visit the generated test, we’ll see the following:
Now we can run our tests by visiting http://localhost:4200/tests or, if we want to run only the acceptance tests for Friends New, http://localhost:4200/tests?module=Acceptance%20%7C%20friends%2Fnew.
Let’s add two more tests but this time starting from the index URL. We want to validate that we can navigate to new and then check that it redirects to the correct place after creating a new user.
The second test we want to add checks that the application stays on the new page if we click save, without adding any fields, and that an error message is displayed:
Mocking the API response
On the previous tests we hit the API, but this is not a common scenario. Normally we’d like to mock the interactions with the API. To do so the community recommendation is to use ember-cli-mirage, a library that allows us to mock servers with a simple DSL.
Let’s stop the server and install mirage running ember install
ember-cli-mirage
.
Next, let’s disable mirage
if the environment is development, since
we only want to use it for testing.
If we go to http://localhost:4200/tests we’ll see an error in the acceptance test like:
That’s because we didn’t add the end-point friends to our mirage server.
Let’s tell mirage
about the routes for friends, adding the following
lines to the file mirage/config.js
:
After refreshing the tests page, we’ll see a new error like the following:
Mirage is telling us that there is a handler for friends
but it doesn’t know about that model, we need to create the model in mirage with the command ember g mirage-model friend
.
If we refresh again, the acceptance tests will pass. Mirage also help us if we want to create fake data in the server. To do so, we need to create a factory and then fill the server with fake data before running the test. Let’s explore that next.
We can create factories with the mirage-factory
generator, to create
one for friends we can run the following command ember g
mirage-factory friend
and then let’s edit the factory to declare the
attributes:
Once we have defined the factory, we can use it in our acceptance test. Let’s create one for the friends index route and then assert that the all the friend are rendered.
First, let’s create the acceptance test running ember g acceptance-test friends
With this we can have some idea of what mirage allow us to do, we can work with relationships and use mirage for development, but that’s out of the scope of this book, for more information check out the documentation http://www.ember-cli-mirage.com/docs/v0.2.x/.
Further Reading
During EmberConf 2014, Eric Berry gave a great talk called The Unofficial, Official Ember Testing Guide where he walked us through testing in Ember.js. Eric also contributed an excellent guide for testing that is now the official guide on the Ember.js website. We recommend the official guide, which provides a complete overview from unit to acceptance testing: http://emberjs.com/guides/testing/.
To know more about using mocks and fixtures, we recommend the following presentation: Real World Fixtures by Chris Ball.
PODS
This section has been removed until the Module Unification RFC arrives.
For more information about future project structure, we recommend the following talk: Ember.js NYC, May 2016: Robert Jackson on app structure
Deploying Ember.js applications
In this chapter we’ll explore different alternatives to deploy our Ember.js applications. We’ll talk about S3 and Pagefront based deployments where our application is completely separated from our API.
Deploying to S3
In order to host our application in S3, we’ll need to change our application adapter so it hits our CORS enabled API and then generate a production build.
To consume the API without using ember-cli’s proxy feature, we need to set the property host in the application adapter.
To do so, let’s add a configuration property called host
in
config/environment.js
and then read it from there.
Now we can use it in the adapter. Let’s create the application adapter
running ember g adapter application
and then add the following:
We also need to change app/routes/index.js
to use the host:
Now we can stop the server and run it again without the option
--proxy
.
Next we need to generate the production build using the command ember
build
.
When we run ember server
, we always run a build
and add
some extra stuff so that we can run our project in development, but we
don’t need the same files in production.
When we do ember build
, the output goes by default to the directory
dist
. Let’s check that:
Inspecting the dist
directory, we’ll see the following contents:
Let’s talk about the assets directory first. All our JavaScript and
stylesheet files will end in this directory. We can also put
other kinds of assets, such as images or fonts, under public/assets
and
they will be merged into this directory. If we had the image
public/assets/images/foo.png
we could reference it in our stylesheets like
images/foo.png
.
What about those test files? They are used for testing and only included in development or test environments. If we go to http://localhost:4200/tests and inspect the network tab, we’ll see that those files are being used.
The tests directory is the entry point for running tests. testem.js
is used by default when we do ember test
. It uses Testem to run
the test with PhantomJS.
If we run the build command but we specify production environment (e.g.,
ember build --environment production
) we’ll see a very different
output:
We have fewer files this time. Nothing related with testing is
included because that is only a development/tests concern. Our assets
files were fingerprinted and minified. If we open dist/index.html
we’ll see that the references to them were updated as well:
Fingerprinting is achieved using broccoli-asset-rev. This allows us the option to select the format of the files we want to fingerprint and to append an URL to every asset.
Now we are ready to deploy to an S3 bucket. We need to create
the bucket and enable static website hosting. Let’s set up an
index document, index.html
.
The following guide explains how to set up your S3 bucket: Hosting a Static Website on Amazon S3
Once the bucket is set up, we can run ember build --environment
production
and then manually upload all the files under dist
. The
following is an example of the site working on S3:
http://ember-101.s3-website-us-east-1.amazonaws.com/
It is very important that we set our bucket as public. To do this, we can use the following bucket policy:
The following tutorial explains how to achieve a setup using custom routing and Cloudfront: Hosting a Static Website on Amazon Web Services
If we decide to use Cloudfront, we need to prepend the URL to our assets. To do this, we simply pass the option in the Brocfile as follows:
If we run ember build --environment production
and open
dist/index.html
, we’ll notice the URL in our assets.
A better approach for deployments would be to use the ember addon ember-cli-deploy. Their website has a complete guide on how to use their addon.
Deploying to Pagefront
Pagefront is a PaaS for ember. This is probably the easiest way to deploy ember applications today. They have an ember addon which makes the whole process super simple.
First let’s install the addon with ember install
, we’ll use
ember-101-GITHUB-HANDLE
to identiy the application:
After installing ember-pagefront, a new file config/deploy.js
will be created with the required data to deploy to pagefront.
Next we can run ember deploy production
and that’s it. Our
application will be deployed to
https://ember-101-abuiles.pagefrontapp.com
ember-cli-deploy
Ember CLI deploy has become the default solution to manage deployments with ember. Pagefront use it under the hood, and there are many other plugins available, which allow us to deploy to different platforms or just develop our own solutions.
We can find all the available options visiting the plugins section in their website http://ember-cli-deploy.com/docs/v0.6.x/plugins.
Updating your project to the latest version of ember-cli
Ember CLI is a project that is still moving quickly, so from time to time we’ll need to update our applications to use the latest version.
The best way to upgrade is to follow the release notes, which the core team adds with every release.
If we look at the following release https://github.com/ember-cli/ember-cli/releases/tag/v2.7.0-beta.4, we’ll find notes about the commands that we need to run, notable deprecation’s, and links to an example project called ember-new-output which shows the changes after upgrading a project.
Wrapping up.
Ember can have a tough learning curve, but after getting the basics, you’ll see that the time invested in learning will pay off. Not only it will make your more productive, but you’ll be able to take advantage of its addon ecosystem and all the good stuff happening in the JavaScript world. As Yehuda Katz said during Ember Conf 2016: Eventually all the good ideas will end up in Ember.
Learning Ember today is very different to the time when this book was first published. We have an official “Learning team”, they are doing a terrific job keeping the guides up to date and making sure that every new feature is documented for mainstream adoption.
If you are asking yourself were to go next, my recommendation would be start building applications, since that will be the only way to get real world experience. I’ll try to update the book as new features arrive and let you know what has changed in the release notes. You can also follow me on twitter abuiles for updates.
Lastly, if you enjoy this book and want to support my work, you can do so buying my other book https://leanpub.com/json-api-by-example or if you download this book for free and think it was worth something, you can send me donations clicking here.
Happy learning!
- Adolfo Builes
Notes
1http://speakingjs.com/↩
2https://github.com/broccolijs/broccoli↩
4https://github.com/airportyh/testem↩
5https://people.mozilla.org/~jorendorff/es6-draft.html↩
7The transpiling process is done with es6-module-transpiler.↩
8To know more about AMD checkout their wiki↩
10We recommend going through the documentation to get more insights on this adapter DS.JSONAPIAdapter.↩