Till now most of our examples in our previous chapters have been using the index
or /
route. Real-world web applications seldom have only one route. Ambitious web applications, on the other hand, have a separate URL endpoint mapped to each different state of the application.
The Ember.js framework provides the concept of a router, a route, and a resource to manage the mapping between the URL and the state of the application.
Router in Ember.js is the core and the central part of the framework. It maintains the mapping of the URLs to individual routes. It monitors the URL of the web application and then, based on the mapping, it invokes the individual routes.
Let's see how to add this mapping. We will be adding two new routes, products
and about
, to our application.
The Router
definition present at example1/app/router.js
is as follows:
import Ember from 'ember'; import config from './config/environment'; var Router = Ember.Router.extend({ location: config.locationType // "auto" }); Router.map(function() { this.route("products",{ path: "/products" }); this.route("about",{ path: "/about" }); }); export default Router;
As you can see, we first create the Router
object by extending Ember.Router
. The location
property of the router governs how to build the URLs of the application. We will be talking about the location API later in this chapter.
We use the map
method of Ember.Router
to create individual routes. We use the this.route
method of the router to map URLs with the routes. The route
method takes in three arguments: name
, options
, and callback
.
The name
corresponds to the name of the route and helps in identifying this route from other parts of the application.
The options
argument expects a JavaScript object with your desired options set. In our case, we pass in the path
attribute in the option's argument object to map our route to a specific URL.
The last argument is the callback
, which is used for nesting the routes.
As you can see in the preceding code, we defined two routes with names products
and about
.
Let's now look at the templates for these corresponding routes. As discussed in the previous chapter, all the templates go inside the app/templates/
folder.
<h2>Welcome to Ember.js</h2> <div> {{#link-to 'about'}}About{{/link-to}} </div> <div> {{#link-to 'products'}}Products{{/link-to}}</div> {{outlet}}
The application template is present at example1/app/templates/application.hbs
<br> <br> <div>Products Template</div>
The products template is present at example1/app/templates/products.hbs
<br> <br> <div>About Template</div>
The products template is present at example1/app/templates/about.hbs
You can see from what precedes that apart from the application template, we have defined two additional templates, products
and about
. These templates contain the data to be displayed for the products
and about
pages.
In application.hbs
, we are also using a very useful handlebars helper expression, {{#link-to}}
. The link-to
helper expression helps us avoid hardcoding the URL address in our application. It fetches the URL pattern from the mappings in the router.
So {{#link-to 'products'}} Products {{/link-to}}
would generate something like the following:
<a id="ember258" class="ember-view" href="/about">About</a>.
You can see that the generated HTML link's href
points to the correct URL path. The link-to
helper makes our application transparent to any changes in the URL pattern of the application.
Let us say that later in development cycle of your application, the SEO (search engine optimization) expert comes in and suggests that by changing the /about
endpoint to /about-us
, you will improve the search engine ranking for your site.
If you had used the {{link-to}}
helper, you would just edit the router code to map to about-us
instead of about
, and your application would work just fine. Had you hardcoded the URL in your application, incorporating this change would have been error-prone and time consuming.
If you run the above code, you will see a screen that now has About
and Products
links on the homepage.
If you click on any of the links, you will see that the respective products
or about
template appears on the screen, as shown in the following figure:
In Ember.js, the routes of your application should inherit the framework's Ember.Route
class to provide any custom implementation of the route. One thing that you might have noticed in the preceding example would be that we did not create app/routes/products.js
or app/routes/about.js
in our application. Whenever a user visits the /products
or /about
URL, the Ember.js framework tries to find the corresponding routes based on naming conventions. For example, for /products
, the router will try and instantiate app/routes/products.js
for you, and if the framework is not able to find its definition, then it will generate the route for you. So, in our case, since we don't define the app/routes/products.js
or app/routes/about.js
, the Ember.js framework generates them for us. Ember.js frameworks rely heavily on naming conventions, and routes are at the core of these conventions. Based on the name of the route supplied to the this.route()
function, the framework tries to find the respective controller and template to use.
Let's see this by an example; in the above example, we created the product route which uses the following:
this.route("products",{ path: "/products" });
When the end user visits the /products
path, the framework will try to find the route with the matching name and path, and will look for definition exported in app/routes/products.js
, and when it finds one, it will instantiate the route for you and execute the hooks associated with the route (we will be covering the initialization of routes in more detail later in this chapter).
Then, the framework will try to find the controller that matches the route, which in our case should be defined in the app/controllers/products.js
file. The framework will finally resolve the handlebars.js template it has to render, which should be defined in the app/templates/products.hbs
file.
As you can see, this clearly forms a pattern. An example
route declaration will map to the /example
URL by default, and the framework will look for its matching route definition exported from the app/routes/example.js
file, matching controller in app/controllers/example.js
, and render the template defined in app/templates/example.hbs
.
Now, since all of the application will follow this convention, it becomes really easy to find where the code for a specific functionality resides. It also makes debugging errors in your application very straightforward.
Till now, we have seen very basic usage of the Ember.js routes; we have been using only top-level routes such as /products
and /about
, but very seldom do real-world applications have such simple routes.
Real-world applications will have resources or nouns, and actions that can be executed on these nouns that are depicted by verbs.
For example, there could be a /products/2
endpoint to show the product details page for a product with ID as 2, or /products/new
and /products/2/edit
routes to create and edit a product, respectively.
The Ember.js framework encourages using resource
for all the nouns and route
for all the verbs. This means that if you are creating your application for a specific domain area, all the entities of that domain should map to a resource and all the actions on the domain entities should translate to routes. A resource then becomes a collection of routes.
You can create a new resource using this.resource
in the router. It expects two arguments, the first one is the name of the resource and other is a function that defines the nested routes, if any. If you don't have any nested routes in the resource, you can omit this argument. The following code snippet shows how to create a resource with nested routes:
Router.map(function() { this.resource("products",function(){ this.route("new"); }); this.route("about"); });
The nested route is defined in example2/app/router.js
Here, we have created a resource products
and a nested route, new
. To be able to display these routes, we will have to create the corresponding nested templates.
One very important thing to note here is that the nesting of resources/routes
also means that their templates should also be nested in a similar fashion. Let's make it more clearer by defining our products
, products/new
and about
templates:
<br> <br> <div>Products Template</div> {{outlet}}
The products template is present at example2/app/templates/products.hbs
<br> <br> <div>About Template</div> <div> This Template should contains some information about us
The about template is present at example2/app/templates/about.hbs
<br> <h3>Create a New Product</h3> <br> Product name:<br> {{input value=name }} <br> Product Description:<br> {{textarea value=description }} <br><br> <button {{action 'create'}}>Create</button>
The products.new template is present at example2/app/templates/products/new.hbs
You can see above that we have defined two templates, one for products
and the other for the new
route that is nested under the products
resource.
When you first look at the
products
template, you will notice that we have now added {{outlet}}
at the end of the template. This outlet will enable nesting for this template, and all the nested routes that are present under the products
resource will first render the products
template present at app/templates/products.hbs
, and then render the nested route's template in the outlet provided by the products
template. If you remove the {{outlet}}
from the products
template, you will notice that the products.new
template is never rendered, as it could not get the parent outlet to render the child route template.
You might also have noticed by now that we are referring to the new
route that is nested inside the products
resource by products.new
. Ember.js follows this convention to refer to the nested routes in handlebars and other helpers, which is <<parent resource>>.<<nested route>>
.
To link this nested route using the {{link-to}}
handlebars helper, we would do something like the following:
{{#link-to 'products.new'}}Create a new product{{/link-to}}</div>
The following table shows the mapping of the different routes that you defined in your router to respective controller, route, and template files:
URL |
Route name |
Controller |
Route |
Template |
---|---|---|---|---|
|
|
|
|
|
N/A |
|
|
|
|
|
|
↳ |
↳ |
↳ |
|
|
↳ |
↳ |
↳ |
|
|
|
|
|
Let us see how the routes, which we have defined in our router above, map and initialize the controller, route, and template. Let us start from products.new
route. Now, since the new
route is defined inside the products
resource, we would refer it by products.new
. As we discussed earlier, route nesting also means controller and template nesting. This means that when the user visits the /products/new
route, first of all, the products
template is rendered from app/templates/products.hbs
using the route exported from app/routes/products.js
, and this route injects the controller from app/controllers/products.js
to back the template.
After rendering the products template, the framework will look for the products.new
template in app/template/products/new.hbs
to render it in the {{outlet}}
provided by the products
template.
The controller exported at app/controllers/products/new.js
and the route exported at app/routes/products/new.js
will back the app/templates/products/new.hbs
template.
The general rule of thumb is that first the parent resource is rendered, using its route and controller. Then, the nested child route is rendered in the parent template's outlet.
The nested or child route has its own controller and route, just like an independent route. The location of the nested route should be inside a folder whose name is the name of the resource it is nested in, for example, app/routes/<<resource>>/<<nested route>>
.
It may seem a bit odd at first, but the trick here is to think of your application layout as nested templates rather than independent ones.
One thing that you might have noticed in the above table would be the products
route, which is not mapped to any URL, and the products.index
route, which we did not define anywhere. Lets revisit the products
and products.index
routes again here:
URL |
Route name |
Controller |
Route |
Template |
---|---|---|---|---|
|
|
|
|
|
|
|
↳ |
↳ |
↳ |
The products
route is not mapped to any URL and is always invoked when a user visits /products
or any of its child routes. It is very much like the application route present in app/routes/application.js
, but only for all the routes that are nested inside the products
resource.
If we want to handle errors or add in a common functionality for all the products, then its app/routes/products.js
or app/controllers/products.js
routes would be the right place to put in the common behavior.
For example, if we want to handle the errors that originate from the products pages in a specific way, we would put this specific behavior in the products
route.
Similarly, whenever you create a nested route, you get resource.index
that maps to /resource
automatically. You just need to override the default implementation of ResourceIndexRoute
and ResourceIndexController
by defining them in your application with its custom behavior. This behavior is in line with the event bubbling topic we discussed in Action event bubbling section of Chapter 3, Rendering Using Templates.
This hierarchy of controllers and routes keeps the entities focused toward providing functionality to one section, rather than putting everything in one place and later finding it difficult to maintain that. Another view of the template hierarchy of our application is shown in the following image:
Now, you should be in a better position to understand the overall routes and template nesting in Ember.js.
Till now, almost all of the examples we have seen so far have one thing in common: they all render static data that is returned from the model
method of the corresponding route, something like the following:
export default Ember.Route.extend({ model: function() { return ['red', 'yellow', 'blue']; } });
As we saw in Chapter 2, Understanding Ember.js Object-oriented Patterns, one of the very important advantages of using Ember.js framework is that it tries to build an application that uses components that are highly decoupled, yet internally cohesive.
The templates present the data that is fetched from the models. Routes play a very important role in this process. Routes help you to decide which model to fetch and how to customize it. As shown in the preceding snippet, the model can return a static list of colors or it can also fetch a list of colors from a remote server. All of this remains transparent to the templates that focus on displaying the list of item(s) returned from the model method.
Up until now, we have seen the model method in the route returning static data to the templates. But it will seldom be the case where your models return static data, and most of the time the data is fetched from the server and displayed to the user.
For the next example, we will be using GitHub's public API (found at https://developer.github.com/v3/)to fetch the commits in the Ember.js repository.
If you open the https://api.github.com/repos/emberjs/ember.js/commits link in a browser, you will get the commit data in JSON format for the Ember.js repository on GitHub, something like the following:
[{ "sha": "2da6e0b981ee20d2e2361102fcf7b8cb3ef812c5", "commit": { "author": { "name": "Stefan Penner", "email": "[email protected]", "date": "2014-12-06T17:38:03Z" }, "committer": { "name": "Stefan Penner", "email": "[email protected]", "date": "2014-12-06T17:38:03Z" }, "message": "Merge pull request #9826 from twokul/brocfile-dup-funct Removes duplicate function", "tree": { "sha": "3eaae01753f4a2a919921232013ba32dda658bab", "url": "https://api.github.com/repos/emberjs/ember.js/git/trees/3eaae01753f4a2a919921232013ba32dda658bab" }, "url": "https://api.github.com/repos/emberjs/ember.js/git/commits/2da6e0b981ee20d2e2361102fcf7b8cb3ef812c5", "comment_count": 0 }, ]
Now, let's consume this data and make it presentable for an end user.
We shall create two routes here: the commits.index
route and the application index
route.
As we don't have anything at present to show on the homepage of our application, we shall use the redirect
method to transition from the index
route to the commits.index
route when anyone hits the /
or the root URL. This is how the commits.index
route will look:
export default Ember.Route.extend({ model: function() { varurl = 'https://api.github.com/repos/emberjs/ember.js/commits'; return Ember.$.getJSON(url); } });
Commits index route is present at example3/app/routes/commits/index.js
The following code shows how we can redirect the index route of our application to commits.index
route:
export default Ember.Route.extend({
redirect: function(){
this.transitionTo("commits.index");
}
});
Application index route is present at example3/app/routes/index.js
As you can see in the preceding code snippet, we are using the JQuery $.getJSON()
method to retrieve the data from the server. Now, instead of static text, our model
function retrieves the commit data from the server and returns it to the template.
Now let's look at the two templates we will have for our application. One is for the application that will contain our application name. The application template can contain things that are common for the entire application. The other commits.index
template can contain the code to display the list of commits of the repository. These commits can be retrieved from the model object set in the commits.index
route, as shown in the following:
<h1>Ember.js Repo</h2> {{outlet}}
Application template is present at example3/app/templates/application.hbs
<h2>Commits</h2> {{#each c in model}} <div><em>Sha</em>: {{c.sha}}</div> <div><em>Author</em>: {{c.commit.author.name}}</div> <div><em>Message</em>: {{c.commit.message}}</div> <hr/> {{/each}}
The commits.index template is present at example3/app/templates/commits/index.hbs
As you can see in the example3/app/templates/commits/index.hbs
file, to show the list of commits, we just iterate over the model object and then just output the contents of each commit to the user. If you run the preceding code, you will see the output as shown in the following screenshot:
18.117.146.155