While most of my recent work has primarily been with Ruby on Rails and a liberal dose of Javascript (mainly jQuery), I would like to build a single page application and realize that Ember.js seems to be an up-and-coming popular framework for approaching such apps.
From various sources of documentation and tutorials, it seems that Ember.js requires a very different way of thinking about how to solve problems than Ruby on Rails or other typical server-side frameworks. It seems possible that certain assumptions about 'the way things should work' that one develops over time using a framework such as Ruby on Rails may even get in the way of truly understanding and embracing the 'Ember Way.'
What preconceived notions should a Ruby on Rails developer need to eliminate when trying to learn Ember? And what are the most innovative and important Ember concepts that a Ruby on Rails developer should try to wrap his/her mind around?
Thanks in advance!
I'm going to do my best to answer this question within the spirit of StackOverflow by listing some major technical differences between Ember and Rails. I'll leave the more philosophical side for somebody else over at programmers.stackexchange.com.
You can find all the code examples below in a working jsFiddle if that helps you visualize how everything fits together.
One major difference between Ember and Rails is the relation between collection routes (which manage a list of objects) and item routes (which manage a single object). In Rails, these are both handled by a single resource controller. In Ember, these are generally handled by two separate routes, because they manipulate two different data structures:
App.Router.map(function () {
this.route("posts", { path: "posts" });
this.route("post", { path: "post/:post_id" });
});
App.PostsRoute = Ember.Route.extend({
model: function (params) {
return App.Post.find();
}
});
App.PostRoute = Ember.Route.extend({
model: function (params) {
return App.Post.find(params.post_id);
}
});
In Rails, your code is split between three major groups of classes:
In Ember, the breakdown of responsibilities is considerably different.
Models. Ember models work much like Rails models.
App.Post = DS.Model.extend({
title: DS.attr("string"),
body: DS.attr("string"),
comments: DS.hasMany("App.Comment")
});
Routes. Routes represent user-visible locations within your app, and they correspond to URLs like /post/7
or /about
. As you can see in the code examples above, routes do a lot more in Ember. Most importantly, they look up the models corresponding to a given URL. They're also in charge of hooking up appropriate controllers and views, as you'll see in a second.
Controllers. Controllers are nothing like Rails! The two most important things to understand about Ember controllers are that: (1) they're basically smart proxies around model objects, and (2) they're normally singletons. So you'll only have one PostController
which will be wired up to whichever post you're looking at right now.
Generally speaking, you use Ember controllers to manage transient state that doesn't belong in the URL or in the database. Here's an example:
App.PostController = Ember.ObjectController.extend({
// This shared between all posts for as long as the app runs (because
// this controller is a singleton), but it doesn't get saved in the database
// (because this is a controller, not a model).
lowRatedCommentsShown: false,
// Called by PostView or its template in response to an HTML event.
showLowRatedComments: function () {
this.set("lowRatedCommentsShown", true);
},
// Called by PostView or its template in response to an HTML event.
hideLowRatedComments: function () {
this.set("lowRatedCommentsShown", false);
}
});
Because Ember controllers are proxies around models, they also tend to accumulate logic that almost belongs in the model, but feels too closely tied to a specific screen in your app.
Views and templates. Ember views and templates work together. It's best to think of them as a GUI widget.
App.PostView = Ember.View.extend({
// This can be omitted when we're created by a route.
templateName: 'post'
// Any HTML event handlers would go here if we needed them. Our job is to
// map between HTML events and events understood by the controller.
//doubleClick: function (evt) {
// // We'll actually bind this to a specific button, not a click event.
// this.get("controller").send("showLowRatedComments");
//}
});
Our post template freely mixes fields defined by the model and fields defined by the controller:
<script type="text/x-handlebars" data-template-name="post">
<h2>{{title}}</h2>
<div class="body">{{body}}</div>
{{#if lowRatedCommentsShown}}
<button {{action 'hideLowRatedComments'}}>Hide Low-Rated Comments</button>
{{else}}
<button {{action 'showLowRatedComments'}}>Show Low-Rated Comments</button>
{{/if}}
{{partial "comments"}}
</script>
Note that as fields on our model or controller change, the view will automatically rerender just those portions of the HTML which need to be updated!
Because Ember.js runs in the browser, many operations are asynchronous. Much of Ember's fundamental design is based on making asynchronous updates pleasant and easy. One key piece consequence of this situation is that objects load asynchronously. When you call find
, you'll get back an unloaded object:
post = App.Post.find(params.post_id)
post.get("isLoaded"); // -> false
post.get("title"); // -> not yet available
When the server sends you the data, you'll see:
post.get("isLoaded"); // -> true
post.get("title"); // -> "Post #1"
To help make this painless, Ember relies heavily on computed properties, observers and bindings. In each of these cases, the key idea is that changes to data should automatically ripple through the system. For example, we can use a computed property to ensure that isLowRated
is updated any time that a comment's rating
changes:
App.Comment = DS.Model.extend({
post: DS.belongsTo("App.Post"),
body: DS.attr("string"),
rating: DS.attr("number"),
isLowRated: function () {
return this.get("rating") < 2;
}.property("rating") // A list of properties we depend on.
});
Note that Ember's Handlebars templates are deeply integrated with this system. When you write {{title}}
in a template, you establish a binding that will automatically update the DOM whenever title
changes. In the case of edit fields, this binding works in both directions! Changes to a displayed value will be pushed straight back to the model (though transactions may be used to roll it back).
It's also worth remembering that many of the dynamic updates—particularly bindings—run asynchronously at the end of the current "run loop". This is why you'll often see calls to Ember.run
in test suites:
Ember.run(function () {
// Change some bindings here. Not all changes will propagate immediately.
});
// Test that the values have all propagated here, after the run loop is done.
In practice, Ember feels much more asynchronous than Rails, but less asynchronous than an evented system like Node.js. This is because most asynchronous updates are managed automatically by bindings.
This is the one place I'm going to stray from strictly technical details and mention some practical advice. Ember Data provides DS.Model
, seen above. It's not the only model layer for Ember.js—check out ember-rest, ember-resource and similar libraries for alternatives. At this point in time, there's no official release of Ember Data, but it can be used very cautiously in production apps if you like to live on the bleeding edge. Some tips:
hasMany
relationship in Rails when serializing an object. See active_model_serializers if you're using Rails.It's possible to get very good results with Ember Data. But it's vastly less mature than ActiveModel, and it needs to be treated as such.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With