Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add a couple of new guides

  • Loading branch information...
commit 805075e47d3740131ea834c88c8ad9c5f636474e 1 parent d8bed58
@wycats wycats authored
View
5 data/guides.yml
@@ -2,5 +2,8 @@ ember_mvc:
name: "Ember MVC"
view_layer:
name: "Understanding the Ember.js view layer"
-
+outlets:
+ name: "Ember Application Structure"
+asynchrony:
+ name: "Asynchronous Behavior in Ember"
View
341 source/guides/asynchrony.md
@@ -0,0 +1,341 @@
+# Managing Asynchrony in Ember
+
+Many Ember concepts, like bindings and computed properties, are designed
+to help manage asynchronous behavior.
+
+## Without Ember
+
+We'll start by taking a look at ways to manage asynchronous behavior
+using jQuery or event-based MVC frameworks.
+
+Let's use the most common asynchronous behavior in a web application,
+making an Ajax request, as an example. The browser APIs for making Ajax
+request provide an asynchronous API. jQuery's wrapper does as well:
+
+```javascript
+jQuery.getJSON('/posts/1', function(post) {
+ $("#post").html("<h1>" + post.title + "</h1>" +
+ "<div>" + post.body + "</div>");
+});
+```
+
+In a raw jQuery application, you would use this callback to make
+whatever changes you needed to make to the DOM.
+
+When using an event-based MVC framework, you move the logic out of the
+callback and into model and view objects. This improves things, but
+doesn't get rid of the need to explicitly deal with asynchronous
+callbacks:
+
+```javascript
+Post = Model.extend({
+ author: function() {
+ return [this.salutation, this.name].join(' ')
+ },
+
+ toJSON: function() {
+ var json = Model.prototype.toJSON.call(this);
+ json.author = this.author();
+ return json;
+ }
+});
+
+PostView = View.extend({
+ init: function(model) {
+ model.bind('change', this.render, this);
+ },
+
+ template: _.template("<h1><%= title %></h1><h2><%= author %></h2><div><%= body %></div>"),
+
+ render: function() {
+ jQuery(this.element).html(this.template(this.model.toJSON());
+ return this;
+ }
+});
+
+var post = Post.create();
+var postView = PostView.create({ model: post });
+jQuery('#posts').append(postView.render().el);
+
+jQuery.getJSON('/posts/1', function(json) {
+ // set all of the JSON properties on the model
+ post.set(json);
+});
+```
+
+This example doesn't use any particular JavaScript library, but its
+approach is typical of event-driven MVC frameworks. It helps organize
+the asynchronous events, but asynchronous behavior is still the core
+programming model.
+
+## Ember's Approach
+
+In general, Ember's goal is to eliminate explicit forms of asynchronous
+behavior. As we'll see later, this gives Ember the ability to coalesce
+multiple events that have the same result.
+
+It also provides a higher level of abstraction, eliminating the need to
+manually register and unregister event listeners to perform most common
+tasks.
+
+You would normally use ember-data for this example, but let's see how
+you would model the above example using jQuery for Ajax in Ember.
+
+```javascript
+App.Post = Ember.Object.extend({
+
+});
+
+App.PostController = Ember.ObjectController.extend({
+ author: function() {
+ return [this.get('salutation'), this.get('name')].join(' ');
+ }.property('salutation', 'name')
+});
+
+App.PostView = Ember.View.extend({
+ // the controller is the initial context for the template
+ controller: null,
+ template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>")
+});
+
+var post = App.Post.create();
+var postController = App.PostController.create({ content: post });
+
+App.PostView.create({ controller: postController }).appendTo('body');
+
+jQuery.getJSON("/posts/1", function(json) {
+ post.setProperties(json);
+});
+```
+
+In contrast to the above examples, the Ember approach eliminates the
+need to explicitly register an observer when the `post`'s properties
+change.
+
+The `{{title}}`, `{{author}}` and `{{body}}` template elements are bound
+to those properties on the `PostController`. When the `PostController`'s
+content changes, it automatically propagates those changes to the DOM.
+
+Using a computed property for `author` eliminated the need to explicitly
+invoke the computation in a callback when the underlying property
+changed.
+
+Instead, Ember's binding system automatically follows the trail from the
+`salutation` and `name` set in the `getJSON` callback to the computed
+property in the `PostController` and all the way into the DOM.
+
+## Benefits
+
+Because Ember is usually responsible for propagating changes, it can
+guarantee that a single change is only propagated one time in response
+to each user event.
+
+Let's take another look at the `author` computed property.
+
+```javascript
+App.PostController = Ember.ObjectController.extend({
+ author: function() {
+ return [this.get('salutation'), this.get('name')].join(' ');
+ }.property('salutation', 'name')
+});
+```
+
+Because we have specified that it depends on both `salutation` and
+`name`, changes to either of those two dependencies will invalidate the
+property, which will trigger an update to the `{{author}}` property in
+the DOM.
+
+Imagine that in response to a user event, I do something like this:
+
+```javascript
+post.set('salutation', "Mrs.");
+post.set('name', "Katz");
+```
+
+You might imagine that these changes will cause the computed property to
+be invalidated twice, causing two updates to the DOM. And in fact, that
+is exactly what would happen when using an event-driven framework.
+
+In Ember, the computed property will only recompute once, and the DOM
+will only update once.
+
+How?
+
+When you make a change to a property in Ember, it does not immediately
+propagate that change. Instead, it invalidates any dependent properties
+immediately, but queues the actual change to happen later.
+
+Changing both the `salutation` and `name` properties invalidates the
+`author` property twice, but the queue is smart enough to coalesce those
+changes.
+
+Once all of the event handlers for the current user event have finished,
+Ember flushes the queue, propagating the changes downward. In this case,
+that means that the invalidated `author` property will invalidate the
+`{{author}}` in the DOM, which will make a single request to recompute
+the information and update itself once.
+
+**This mechanism is fundamental to Ember.** In Ember, you should always
+assume that the side-effects of a change you make will happen later. By
+making that assumption, you allow Ember to coalesce repetitions of the
+same side-effect into a single call.
+
+In general, the goal of evented systems is to decouple the data
+manipulation from the side effects produced by listeners, so you
+shouldn't assume synchronous side effects even in a more event-focused
+system. The fact that side effects don't propagate immediately in Ember
+eliminates the temptation to cheat and accidentally couple code together
+that should be separate.
+
+## Side-Effect Callbacks
+
+Since you can't rely on synchronous side-effects, you may be wondering
+how to make sure that certain actions happen at the right time.
+
+For example, imagine that you have a view that contains a button, and
+you want to use jQuery UI to style the button. Since a view's `append`
+method, like everything else in Ember, defers its side-effects, how can
+you execute the jQuery UI code at the right time.
+
+The answer is lifecycle callbacks.
+
+```javascript
+App.Button = Ember.View.extend({
+ tagName: 'button',
+ template: Ember.Handlebars.compile("{{view.title}}"),
+
+ didInsertElement: function() {
+ this.$().button();
+ }
+});
+
+var button = App.Button.create({
+ title: "Hi jQuery UI!
+}).appendTo('#something');
+```
+
+In this case, as soon as the button actually appears in the DOM, Ember
+will trigger the `didInsertElement` callback, and you can do whatever
+work you want.
+
+The lifecycle callbacks approach has several benefits, even if we didn't
+have to worry about deferred insertion.
+
+First, relying on synchronous insertion means leaving it up to the
+caller of `appendTo` to trigger any behavior that needs to run
+immediately after appending. As your application grows, you may find
+that you create the same view in many places, and now need to worry
+about that concern everywhere.
+
+The lifecycle callback eliminates the coupling between the code that
+instantiates the view and its post-append behavior. In general, we find
+that making it impossible to rely on synchronous side-effects leads to
+better design in general.
+
+Second, because everything about the lifecycle of a view is inside the
+view itself, it is very easy for Ember to re-render parts of the DOM
+on-demand.
+
+For example, if this button was inside of an `{{#if}}` block, and Ember
+needed to switch from the main branch to the `else` section, Ember can
+easily instantiate the view and call the lifecycle callbacks.
+
+Because Ember forces you to define a fully-defined view, it can take
+control of creating and inserting views in appropriate situations.
+
+This also means that all of the code for working with the DOM is in a
+few sanctioned parts of your application, so Ember has more freedom in
+the parts of the render process outside of these callbacks.
+
+## Observers
+
+In some rare cases, you will want to perform certain behavior after a
+property's changes have propagated. As in the previous section, Ember
+provides a mechanism to hook into the property change notifications.
+
+Let's go back to our salutation example.
+
+```javascript
+App.PostController = Ember.ObjectController.extend({
+ author: function() {
+ return [this.get('salutation'), this.get('name')].join(' ');
+ }.property('salutation', 'name')
+});
+```
+
+If we want to be notified when the author changes, we can register an
+observer. Let's say that the view object wants to be notified:
+
+```javascript
+App.PostView = Ember.View.extend({
+ controller: null,
+ template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
+
+ authorDidChange: function() {
+ alert("New author name: " + this.getPath('controller.author'));
+ }.observes('controller.author')
+});
+```
+
+Ember triggers observers after it successfully propagates the change. In
+this case, that means that Ember will only call the `authorDidChange`
+callback once in response to each user event, even if both of `salutation`
+and `name` changed.
+
+This gives you the benefits of executing code after the property has
+changed, without forcing all property changes to be synchronous. This
+basically means that if you need to do some manual work in response to a
+change in a computed property, you get the same coalescing benefits as
+Ember's binding system.
+
+Finally, you can also register observers manually, outside of an object
+definition:
+
+```javascript
+App.PostView = Ember.View.extend({
+ controller: null,
+ template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
+
+ didInsertElement: function() {
+ this.addObserver('controller.name', function() {
+ alert("New author name: " + this.getPath('controller.author'));
+ });
+ }
+});
+```
+
+However, when you use the object definition syntax, Ember will
+automatically tear down the observers when the object is destroyed. For
+example, if an `{{#if}}` statement changes from truthy to falsy, Ember
+destroys all of the views defined inside the block. As part of that
+process, Ember also disconnects all bindings and inline observers.
+
+If you define an observer manually, you need to make sure you remove it.
+In general, you will want to remove observers in the opposite callback
+to when you created it. In this case, you will want to remove the
+callback in `willDestroyElement`.
+
+```javascript
+App.PostView = Ember.View.extend({
+ controller: null,
+ template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
+
+ didInsertElement: function() {
+ this.addObserver('controller.name', function() {
+ alert("New author name: " + this.getPath('controller.author'));
+ });
+ },
+
+ willDestroyElement: function() {
+ this.removeObserver('controller.name');
+ }
+});
+```
+
+If you added the observer in the `init` method, you would want to tear
+it down in the `willDestroy` callback.
+
+In general, you will very rarely want to register a manual observer in
+this way. Because of the memory management guarantees, we strongly
+recommend that you define your observers as part of the object
+definition if possible.
View
409 source/guides/outlets.md
@@ -0,0 +1,409 @@
+# Ember Application Structure
+
+On a high-level, you structure an Ember application by designing a series of nested routes that correspond to nested application state. This guide will first cover the high-level concepts, and then walk you through an example.
+
+## Routing
+
+A user navigates through your application by making choices about what
+to view. For example, if you had a blog, your user might first choose
+between your Posts and your About page. In general, you want to have a
+default for this first choice (in this case, probably Posts).
+
+Once the user has made their first choice, they're usually not done. In
+the context of Posts, the user will eventually view an individual post
+and its comments. Inside of an individual post, they can choose between
+viewing a list of comments and a list of trackbacks.
+
+Importantly, in all of these cases, the user is choosing what to display
+on the page. As you descend deeper into your application state, those
+choices affect smaller areas of the page.
+
+In the next section, we'll cover how you control these areas of the
+page. For now, let's look at how to structure your templates.
+
+When the user first enters the application, the application is on the
+screen, and it has an empty outlet that the router will control. In
+Ember, an `outlet` is an area of a template that has its child template
+determined at runtime based on user interaction.
+
+<figure>
+ <img src="/images/outlet-guide/application-choice.png">
+</figure>
+
+The template for the Application (`application.handlebars`) will look
+something like this:
+
+```
+<h1>My Application</h1>
+
+{{outlet}}
+```
+
+By default, the router will initially enter the _list of posts_ state,
+and fill in the outlet with `posts.handlebars`. We will see later how
+this works exactly.
+
+<figure>
+ <img src="/images/outlet-guide/list-of-posts.png">
+</figure>
+
+As expected, the _list of posts_ template will render a list of posts.
+Clicking on the link for an individual post will replace the contents of
+the application's outlet with the template for an individual post.
+
+The template will look like this:
+
+```
+{{#each post in controller}}
+<h1><a {{action showPost context="post" href=true}}>{{post.title}}</a></h1>
+<div>{{post.intro}}</div>
+{{/post}}
+```
+
+When clicking on a link for an individual post, the application will
+move into the _individual post_ state, and replace `posts.handlebars` in
+the application's outlet with `post.handlebars`.
+
+<figure>
+ <img src="/images/outlet-guide/individual-post.png">
+</figure>
+
+In this case, the individual post also has an outlet. In this case, the
+outlet will allow the user to choose between viewing comments or
+trackbacks.
+
+The template for an individual post looks like this:
+
+```
+<h1>{{title}}</h1>
+
+<div class="body">
+ {{body}}
+</div>
+
+{{outlet}}
+```
+
+Again, the `{{outlet}}` simply specifies that the router will make the
+decision about what to put in that area of the template.
+
+Because `{{outlet}}` is a feature of all templates, as you go deeper
+into the route hierarchy, each route will naturally control a smaller
+part of the page.
+
+## How it Works
+
+Now that you understand the basic theory, let's take a look at how the
+router controls your outlets.
+
+### Templates, Controllers, and Views
+
+First, for every high-level handlebars template, you will also have a
+view and a controller with the same name. For example:
+
+* `application.handlebars`: the template for the main application view
+* `App.ApplicationController`: the controller for the template. The
+ initial variable context of `application.handlebars` is an instance of
+ this controller.
+* `App.ApplicationView`: the view object for the template.
+
+In general, you will use view objects to handle events and controller
+objects to provide data to your templates.
+
+Ember provides two primary kinds of controllers, `ObjectController` and
+`ArrayController`. These controllers serve as proxies for model objects
+and lists of model objects.
+
+We start with controllers rather than exposing the model objects
+directly to your templates so that you have someplace to put
+view-related computed properties and don't end up polluting your models
+with view concerns.
+
+You also connect `{{outlet}}`s using the template's associated
+controller.
+
+### The Router
+
+Your application's router is responsible for moving your application
+through its states in response to user action.
+
+Let's start with a simple router:
+
+```javascript
+App.Router = Ember.Router.extend({
+ root: Ember.State.extend({
+ index: Ember.State.extend({
+ route: '/',
+ redirectsTo: 'posts'
+ }),
+
+ posts: Ember.State.extend({
+ route: '/posts'
+ }),
+
+ post: Ember.State.extend({
+ route: '/posts/:post_id'
+ })
+ })
+});
+```
+
+This router sets up three top-level states: an index state, a state that
+shows a list of posts, and a state that shows an individual post.
+
+In our case, we'll simply redirect the index route to the `posts` state.
+In other applications, you may want to have a dedicated home page.
+
+So far, we have a list of states, and our app will dutifully enter the
+`posts` state, but it doesn't do anything. When the application enters
+the `posts` state, we want it to connect the `{{outlet}}` in the
+application template. We accomplish this using the `connectOutlets`
+callback.
+
+```javascript
+App.Router = Ember.Router.extend({
+ root: Ember.State.extend({
+ index: Ember.State.extend({
+ route: '/',
+ redirectsTo: 'posts'
+ }),
+
+ posts: Ember.State.extend({
+ route: '/posts',
+
+ connectOutlets: function(router) {
+ router.get('applicationController').connectOutlet(App.PostsView, App.Post.find());
+ }
+ }),
+
+ post: Ember.State.extend({
+ route: '/posts/:post_id'
+ })
+ })
+});
+```
+
+This connectOutlet call does a few things for us:
+
+* It creates a new instance of `App.PostsView`, using the
+ `posts.handlebars` template.
+* It sets the `content` property of `postsController` to a list of all
+ of the available posts (`App.Post.find()`) and makes `postController`
+ the controller for the new `App.PostsView`.
+* It connects the new view to the outlet in `application.handlebars`.
+
+In general, you should just think of these objects as operating in
+tandem. You will always provide the content for a view's controller when
+you create a view.
+
+## Transitions and URLs
+
+Next, we will want to provide a way for an application in the `posts`
+state to move into the `post` state. We accomplish this by specifying a
+transition.
+
+```javascript
+posts: Ember.State.extend({
+ route: '/posts',
+ showPost: Ember.State.transitionTo('post'),
+
+ connectOutlets: function(router) {
+ router.get('applicationController').connectOutlet(App.PostsView, App.Post.find());
+ }
+})
+```
+
+You invoke this transition by using the `{{action}}` helper in the
+current template.
+
+```
+{{#each post in controller}}
+ <h1><a {{action showPost context="post" href=true}}>{{post.title}}</a></h1>
+{{/each}}
+```
+
+When a user clicks on a link with an `{{action}}` helper, Ember will
+dispatch an event to the current state with the specified name. In this
+case, the event is a transition.
+
+Because we used a transition, Ember was also able to generate a URL for
+this link. Ember uses the `id` property of the context to fill in the
+`:post_id` dynamic segment of the `post` state.
+
+Next, we will need to implement `connectOutlets` on the `post` state.
+This time, the `connectOutlets` method will receive the post object
+specified as the context to the `{{action}}` helper.
+
+```javascript
+post: Ember.State.extend({
+ route: '/posts/:post_id',
+
+ connectOutlets: function(router, post) {
+ router.get('applicationController').connectOutlet(App.PostView, post);
+ }
+})
+```
+
+To recap, the `connectOutlet` call performs a number of steps:
+
+* It creates a new instance of `App.PostView`, using the
+ `post.handlebars` template.
+* It sets the `content` property of `postController` to the post that
+ the user clicked on.
+* It connects the new view to the outlet in `application.handlebars`.
+
+You don't have to do anything else to get the link (`/posts/1`) to work
+if the user saves it as a bookmark and comes back to it later.
+
+If the user enters the page for the first time with the URL `/posts/1`,
+the router will perform a few steps:
+
+* Figure out what state the URL corresponds with (in this case, `post`)
+* Extract the dynamic segment (in this case `:post_id`) from the URL and
+ call `App.Post.find(post_id)`. This works using a naming convention:
+ the `:post_id` dynamic segment corresponds to `App.Post`.
+* Call `connectOutlets` with the return value of `App.Post.find`.
+
+This means that regardless of whether the user enters the `post` state
+from another part of the page or through a URL, the router will invoke
+the `connectOutlets` method with the same object.
+
+## Nesting
+
+Finally, let's implement the comments and trackbacks functionality.
+
+Because the `post` state uses the same pattern as the `root` state, it
+will look very similar.
+
+```javascript
+post: Ember.State.extend({
+ route: '/posts/:post_id',
+
+ connectOutlets: function(router, post) {
+ router.get('applicationController').connectOutlet(App.PostView, post);
+ },
+
+ index: Ember.State.extend({
+ route: '/',
+ redirectsTo: 'comments'
+ }),
+
+ comments: Ember.State.extend({
+ route: '/comments',
+ showTrackbacks: Ember.State.transitionTo('trackbacks'),
+
+ connectOutlets: function(router) {
+ var postController = router.get('postController');
+ postController.connectOutlet(App.CommentsView, postController.get('comments'));
+ }
+ }),
+
+ trackbacks: Ember.State.extend({
+ route: '/trackbacks',
+ showComments: Ember.State.transitionTo('comments'),
+
+ connectOutlets: function(router) {
+ var postController = router.get('postController');
+ postController.connectOutlet(App.TrackbacksView, postController.get('trackbacks'));
+ }
+ })
+})
+```
+
+There are only a few changes here:
+
+* We specify the `showTrackbacks` and `showComments` transitions only in
+ the states where transitioning makes sense.
+* Since we are setting the view for the outlet in `post.handlebars`, we
+ call `connectOutlet` on `postController`
+* In this case, we get the content for the `commentsController` and
+ `trackbacksController` from the current post. The `postController` is
+ a proxy for the underlying Post, so we can retrieve the associations
+ directly from the `postController`.
+
+Here's the template for an individual post.
+
+```
+<h1>{{title}}</h1>
+
+<div class="body">
+ {{body}}
+</div>
+
+<p>
+ <a {{action showComments href=true}}>Comments</a> |
+ <a {{action showTrackbacks href=true}}>Trackbacks</a>
+</p>
+
+{{outlet}}
+```
+
+And finally, coming back from a bookmarked link will work fine with this
+nested setup. Let's take a look at what happens when the user enters the
+site at `/posts/1/trackbacks`.
+
+* The router determines what state the URL corresponds with
+ (`post.trackbacks`), and enters the state.
+* For each state along the way, the router extracts any dynamic segments
+ and calls `connectOutlets`. This mirrors the path a user would take as
+ they move through the application. As before, the router will call the
+ `connectOutlet` method on the post with `App.Post.find(1)`.
+* When the router gets to the trackbacks state, it will invoke
+ `connectOutlets`. Because the `connectOutlets` method for `post` has
+ set the `content` of the `postController`, the trackbacks state will
+ retrieve the association.
+
+Again, because of the way the `connectOutlets` callback works with
+dynamic URL segments, the URL generated by an `{{action}}` helper is
+guaranteed to work later.
+
+## Asynchrony
+
+One final point: you might be asking yourself how this system can work
+if the app has not yet loaded Post 1 by the time `App.Post.find(1)` is
+called.
+
+The reason this works is that `ember-data` always returns an object
+immediately, even if it needs to kick off a query. That object starts
+off with an empty `data` hash. When the server returns the data,
+ember-data updates the object's `data`, which also triggers bindings on
+all defined attributes (properties defined using `DS.attr`).
+
+When you ask this object for its `trackbacks`, it will likewise return
+an empty `ManyArray`. When the server returns the associated content
+along with the post, ember-data will also automatically update the
+`trackbacks` array.
+
+In your `trackbacks.handlebars` template, you will have done something
+like:
+
+```
+<ul>
+{{#each trackback in controller}}
+ <li><a {{bindAttr href="trackback.url"}}>{{trackback.title}}</a></li>
+{{/each}}
+</ul>
+```
+
+When ember-data updates the `trackbacks` array, the change will
+propagate through the `trackbacksController` and into the DOM.
+
+You may also want to avoid showing partial data that is not yet loaded.
+In that case, you could do something like:
+
+```
+<ul>
+{{#if controller.isLoaded}}
+ {{#each trackback in controller}}
+ <li><a {{bindAttr href="trackback.url"}}>{{trackback.title}}</a></li>
+ {{/each}}
+{{else}}
+ <li><img src="/spinner.gif"> Loading trackbacks...</li>
+{{/if}}
+</ul>
+```
+
+When ember-data populates the `ManyArray` for the trackbacks from the
+server-provided data, it also sets the `isLoaded` property. Because all
+template constructs, including `#if` automatically update the DOM if the
+underlying property changes, this will "just work".
View
BIN  source/images/outlet-guide/application-choice.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  source/images/outlet-guide/individual-post.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  source/images/outlet-guide/list-of-posts.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Please sign in to comment.
Something went wrong with that request. Please try again.