Quantcast
Viewing all articles
Browse latest Browse all 36

Ember.js Tutorial – using Ember CLI

Image may be NSFW.
Clik here to view.
Joe is a user-focused developer with years of experience building complex applications using Ruby and Ember.js. Ask Joe for help.

FREE WORKSHOP: Ember.js Tutorial
Joe Fiorini is giving a free virtual workshop to help you understand the basics of using Ember CLI and answer any questions you may have.
>> Sign up to secure a spot

1 Introduction

1.1 Developing web applications with JavaScript MVC frameworks

Building rich web applications with lots of dynamic behavior has always been rife with complexities. Varying levels of browser feature support, extremely fragmented third-party libraries and a lack of code organization conventions have contributed to extremely high costs for building great user experiences.

Ember.js provides conventions for solving all of these issues. The model-view-controller pattern removes questions of what code goes where; Ember itself provides support for all the major browsers, taking the burden off the developer. Ember CLI makes working with third-party dependencies a breeze. Ember.js exists to lower the cost of building great experiences.

1.2 Ember.js vs. Backbone.js

Applying MVC to JavaScript is nothing new, of course — Backbone.js brought this concept to early adopters in 2010. Backbone is a very minimal framework, providing some structure for code, but not much more. For example, Backbone does not handle templating, data binding, asynchronous code patterns or a host of other features that Ember does. Of course, use cases vary and you have to pick the solution that works best for your situation. In my experience, and as I will show in this article, Ember provides everything necessary to build extremely rich user experiences with a fraction of the time and effort required by more minimal frameworks like Backbone.

1.3 The Ember.js learning curve

Many people getting started with Ember find that they have somewhat of a hurdle to jump before they start reaping the productivity benefits that come with the framework, since Ember provides abstractions on top of the most common existing JavaScript patterns.

Learning these abstractions adds some extra mental overhead, but gives you the freedom to focus on building the right solution for your problem with minimal debate on file organization or performance concerns. Ember gives you lots of small pieces; it’s up to you to figure out how they apply to your problem (if you’re on a Mac I highly recommend buying Dash and keeping it open to the Ember docs).

1.3.1 Convention over configuration

The one abstraction that seems to trip up most developers I’ve worked with is the way Ember manages your object lifecycle for you. Rather than having you write a bunch of boilerplate to wire together routes, controllers, and templates, Ember uses dependency injection to automatically load and instantiate classes for you. In order to find the classes, they have to correspond to strict naming conventions. Prior to the introduction of ember-cli, this was done by having objects that are named for their parent class. For example, to create controller that describes a person, you’d do:

MyApp.PersonController = Ember.Controller.extend({
});

These conventions mean that you don’t have to write any boilerplate. You also don’t have to spend time up front thinking about how to structure code in your app; you can start reaping the benefits of a usable application from your first line of code. But, as with any convention-driven framework, understanding the conventions takes a little bit of work. For more details on the specific conventions in Ember see the Core Concepts guide.

2 Installing Ember.js

2.1 Single-page application

If you download the Ember Starter Kit, you can use Ember in a manner similar to other JavaScript libraries. With this approach, you’ll use global constants to refer to all of your classes and script tags to hold templates. This might be okay when you’re first getting started, but globals quickly make things more confusing when you introduce a codebase to a team. It also means you’re on your own if you want to use CSS preprocessing like Less or Sass, minify code, compress images, et cetera. This article does not use this approach, so if you want to read more about it I recommend checking out the Ember Guides.

2.2 ember-cli

The way I recommend using Ember is via the ember-cli utility. It requires node.js, which you can get by downloading and running the installer for your platform. There are some additional tips for Windows users.

Once you have node installed, you can get ember-cli by opening up a command prompt and running:

$ npm install -g ember-cli

To create a structure for the application we’ll be building throughout this tutorial you can run:

$ ember new releases

This will create a folder called releases with everything needed to start coding. Change directory into releases and run:

$ ember server

Now you should be able to visit http://localhost:4200 and see a very basic page — you’re ready to follow along with this tutorial.

3 Customizing URLs with the router

The Ember router handles changes to the URL as a user moves around your website. It allows you to render a template, update the page title, transition to another route, or really anything you might want to do in response to navigation.

3.1 Routes vs. Resources

Each URL endpoint in your application is referred as a route. We call groups of routes related to a domain concept &resources. Every time someone visits a route, Ember loads a route handler — this is how you tell Ember what to do in response to a URL change. More often than not you will have multiple route handlers loaded in your application at one time.

Setting up the router looks something like:

// This example uses actual routes from vine.co
Router.map(function() {
    this.route('popular-now');
    this.resource('channels', function() {
        this.route('show', { path: '/:channel' });
    });
});

3.2 Creating routes with custom URLs

By default, Ember’s router will listen for a URL matching the name of the route. In the above example, when you visit http://vine.co/popular-now, Ember looks for and loads a rout handler corresponding to the popular-now route.

Let’s look at another example:

Router.map(function() {
    this.route('favorites', { path: '/favs' });
});

Here we pass an options object with a path option to the router’s route method. This allows us to customize the URL that Ember watches for. In this example, visiting /favorites will not do anything; instead, Ember is going to watch for /favs.

3.2.1 Custom URLs and nesting

An important note about specifying custom paths:

Router.map(function() {
    this.resource('posts', function() {
        this.route('favorites', { path: '/favs' });
    });
});

When nesting like this, the router will route any child routes under their parents. Here, this means that instead of listening for /favs, the router will listen for /posts/favs.

3.2.2 Using dynamic segments

Now that we know about custom paths, let’s look at the rest of the Vine example:

this.resource('channels', function() {
    this.route('show', { path: '/:channel' });
});

Note the custom path specified for channels/show. The : character in the path signifies a dynamic segment. Instead of looking for the word channel, Ember will match any set of characters following /channels up until the end of the URL or the next /, whichever comes first. It will then take the string from that segment of the URL and hand it to the route handler as a parameter.

This means if you visit http://vine.co/channels/animals Ember will load up a route handler and give it the value animals as a parameter named channel.

3.4 Push state vs. hash routing

Long ago (in internet time) it was not possible to actually change the browser’s URL via JavaScript. Therefore, most sites avoided using JavaScript for routing because they would lose the browser’s built-in history tracking (back & forward buttons). Browsers did allow changing a URL’s hash — the # character at the end of a URL — though. Hash changes are tracked as history, so the back button would work. Some of the early single-page apps started using this as a hack to permalink states loaded entirely via JavaScript, and it became a common pattern.

Today, all modern browsers support changing the URL via the History API. You can push a new state onto the history stack (simultaneously updating the URL) and then pop off of it to return to a previous state. You can read more about the HTML5 History API if you’re interested.

Ember has support for both methods of navigation, and the one you use depends on your application’s browser support. By default, Ember will use hash URLs; if you want to enable the History API support instead, you can set the Router’s location property to history. If you want to support the History API but fall back to hash URLs for older browsers, you can use the value auto instead.

// in app/router.js
Router.reopen({
  location: "history" // or "auto"
});

If you really want to, you can disable URL watching altogether by using the value none.

3.4.1 Important note on the History API

If you use the History API, make sure you serve the Ember app at all route paths defined in your router, even dynamic segments. The implementation of this depends on how you’re serving the Ember app (and thus is outside the scope of this article).

4 Using a route handler to retrieve items from an API

In Ember, you define a route handler (for both routes & resources) by creating an object that extends from Ember.Route. From there you have a number of methods you can override to handle browser behavior.

4.1 Router Naming Conventions

To load routes, Ember will automatically look for objects declared in app/routes with names that match defined endpoints in the router. Let’s look at our Vine example again:

// This example uses actual routes from vine.co
Router.map(function() {
    this.route('popular-now');
    this.resource('channels', function() {
        this.route('show', { path: '/:channel' });
    });
});

Let’s say we want to customize some behavior for the popular-now route — the first thing we need to do is define the route handler. Create a file called app/routes/popular-now.js that looks like this:

export default Ember.Route.extend({
});

That’s it! You’ve created a route handler. Now let’s start customizing this by overriding the default behavior.

4.2 Ember.Route Model callback

An Ember route typically represents the state of a model within your application. Therefore, the primary responsibility of a route handler is to load data from some other service so you can display it in a template. Let’s say we want to display a list of releases from a GitHub repository. First off, we could mimic GitHub’s <user>/<repository> structure in Ember by adding the following to app/router.js:

Router.map(function() {
    this.resource('releases', {path: '/:owner/:repo/releases'});
});

Now we can create a route handler in app/routes/releases.js that loads releases via GitHub’s API. Note that ember-cli comes with ic-ajax, an Ember-friendly wrapper around jQuery.ajax. We’ll do this in that route’s model callback:

export default Ember.Route.extend({
    model: function(params) {
        return ic.ajax({
            url: 'http://api.github.com/repos/' + params.owner + '/' + params.repo + '/releases',
            type: 'get'
        });
    }
});

Our model callback is returning a model, but it’s not using ember-data or any other special library for creating models. In fact, all it’s doing is making an AJAX request and using the raw response as the model. I’ve found this to be the simplest way to get started with Ember as libraries like ember-data add cognitive overhead.

4.2.1 Blocking page load with promises

In the above example, what do you think the call to the ic.ajax function returns? Since it makes an AJAX request, it obviously won’t return the response, at least not directly. Instead it hands back a promise (an object that represents an eventual result) that will contain the response from the GitHub API. Ember’s router has built-in support for promises, meaning any time you return a promise from a beforeModel, model, or afterModel callback the router will wait for the request to complete before rendering the template and completing it’s lifecycle.

Historical Note: In the early days of JavaScript MVC frameworks one of the biggest problems was how to handle rendering a template with data that may not have loaded yet. Since JavaScript applications primarily deal with data from APIs, it’s very common for it to take seconds for data to load. But you don’t want your content displaying to the user until all the data is ready, right?

4.2.2 Modifying response before returning

GitHub’s releases API gives us timestamps indicating when the release was created and when it was published. It would be great to display these dates to the user, but the raw response object exposes the timestamps as strings. It would be nice if we could transform them into actual date objects so we could format them with a library like moment.js.

Does that mean we need to pull in a data library like ember-data now? Not necessarily. Let’s play around with the ic.ajax promise in the console for a minute. In an ember-cli app, open up the Developer Tools console to follow along:

require('ic-ajax').default({
  url: 'https://api.github.com/repos/stefanpenner/ember-cli/releases',
  type: 'get'
}).then(function(response) {
    console.log("response:", response);
});

When you run that, you should get log output containing an array of releases from the ember-cli repository. Above that, you should also see that it returned a promise. Remember that promises are chainable, so when we called .then, RSVP.js (the promise implementation used by Ember) created a new promise representing the new value and returned that instead. Try calling .then on that promise and see what value gets handed into the callback, like so:

require('ic-ajax').default({
  url: 'http://api.github.com/repos/stefanpenner/ember-cli/releases',
  type: 'get'
}).then(function(response) {
    console.log('response:', response);
}).then(function(response2) {
    console.log('response2:', response2);
});

If you did it right you should get response2: undefined. Why? Because we aren’t returning anything from the first .then callback. Try changing it to:

require('ic-ajax').default({
  url: 'http://api.github.com/repos/stefanpenner/ember-cli/releases',
  type: 'get'
}).then(function(response) {
    response.testing = 'testing';
    return response;
}).then(function(response2) {
    console.log('response2:', response.testing);
});

Now if you run that, you should see "testing" in the console. So how does this apply to the timestamp problem mentioned previously? Remember, we want to get the timestamps as regular JavaScript date objects so we can format them nicely later in the UI. Given what we just discovered, we can do it right in our model callback:

import ajax from 'ic-ajax';

export default Ember.Route.extend({
    model: function(params) {
        return ajax({
            url: 'http://api.github.com/repos/' + params.owner + '/' + params.repo + '/releases',
            type: 'get'
        }).then(function(releases) {
            releases.forEach(function(release) {
              release.created_at = new Date(release.created_at);
              release.published_at = new Date(release.published_at);
            });
            return releases;
        });
    }
});

Now in our UI we can refer to the created_at and updated_at properties and know we have proper date objects instead of just strings!

5 Render a list of items in a template

Now that we can retrieve a list of releases from the GitHub API, we should probably do something with them. The goal of our application is to see what commits are contained in a given release, something that is extremely useful, but not very easy to do on GitHub itself. Unfortunately we will not get there in the scope of this article, but that’s what we are working towards.

So it seems the next logical step would be to display a list of releases and let the user choose which release they want to see commits for. A reasonable UI for this would be to use a master-detail pattern, showing the list of releases on the left and the details of the selected release on the right. To keep things simple for this step, we’ll display the name of the release and the release notes in the detail. Also, we’re using Zurb Foundation for UI styles.

5.1 Create a template

Template names match the name of the route that they display within. Therefore, we’ll create a template in app/templates/releases.hbs. We’ll iterate over the releases that came from the API and then display the tag name for each release.

<div class="row">
    <div class="large-2 columns">
        <ul class="side-nav">
            {{#each}}
                <li>{{tag_name}}</li>
            {{/each}}
        </ul>
    </div>
</div>

each is a Handlebars helper that iterates over a collection. If you don’t pass a collection, as we’re doing here, it will automatically use the current context (this). In Ember, templates are rendered using the route’s model as the context. Since our route has an array as its model, we can iterate over it without having to explicitly pass a variable to each.

If you’ve used other client-side MVC frameworks you may be accustomed to checking the status of the API request before trying to display anything, perhaps to display a spinner until the request is complete. Thanks to the built-in promise support in the Ember router we don’t have to worry about that here! Of course, that means the page will be blank until the data is loaded. We’ll go over how to display a loading state later in this article.

If you load this new page in the browser (eg. http://localhost:4200/stefanpenner/ember-cli/releases) you should now see a list of tags down the left side of the page. The only problem is, they aren’t clickable! Let’s fix that.

5.2 Linking to other pages with link-to helper

For each release displayed in the list, we want to link to a state that displays that release’s details. You might be tempted to do something like:

><a href="/releases/{{release.id}}">{{tag_name}}</a>

This won’t work for a couple of reasons. The main reason you don’t want to do this is because the URLs are completely controlled by the router. If you change the router’s configuration, you don’t want to have to go through the app and change every instance of a changed URL. Instead, Ember provides the link-to helper that uses the router’s configuration to generate link tags. So instead we can simply do:

{{#each}}
    {{link-to tag_name "releases.show" this}}
{{/each}}

The first parameter to link-to is the text to display, in this case tag_name. The second parameter takes a route path and the third is the actual object with the details we want to display. Save and reload the page — if you get a blank page, open up your browser’s developer tools and look at the console output. You should see an error message, something like:

Uncaught Error: Assertion Failed: The attempt to link-to route ‘releases.show’ failed. The router did not find ‘releases.show’ in its possible routes: ‘loading’, ‘error’, ‘auth’, ‘callback’, ‘releases.loading’, ‘releases.error’, ‘releases.new’, ‘releases.edit’, ‘releases.index’, ‘releases’, ‘index’, ‘application’

This is because we don’t actually have a show route for our releases resource yet. Let’s create one by changing app/router.js to look like:

this.resource('releases', {path: '/:owner/:repo/releases'}, function() {
    this.route('show', {path: '/:release_id'});
});

Next let’s implement our show template for releases. Create app/templates/releases/show.hbs and add the following:

<div class="row">
  <div class="large-12 columns clearfix">
    <h2 class="left">{{name}}</h2>
  </div>
</div>

<div class="row">
  <div class="large-12 columns">
    {{body}}
  </div>
</div>

Now reload the page and click on a model. You should get…absolutely nothing! No errors, and certainly no release details. But look at the URL, do you see the release ID at the end? Obviously something worked, but why don’t we see the template?

5.3 Use of outlets with nested routes

This is where Ember routing is very different from what you might be used to if you’ve used a server-side MVC framework. As I mentioned earlier, the Ember router can have multiple routes loaded at once, in a tree layout. When we just had a list of releases, the releases route was loaded in memory. Clicking on a release kept the releases route loaded and also loaded up the releases.show route underneath it. It rendered the show template, but we need to tell Ember where to insert it into the current context. We do this by using the outlet helper. Change the releases template to load in the details next to the list of releases:

<div class="row">

  <div class="large-2 columns">
    <ul class="side-nav">
    <li>{{link-to "+ New Release" "releases.new"}}</li>
    {{#each}}
      <li>{{link-to tag_name "releases.show" this}}</li>
    {{/each}}
    </ul>
  </div>

  <div class="large-10 columns end">
    {{outlet}}
  </div>

</div>

Make sure you use the browser’s back button to unload the show route (the URL should end with /releases) and then refresh the page. If you click on a release you should now see the details loaded in next to it. Click around a bit to watch the different show routes swap out with one another.

6 Direct Access to Router States

While you’re viewing the release details, reload the page. You should have a different error now:

Error while processing route: releases.show No model was found for ‘release’ Error: No model was found for ‘release’

This is actually a bigger problem than just reloading the page. If you try to share this link with someone else they will get the error too! Why? What do we already know about how the router works? It expects a model callback to try to load data from the API.

Since we haven’t created our own route handler yet, Ember will fall back to the default behavior for a route with a single dynamic segment (/:release_id in the router). Unfortunatley, since we’re just using raw responses from GitHub’s API, the default behavior isn’t going to work for us. We need to create our own model callback. Create app/routes/releases/show.js and add:

import ajax from 'ic-ajax';

export default Ember.Route.extend({
  model: function(params, transition) {
    var owner = transition.params.releases.owner,
        repo = transition.params.releases.repo;
    var url = 'https://api.github.com/repos/' + owner + '/' + repo + '/releases' + '/' + params.release_id;
    return ajax({
      url: url,
      type: 'get'
    });
  }
 });

Reload the page again and bask in the glory of a working product!

6.1 Using the afterModel hook for additional API calls

Go back to your browser and look at a release. Notice the release notes? Looks pretty bad, right? That’s because you’re looking at the raw Markdown, but on GitHub you see it rendered as HTML. We should do the same here. The Markdown we use on GitHub is a superset of traditional Markdown known as GitHub Flavored Markdown (GFM), and there’s no library that supports GFM entirely. However, the GitHub API gives us an endpoint that renders GFM in the same way as the server does. If we want our app to look the best, we should use that.

Think for a second about how we could do this. The simplest thing we could do is put the API call in a .then inside the route’s model callback. Then we can set a property on the model called body_html once we have the rendered content. Make your model callback look like the following:

var owner = transition.params.releases.owner,
    repo = transition.params.releases.repo;
var url = 'https://api.github.com/repos/' + owner + '/' + repo + '/releases' + '/' + params.release_id;
return ajax({
  url: url,
  type: 'get'
}).then(function(release) {
  return ajax({
    url: 'https://api.github.com/markdown',
    type: 'POST',
    contentType: 'application/x-www-form-urlencoded',
    dataType: 'text',
    data: JSON.stringify({
      text: release.body,
      mode: 'gfm',
      context: owner + '/' + repo
    })
  }).then(function(text) {
    release.body_html = text;
    return release;
  });
});

And then update app/releases/show.hbs to display that property:

<div class="row">
  <div class="large-12 columns clearfix">
    <h2 class="left">{{name}}</h2>
    {{link-to "Edit" "releases.edit" model class="button tiny right"}}
  </div>
</div>

<div class="row">
  <div class="large-12 columns">
    {{{body_html}}}
  </div>
</div>

Notice the triple-handlebars around {{{body_html}}}? By default, Handlebars (the template-rendering library used by Ember) will escape all markup inside strings, so we use this syntax to tell Handlebars that we know this string contains safe HTML.

Now reload the page we were just looking at. Looks great, right? But there’s still a problem here. Select another release in the sidebar. The release notes are gone! What could have happened here?

Remember when we used the link-to helper to create those sidebar links?

{{link-to tag_name "releases.show" this}}

The last parameter, this, is the release we want Ember to use for the route’s model when we transition to it from clicking the link. Since we’re already handing in a model, the router will skip the model callback because it would result in unnecessary API requests. So where do we do our Markdown rendering? A quick look through the API docs shows that there is an afterModel callback we can use. This gets called after the model gets set, either through a transition (as in the link-to scenario), the value returned from model or the resolved promise returned from model.

This means we can be confident we’ll always have a usable model object in this callback. afterModel also supports the same promise behavior as model, meaning we can safely make an API call here and know we’ll have the rendered content by the time the template renders.

Undo the previous changes to app/routes/releases/show.js and update it to include the afterModel hook:

export default Ember.Route.extend({
  model: function(params, transition) {
    // snipped for space
  },
  afterModel: function(model, transition) {
    var owner = transition.params.releases.owner,
        repo = transition.params.releases.repo;
    return ajax({
      url: 'https://api.github.com/markdown',
      type: 'POST',
      contentType: 'application/x-www-form-urlencoded',
      dataType: 'text',
      data: JSON.stringify({
        text: model.body,
        mode: 'gfm',
        context: owner + '/' + repo
      })
    }).then(function(text) {
      model.body_html = text;
      return model;
    });
  }
});

Reload the page and you should still have rendered release notes. Select some other releases from the sidebar and you should see that the release notes are always rendered.

7 Handling API errors & loading states

The router supports a couple events that are useful to understand. You can use these events to handle errors in API requests or to give the user some feedback when a particular request is taking a while to load. These all make use of an Ember feature called actions.

7.1 Error template

The easiest way to implement error handling is to create a template in app/templates/error.hbs and use it to display a user-friendly notification. The error template is rendered with the error object as its context, so you could do something like:

<h1>The system encountered an error processing that request. Please refresh or try again later.</h1>

<h3>The error was:</h3>

<p>{{message}}</p>
<pre>
{{stack}}
</pre>

7.2 Handling errors with the error action

If you need some more custom error handling, perhaps to log errors to an error collecting service or render different templates based on the type of error, you can use the error action in a route.

7.2.1 How actions bubble in Ember

Actions in Ember work similarly to events in JavaScript in that they bubble up to some parent object. Note this does not use the inheritance chain; rather, it uses the conceptual hierarchy that Ember implements in its lifecycle. Meaning an action sent to a controller will be called first on the controller it’s sent to, then (if the action is not handled or returns true) bubble up to the route that created the controller, and action sent to a template will then be sent to its controller, and so on.

What about actions sent to routes? They will be called first on the route they are sent to, then (if necessary) be sent up the route hierarchy to any parent resource handlers and finally to the application route.

7.2.2 Implementing a global error action

So, to implement a global error handler we can handle the error action on the application route. Create a file in app/routes/application.js with the following:

ErrorLoggingService = {
    logError: function(error) {
        console.log('TODO: Log error via ajax request', error);
    }
};

export default Ember.Route.extend({
    actions: {
        error: function(transition, error) {
            ErrorLoggingService.logError(error);
            this.render('error', {
                controller: error, 
                into: 'application'
            });
        }
    }
});

Let’s trigger a test error in the model callback on app/routes/releases/show.js. You can easily trigger an error by creating a rejected promise like so:

model: function(params) {
    return Ember.RSVP.reject(new Error('test error'));
    // var owner = transition.params.releases.owner,
    //     repo = transition.params.releases.repo;
    // var url = 'https://api.github.com/repos/' + owner + '/' + repo + '/releases' + '/' + params.release_id;
    // return ajax({
    //   url: url,
    //   type: 'get'
    // });
}

Now select a release and reload the page so the model callback gets called. You should see your error, and you’ll see the message from the logging service in the developer tools log.

7.3 Displaying a loading spinner

Besides the error action, Ember routes also support a loading action. This gets called when a route’s model hook returns a promise that is not already resolved. Just like the error state, you can either implement it as a template or take some custom action. In our GitHub releases example, loading a release and parsing the markdown into HTML has the potential to take a few seconds. How would we display a spinner while all this is going on?

Create a template in app/templates/releases/loading.hbs with this content:

<style>
/**
 * (C)Leanest CSS spinner ever
 */

@-webkit-keyframes spin {
  to {
    transform: rotate(1turn);
    -webkit-transform: rotate(1turn);
  }
}

.spinner {
  position: relative;
  display: inline-block;
  width: 5em;
  height: 5em;
  margin: 0 .5em;
  font-size: 12px;
  text-indent: 999em;
  overflow: hidden;
  animation: spin 1s infinite steps(8);
  -webkit-animation: spin 1s infinite steps(8);
}

.small.spinner {
  font-size: 6px;
}

.medium.spinner {
  font-size: 18px;
}

.large.spinner {
  font-size: 24px;
}

.spinner:before,
.spinner:after,
.spinner > div:before,
.spinner > div:after {
  content: '';
  position: absolute;
  top: 0;
  left: 2.25em; /* (container width - part width)/2  */
  width: .5em;
  height: 1.5em;
  border-radius: .2em;
  background: #eee;
  box-shadow: 0 3.5em #eee; /* container height - part height */
  transform-origin: 50% 2.5em; /* container height / 2 */
  -webkit-transform-origin: 50% 2.5em; /* container height / 2 */
}

.spinner:before {
  background: #555;
}

.spinner:after {
  transform: rotate(-45deg);
  -webkit-transform: rotate(-45deg);
  background: #777;
}

.spinner > div:before {
  transform: rotate(-90deg);
  -webkit-transform: rotate(-90deg);
  background: #999;
}

.spinner > div:after {
  transform: rotate(-135deg);
  -webkit-transform: rotate(-135deg);
  background: #bbb;
}

.loading-container {
  padding: 1.5em;
  padding-left: 3em;
}

</style>

<div class="loading-container">
  <div class="spinner medium">
    <div>Loading...</div>
  </div>
</div>

This route typically loads fairly quickly, so we’ll pause for 3 seconds in the route before resolving the promise — this way we can see the full effect of the loading spinner before it goes away. Change the model callback in app/routes/releases/show.js to the following:

    var owner = transition.params.releases.owner,
        repo = transition.params.releases.repo;
    var url = 'https://api.github.com/repos/' + owner + '/' + repo + '/releases' + '/' + params.release_id;
    return new Ember.RSVP.Promise(function(resolve, reject) {
      var p = ajax({
        url: url,
        type: 'get'
      });
      p.then(function(result) {
        setTimeout(function() {
          resolve(result);
        }, 3000);
      });
    });

Now, in the browser, select a release and reload the page. You should see the spinner appear then get replaced by the actual release details. You can actually implement loading templates at any level of the route hierarchy. If you create a template in app/templates/loading.hbs it will be rendered into the application outlet any time a route triggers its loading state

7.5 Custom actions

Of course, you aren’t limited to just error and loading actions. You can also implement your own actions; for instance, recording an event to Google Analytics before performing some other action. To do this, update app/templates/releases.hbs to use an action rather than the link-to helper:

<div class="row">

  <div class="large-2 columns">
    <ul class="side-nav">
    <li>{{link-to "+ New Release" "releases.new"}}</li>
    {{#each controller}}
    <li><a href="#" {{action "selectRelease" this on="click"}}>{{tag_name}}</a></li>
    {{/each}}
    </ul>
  </div>

  <div class="large-10 columns end">
    {{outlet}}
  </div>

</div>

We switched to using a regular a tag, and inside the angle brackets we’re telling it to call an action named selectRelease when this release is clicked. We can change this to use any DOM event that the tag supports by changing the string passed to on; click is actually the default, so if we remove it it won’t make a difference. We can implement the selectRelease action in the releases route — edit app/routes/releases.js to look like this:

import ajax from 'ic-ajax';

var AnalyticsService = {
  track: function(eventName, data) {
    console.log("Tracking analytics hit with ", eventName, data);
  }
};

export default Ember.Route.extend({
    model: function(params) {
        return ajax({
            url: 'http://api.github.com/repos/' + params.owner + '/' + params.repo + '/releases',
            type: 'get'
        }).then(function(releases) {
            releases.forEach(function(release) {
              release.created_at = new Date(release.created_at);
              release.published_at = new Date(release.published_at);
            });
            return releases;
        });
    },
    actions: {
        selectRelease: function(release) {
          AnalyticsService.track('release', release);
          this.transitionTo('releases.show', release);
        }
    }
});

Now if you go back to the browser, reload, and select a release you should see output from the AnalyticsService in the console.

8 Closing thoughts

When you need it, using Ember can bring enormous productivity gains. It has a rich community, and with tools like ember-cli is pushing the state-of-the-art in development workflows. It’s currently being used by some major companies including Yahoo, Twitter, Square, Groupon and more.

8.1 Where do I go from here?

This tutorial gave you a quick overview of the concepts you’ll need to write Ember apps. If you want a more in-depth tutorial I’d recommend going through the official getting started tutorial. If you learn better with an instructor, there’s also a CodeSchool course available.

If you’re the type that likes to dig right in, I’d recommend purusing the official Ember guides and keeping them open as you work.

If you have any additional questions or problems as you’re getting into Ember, I’m happy to jump on an AirPair and help you out!

The post Ember.js Tutorial – using Ember CLI appeared first on .


Viewing all articles
Browse latest Browse all 36

Trending Articles