Getting Started with Ember.js

in utopian-io •  7 years ago  (edited)

What Will I Learn?

Getting started with Ember.js and creating a simple application.

Requirements

  • Ember.js
  • Handlebars.js
  • Jquery

Difficulty

Intermediate

Tutorial Contents

  • Learning Curve
  • Comparison to Backbone.js
  • Understanding the pros and cons of ember.js conventions
  • Setting up
  • Adding HTML with Handlebars.js
  • App.js : Minimal Viable Ember App
  • Simple Router
  • Park Center Backetball League App
  • Using A Namespace
  • Evolving our Route Handlers in PC.BballLeague
  • Adding Context
  • Using Set Up Controller
  • Getting Data from Fixtures
  • Navigating Within the App
  • Updating the URL

Learning Curve

It is well documented in the developer forums and on twitter that learning ember.js is difficult at first. While the curve starts out steep, I must ensure you that it is worth your time, and the rewarding result is you have a framework that provides an amazing amount of flexibility and tooling that is painstaking to write custom.When explaining ember.js, models tutorials start with the data models first, and others the router. But after reading through many of these and learning ember.js on my own, I came to the conclusion that starting with the templates and HTML first made the concepts the most clear.

Comparison to Backbone.js

It would be a challenge to talk about the benefits of ember.js without comparing it to other micro-framework client-MVC libraries like backbone.js.Ember.js provides a lot more than backbone.js as it is intended to solve broader problems presented by 'ambitious' single page applications. Backbone.js is a micro-framework that shines when you need a simple, lightweight, client-MVC solution.

Some of these broader problems include: providing a single, global DOM listener that prevents the developer from having to create and clean up event listeners manually when creating and tearing down views. This includes the ember run loop that coalesces events together at the end of the loop resulting in a single repaint when data sets update, and providing the ability to override at individual abstraction layers to have backbone-like low level control. In other words, hooks are provided to render directly into a buffer, manually handle low level events, or formulate your XHRs the way you like them.

Understanding the pros and cons of ember.js conventions

If you would like to get moving quickly, ember.js provides a ton of magic behind the scenes. These conventions will be discussed in detail throughout this tutorial.While incredibly useful, these conventions can also be quite frustrating to some developers at first. Especially, those that want to know what is going on 'under the covers.'

This philosophy is very much part of what has become a common Ruby On Rails mantra: "Look at all the things I'm not doing," to quote the founder, David Heinemeier Hansson. While extremely valuable, these frameworks typically trade reduction in developer pain by following the conventions, for creating developer pain when steering off of the path.

Also, keep in mind, ember.js is so well abstracted, it is necessary to learn each layer of the abstraction in order to understand how they fit together. My advice would be to build as solid foundation as you can in each layer before tackling a project, or recommending it as a framework to achieve a business objective.

In short, learn the ember.js 'way' and try to stick to it.

Setting up

The root directory contains the index.html file and a subdirectory /js. The /js directory has five files:

  • ember.js (from emberjs.com)
  • ember-data.js (from emberjs.com)
  • handlebars.js (from handlebars.com)
  • jquery.js (from jquery.com)
  • app.js: our application code

Also included is an index.html with the libraries loaded in the correct order:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Tech.pro Getting Started with Ember.js: Team Roster</title>
    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <script src="js/libs/jquery-1.9.1.js"></script>
    <script src="js/libs/handlebars-1.0.0-rc.3.js"></script>
    <script src="js/libs/ember-1.0.0-rc.2.js"></script>
    <script src="js/libs/ember-data.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

For now, launch your favorite webserver and make sure this code loads without errors showing up in the console.

Adding HTML with Handlebars.js

As I stated before, lets start with HTML. Ember.js uses HTML files with Handlebars templates, so before we get started, let's take a brief look at handlebars.js.There are multiple ways to use Handlebars templates in an ember.js application: inline script tags with type="text/x-handlebars" and using data-template-name to pass Ember a reference, as .handlebars or .hbs files, as an explicit string variable in your code, or as the result (responseText) of an XMLHttpRequest.For getting started, I like to work with templates using the inline script tag declarations. Inline templates are formatted like this:

<script type="text/x-handlebars" data-template-name="application">
  <h1>Basketball League: Team Rosters</h1>    
  {{outlet}}
</script>

Next, let's add the minimum we need to our app.js file and continue onto the templates.

App.js : Minimal Viable Ember App

The smallest possible Ember application of interest can be described as thus:

App = Ember.Application.create();

App.Router.map(function() {
 // put your routes here
});

App.IndexRoute = Ember.Route.extend({
 model: function() {
   return ['foo', 'bar']
 }
});

And in our HTML document body or head:

<script type="text/x-handlebars" data-template-name="application">
  {{outlet}}
</script>

Let's examine each piece of this minimal app in isolation in order to understand the basics. First, create() sets up the application.

App = Ember.Application.create();

Then we can pass a templateName that corresponds to the foundational, application template as designated by the data-template-name="application" on the script tag with the "text/x-handlebars" type.

App.ApplicationView = Ember.View.extend({
 templateName: 'application'
});

But you wil notice this is not included in the minimal app code above. Here is an example as to where App.ApplicationView does not need to be created, it will be created 'behind the scenes' by convention.Outlet is equivalent to yield in Rails. The {{outlet}} declaration inside of the handlebars template is populated by the dynamic content that is reflective of the 'state' of the application. For now, it is empty, but later we will begin to include sub content within {{outlet}} as the user navigates through the application.

<script type="text/x-handlebars" data-template-name="application">
  {{outlet}}
</script>

Simple Router

In the example above, you will also notice the following router code.

App.Router.map(function() {
 // put your routes here
});

App.IndexRoute = Ember.Route.extend({
 model: function() {
    return ['foo', 'bar']
 }
});

This is a very basic router that handles accessing our minimal example from the root url. Notice that inside of our map function, we will begin to define more routes as our app grows. IndexRoute is a route handler and another convention provided by ember.js to handle the state of the application when visiting the root route at /.

PC.BballLeague: Park Center Backetball League App

Now, lets add some data and begin expanding upon this foundation.To do this we will create a simple application that lists out teams that are competing in a youth basketball league. Our app will include two sections: one, a basic info section and two, a list of clickable teams.

Using A Namespace

As your application continues to grow, it is advisable to namespace your code for clarity sake and to sandbox your classes and variables from Global scope.First, we will set up a PC global namespace object. If PC is already defined, the existing PC object will not be overwritten so that defined namespaces are preserved.

if (typeof PC == "undefined" || !PC) {
 var PC = {};
}

Then, we name our application BballLeague and add it to the global namespace: PC.

PC.BballLeague = Ember.Application.create();

And then we can configure a helpful property, LOG_TRANSITIONS, on our application.

PC.BballLeague= Ember.Application.create({
 LOG_TRANSITIONS: true
});

As your application increases in complexity, it can be helpful to see exactly what is going on with the router. Now, we are ready to explore the router.

Evolving our Route Handlers in PC.BballLeague

Now, lets begin to add more routes to the router.

PC.BballLeague.Router.map(function() {
 this.route("home", { path: "/" });
 this.resource('team', { path: '/team/:team_id' });
});

The goal of our application is to provide a 'home' page that contains basic info about the league and a list of the teams that make up the league. As the user clicks on the team they will be directed to a 'detail' page that provides data specific to the team.

Adding Context

Let's add basic information data to the home page. We should have two templates. The main application template:

<script type="text/x-handlebars">
  <h1>Basketball League: Team Rosters</h1>    
  {{outlet}}
</script>

And an additional template that is placed into 'application' when the application is in the 'home' state as indicated by the 'home' route in the router:

this.route("home", { path: "/" });

Rendering the 'home' template within {{outlet}}:

<script type="text/x-handlebars" data-template-name="home">
  <h3>League Info</h3>
    <ul>
      {{#each item in info}}
        <li>{{item}}</li>
      {{/each}}
    </ul>
</script>

So, where does info come from?It always comes from a controller. The HomeController will created by convention. But, we can make it come from a controller that we created, rather than auto generated by ember.js conventions.So instead of accessing it from the model in the route handler:

App.HomeRoute = Ember.Route.extend({
 model: function(){
  return ['Coed', 'Saturdays from 9AM-3PM', 'at Park Center']
 },
...
});

We can set up a controller:

App.HomeController = Ember.Controller.extend({
     info: ['Coed', 'Saturdays from 9AM-3PM', 'at Park Center'];
});

And we can now change our code to use a property from a Controller instead of a hardcoded model. So, we can change our template from this:

<ul>
    {{#each item in info}}
      <li>{{item}}</li>
    {{/each}}
 </ul>

to this:

<ul>
    {{#each item in content}}
      <li>{{item}}</li>
    {{/each}}
 </ul>

Using Set Up Controller

In addition to our 'info' data, we also need to include the 'teams' data within our home page.The HomeRoute route handler is going to populate the teams.

var teams = [
     "Celtics",
     "Lakers",
     "Bulls"
]

App.HomeRoute = Ember.Route.extend({
     setupController: function(controller){
          controller.set('teams', teams)
     }
})

So, in setupController() we setup context for the current template.

<ul> 
   {{#each team in teams}}
      <p> {{team.name}} </p>
   {{/each}}
 </ul>

Now, we can add both datasets to the HomeController:

PC.BballLeague.HomeRoute = Ember.Route.extend({
 model: function(){
  return ['Coed', 'Saturdays from 9AM-3PM', 'at Park Center']

 },
 setupController: function(controller, model){
   controller.set('info', model)
   controller.set('teams', PC.BballLeague.Team.find());
 }
});

Getting Data from Fixtures

For the sake of testing and building our app without a connection to the server we can use Fixtures.To do so, we need to know a little about ember-data, and how to set up a basic data store to use the default FixtureAdapter. We add the following to app.js.

PC.BballLeague.Store = DS.Store.extend({
 revision: 12,
 adapter: 'DS.FixtureAdapter'
});

And then build some basic fixtures:

PC.BballLeague.Team.FIXTURES = [{
 id: 1,
 name: 'Celtics',
 colors: 'Green, White'
}, {
 id: 2,
 name: 'Lakers',
 colors: 'Yellow, Black'
}, {
 id: 3,
 name: 'Bulls',
 colors: 'Red, Black'
}, {
 id: 4,
 name: 'Mavericks',
 colors: 'Blue, White'
}, {
 id: 5,
 name: 'Spurs',
 colors: 'Black, Grey, White'
}];

Adding Async Data Requests

We can also make requests to the server for our data.

PC.BballLeague.HomeRoute = Ember.Route.extend({
 model: function(){
  return ['Coed', 'Saturdays from 9AM-3PM', 'at Park Center']

 },
 setupController: function(controller){
   jQuery.getJSON('/teams').then(function(json){
     controller.set('teams', json.teams)
   })
 }
});

We also need to make a slight change to our adapter:

PC.BballLeague.Store = DS.Store.extend({
 revision: 12,
 adapter: 'DS.RESTAdapter'
});

Our template stays exactly the same.The data bindings between the controller and the template are responsible for automatically updating the template when the data comes back from the server.This is known as live templates, which is a feature of handlebars.js.

So what about a loading spinner?

{{#if teams.isLoaded}}
 <ul> 
   {{#each team in teams}}
      <p> {{team.name}} </p>
   {{/each}}
 </ul>
 {{else}}
 <p class="loading">...Loading teams</p>
 {{/if}}

So, if you update controller .isLoaded, then the template will automatically update.

PC.BballLeague.HomeRoute = Ember.Route.extend({
  setupControllers: function(controller){
    jQuery.getJSON('/teams').then(function(json){
      controller.set('teams', json.teams)
      controller.set('isLoaded', true)
    })
  }
})

As I mentioned earlier, in backbone.js this would perform 2 repaints to the page. In Ember, it doesn't matter how many updates or sets are called they are all coalesced together at the end of the run loop.Just like the browser, the browser waits to see that your interacting and firing a bunch of events, and it waits until your are finished to do a repaint. In other words, it doesn't try to repaint on every event.

That is how the Ember run loop works. More on the run loop can be learned by studying Sproutcore. Although it is not an identical implementation, it is well known that this is a good starting point.

Using Models

So clearly doing jQuery.getJSON in your Router could quickly get out of control as your app grows more complex.Lets further explore ember-data, and how it integrates in with the new Router. First, let's set up a basic model for our teams.

PC.BballLeague.Team = DS.Model.extend({
  name: DS.attr('string'),
  colors: DS.attr('string')
})

Using the RESTAdapter

So now the Route handler becomes a little simpler.

PC.BballLeague.HomeRoute = Ember.Route.extend({
  setupControllers: function(controller){
    controller.set('teams', PC.BballLeague.Team.find())
  }
})

PC.BballLeague.find() returns a RecordArray, which already has an isLoaded property on it.It doesn't matter which ember-data method it is: find, findAll, or findQuery, they all have the isLoaded property.So we could use this to use the model() on HomeRoute handler, like so:

PC.BballLeague.HomeRoute = Ember.Route.extend({
  model: function(){
    return PC.BballLeague.Team.find();
  },
  setupControllers: function(controller, model){
    controller.set('teams', model)
  }
})

Now, we can update the template because the teams.isLoaded comes from the RecordArray.

{{#if teams.isLoaded}}
 <ul>
   {{#each team in teams}}
   <li>{{team.name}}</li>
   {{/each}}
 </ul>
 {{else}}
 <p class="loading">Loading teams</p>
 {{/if}}

A Record Array is an Array that is decorated with some attributes like isLoaded and knows how to async update things when it changes. In ember.js, every single array you work with, is observable. If you use a #each with one of these array-like objects the array-like object updates after the fact. Ember will automatically go render new rows, delete rows, or remove the entire object.

Create an ObjectController

 PC.BballLeague.TeamController = Ember.ObjectController.extend({
   teams: null
 })

Basically, an ObjectController decorates the underlying content, the underlying Model, and allows you to add additional properties.In backbone.js, this often comes up, you have Model and I want some extra stuff that only makes sense in the view.

One might ask, where do I put that logic? In ember.js, the answer is you put it into the controller. ObjectController works like an object proxy. So … if you don't have any additional properties it works like it was a regular model. If you want to add additional properties it serves as a convenient place to put them if they don't need to be persisted in the database. The model should have only persistable properties.

Dynamic Segments to the Router

Our goal is to be able to navigate into a team's detail page by clicking on one of the team's names.

Let's start with the API. You can use route or resource. If you see examples using match, you should know you are looking at old code of Router version 1. This is now deprecated.

PC.BballLeague.Router.map(function() {
  match("/").to("home")
  match("/teams/:team_id").to("team")
}

Instead use this syntax:

PC.BballLeague.Router.map(function() {
 this.route("home", { path: "/" });
 this.resource('team', { path: '/team/:team_id' });
});

We have added a dynamic segment that should be very familiar, if you have used any other routing system.

PC.BballLeague.HomeRoute = Ember.Route.extend({
  model: function(){
    return PC.BballLeague.Team.find();
  },
  setupController: function(controller, model){
    controller.set('teams', model)
  }
})

Again notice the model param ...

PC.BballLeague.HomeRoute = Ember.Route.extend({
     setupController: function(controller, model){
          controller.set('content', model)
     }
})

The cool thing about using dynamic segments in the Ember router is that if you have a team Model, then ember.js will automagically do this for you:

PC.BballLeague.Team.find(params.team_id)

And automatically pass this into the Model. We saw before you could have a model hook, and define what the model should be. If you don't override the model hook by convention, it will just be the model with that id. You don't have to write any additional code.The controller.set('content', model) is also the default.In other words so is:

 setupController: function(controller, model){
   controller.set('content', model)
 }

So you can do this:

PC.BballLeague.TeamRoute = Ember.Route.extend({})

Or even remove it too and have nothing.

this.resource('team', { path: '/team/:team_id' });

So, this is all you need.

This is possible because ember.js uses routing conventions similar to how Ruby on Rails works. If all you want to do is have a template with a team, and with an id, the app should show the team based on the id. You shouldn't have to do any additional work.

As I stated in the beginning of this article, all of this is also configurable. If you want to override this, there are hooks. If you want the id to be a little different, like slugs, first add a 'slug' property to the model.

PC.BballLeague.Team = DS.Model.extend({
   slug: DS.attr('string'),
   name: DS.attr('string'),
   colors: DS.attr('string'),
});

Then, use the serialize method in your route.

PC.BballLeague.HomeRoute = Ember.Route.extend({
 serialize: function(model, params) {
   return { team_id: model.get('slug') };
 }
});

Other hooks you can override are:

  • model
  • setupController
  • renderTemplate

And then, do whatever you want. But, just like Rails, if you stay on the path, you will get a lot for free.

Navigating Within the App

So, how do we go from the home page to the team's detail page?

We want to display a list of teams, so we can set a property in the TeamController called teams of all the teams that exist.Lets go to the home template, and add #linkTo to our handlebars template.

{{#if teams.isLoaded}}
 <ul> 
   {{#each team in teams}}
    {{#linkTo "team" team}}
      <p> {{team.name}} </p>
    {{/linkTo}}
   {{/each}}
 </ul>
 {{else}}
 <p class="loading">...Loading teams</p>
 {{/if}}

So now we have a list of anchor tags. You can click on them and they will direct you to the Team's detail page.So, how does this work?

{{#linkTo "team" team}}

points to the following in the router:

this.resource('team', { path: '/team/:team_id' });

So we are saying goto the 'named route' that is 'team'. This is a unique name for every route in the system, similar to how rails handles named routes.

Updating the URL

So how do we know how to update the url?

{{#linkTo "menu_item" menuItem}}

By using a naming convention to pass this team context and update the url in browser bar.

this.resource('team', { path: '/team/:team_id' });

So, this same team here:

{{#linkTo "team" team}}

gets passed into model.So if you come in from a navigation, link, or bookmarks, then ember.js uses:

PC.BballLeague.Team.find(params.team_id);

But if you navigate internally via linkTo , then the system does the opposite. Ember.js uses the team as the context and creates the serialized url.

The same context is passed into setupController, whether you came from external url or an internal linkTo.

Conclusion

So that's it. This should be enough to get you started building an ember.js application. Ember offers a well balanced architecture that provides well thought out abstraction layers, conventions to speed up trivial development, and the ability to get lower level control through configuration if the minimalist approach is necessary. 



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @jestemkioskiem, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

@yissakhar, Approve is not my ability, but I can upvote you.

Hey @yissakhar I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Thanks for your good posts, I followed you!