Created
November 27, 2012 14:18
-
-
Save trek/4154434 to your computer and use it in GitHub Desktop.
original notes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
https://twitter.com/dagda1/status/231016636847648769 | |
https://twitter.com/garybernhardt/status/227881347346219008/ | |
http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/ | |
backbone: what you'd normally use jquery plugins for, but with an inverted rendering process (js -> DOM instead of DOM -> js) | |
"islands of richness", jquery data callbacks main pattern. | |
other frameworks are building out from jQuery's central pattern of DOM selection and event callbacks. | |
fantastic pattern for adding behavior to documents, but we aren't writing documents. | |
Ember Apps | |
compared to old dev model | |
page rendered by server | |
javascript "wakes up" with list | |
javascript goes through list adding behavior | |
error prone on long-running pages | |
inefficient: two super computers | |
link to DHH. Link to jsperf | |
long running apps involve problem most web developers | |
don't typically have experience with: mostly related to | |
state. | |
We need to borrow patterns from other thick client development: | |
phone, desktop, tablet. | |
we need to connect state to view hierarchy and data availability | |
Ember composed (composite) templates: describe a general view | |
hierarchy in a single location, backing portions of it with | |
objects (decedents of Ember.View) which encapsulate user | |
events. | |
{{outlet}} lets you fill specific portions of that composite view | |
with content based on the state. | |
State is determined by user interaction. Think of certain user interaction | |
as their way of communicating their desire to trigger a state change. | |
Application state can be serialized and deserialized. On the web | |
this isn't yet as robust as in thick client apps, but we do have | |
the unique benefit of sharing state: we can pass state in the url | |
and return later on another device or even pass state to another | |
user. | |
In Ember, this is accomplished by the router, which is a descendant | |
of the general state manager. | |
In the first part of this document we'll explore an existing application | |
become comfortable with the general concept of view hierarchy, how it | |
relates to applications, and how a user can trigger state changes. | |
Examining Rdio | |
rdio is not written in Ember, it's written in Backbone | |
irrelevant, Ember is designed to help you build this | |
thick client style of application and we can describe | |
any application like this using Ember. | |
I don't work for Rdio and am no way afflieted except as a user | |
who listens to lots of lame music. | |
You can play with Rdio right now yourself. I don't need to descrbie hypothetical | |
behavior of a dummy app to you. Rdio is built and explorable (for free, even) | |
We won't be exploring or coding all of Rdio, but after a few examples I hope | |
it "clicks" for you and you'll start seeing all modern web applications | |
in Ember terms. | |
The main composite template for Rdio consists of four sections: | |
* the top bar which contains the logo, search, and user-related | |
top level navigation | |
* the side bar, which contains elements for navigation between | |
collections | |
* the main view, where contain is frequently redrawn based on | |
state changes | |
* the playback area where the current song is controlled | |
We might express them in straight HTML as follows: | |
<div class='header'></div> | |
<div class='navigation'></div> | |
<div class='content'></div> | |
<div class='playback'></div> | |
For now, let's examine and flesh out the Rdio's content navigation and | |
main content area: | |
<div class='navigation'></div> | |
It contains a few lists. For now, I'll add HTML for the Browse and Your Music sections: | |
<div class='navigation'> | |
<h3>Browse</h3> | |
<ul> | |
<li>Heavy Rotation</li> | |
<li>Recent Activity</li> | |
<li>Top Charts</li> | |
<li>New Releases</li> | |
</ul> | |
<h3>Your Music</h3> | |
<ul> | |
<li>Collection</li> | |
<li>History</li> | |
<li>Queue</li> | |
</ul> | |
</div> | |
[1] | |
Next, let's tackle a static mockup of the the main content area. | |
<div class='content'></div> | |
The default content that gets loaded in Rdio is a "Heavy Rotation" | |
album list. I'll provide data for a few albums: | |
<div class='content'> | |
<div class='heavy-rotation'> | |
<h2>Heavy Rotation</h2> | |
<div class='albums'> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-artist'>Gossamer</div> | |
<div class='album-title'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
<!-- many more albums --> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-artist'>Gossamer</div> | |
<div class='album-title'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
</div><!-- end albums --> | |
</div><!-- end heavy-rotation --> | |
</div><!-- end content --> | |
From a user's perspective Clicking "Top Charts" in the content navigation changes | |
the content displayed from "Heavy Rotation" to "Top Charts". It's display is very similar. | |
I've given it's containing div a new class: | |
<div class='content'> | |
<div class='top-charts'> | |
<h2>Top Charts</h2> | |
<div class='albums'> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
<!-- many more albums --> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
</div><!-- end albums --> | |
</div><!-- end top-charts --> | |
</div><!-- end content --> | |
#Connect this design stub to a new application: | |
* you might want to look at a development harness like yeoman.io, brunch.io, iridium, etc. | |
not specific to ember, but a general tool you'll want for any browser-app dev | |
Rdio = Ember.Application.create(); | |
creates a new application. This acts as a namespace for your objects so you don't | |
pollute window. It also acts as a central DOM event coordinator. | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({}) | |
}) | |
Rdio.initialize() | |
Router is a SM. | |
initialize() | |
extends the Router class into app's `router` property | |
creates 'shared instances' of every /Controller/ property on your app | |
and stores them on the router | |
immediately traditions the router into its root state | |
begins url detection attempting to find a matching state | |
appends an instance of ApplicationView to the body and associates it | |
with the instance of ApplicationController stored on the router in | |
applicationController. | |
crack open console, you'll see a warning that you don't have either of these. | |
We've endeavored to guide. PR if message isn't clear or you get errors. | |
Transform our mockup into a handlars template called application. | |
create an application view and controller. (these are not like Rails controllers): | |
Rdio.ApplicationController = Ember.Controller.extend(); | |
Rdio.ApplicationView = Ember.View.extend({ | |
templateName: 'application' | |
}) | |
Reload the page and you should see our mockup loaded. | |
Time to transition between states! | |
<div class='header'></div> | |
<div class='navigation'> | |
... | |
</div> | |
<div class='content'> | |
{{outlet}} | |
</div> | |
<div class='playback'></div> | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}) | |
}) | |
}) | |
// tell the router where to start. | |
Rdio.router.transitionTo('heavyRotation'); | |
give the page a reload and you'll see console warnings that we don't have | |
heavyRotation. | |
Rdio.HeavyRotationController = Ember.Controller.extend({}) | |
Rdio.HeavyRotationView = Ember.View.extend({ | |
templateName: 'heavyRotation' | |
}) | |
reload app and 'Unable to find template "heavyRotation".' | |
move the heavy rotation to the tempalte and reload. | |
Transitioning between states: | |
<div class='navigation'> | |
<h3>Browse</h3> | |
<ul> | |
<li>Heavy Rotation</li> | |
<li>Recent Activity</li> | |
<li {{action showTopCharts}}>Top Charts</li> | |
<li>New Releases</li> | |
</ul> | |
<h3>Your Music</h3> | |
<ul> | |
<li>Collection</li> | |
<li>History</li> | |
<li>Queue</li> | |
</ul> | |
</div> | |
reload, give it a click. | |
'could not respond to event showTopCharts in state root.' | |
Our router doesn't know showTopCharts. Router is the | |
most typical target for {{action}}s. | |
add it: | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}) | |
}) | |
}) | |
reload, new error: 'Could not find state for path: "topCharts" ' | |
add the state: | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}) | |
}) | |
}) | |
reload app, try again. New error: 'The name you supplied topCharts did not resolve to a view TopChartsView' | |
Add a TopChartsView and TopChartsController: | |
Rdio.TopChartsView = Ember.View.extend({ | |
template: 'topCharts' | |
}) | |
Rdio.TopChartsController = Ember.Controller.extend(); | |
move topCharts html into the template, reload and give that li a click. | |
## Navigating back to heavyRotation | |
add this to the nav | |
reload the app, navigation to the 'topCharts' state and click 'Heavy Rotation' | |
<li {{action showHeavyRotation}}>Heavy Rotation</li> | |
new error: 'could not respond to event showHeavyRotation in state root.heavyRotation.' | |
now add the action to the topCharts state: | |
topCharts: Ember.Route.extend({ | |
showHeavyRotation: Ember.Route.transitionTo('heavyRotation') | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('topCharts') | |
} | |
}) | |
reload the app. | |
now you should be able to shift back and forth between these two application states | |
updating the view hierarchy appropriately. | |
If you're coming from a background of jquery, you probably think it terms of | |
DOM manipulation in response to user interaction. Get out of that habit. | |
Think and communicate in terms of state manipulation. So, it's not | |
"when the user clicks Top charts anchor we replace the content's innerHTML with the | |
top charts template" its "when the user invokes the 'show top charts' action we | |
change state to 'topCharts' | |
### Connecting these states to dummy data | |
Switch HeavyRotationController from a Ember.Controller to an Ember.ArrayController. | |
Controller knows how to be the target of an action (it just send on to router) | |
Diff between Array and ArrayController? Separation of concerns. Array is a core | |
type and can answer questions like "what are my contents", "what is their current order" | |
and perform actions related to adding and removing. ArrayController acts as a type of | |
proxy and can answer questions like "what is the current sort order", "what is the | |
currently selected item?" and can perform actions like "add this item preserving | |
the collection's ordering" | |
A good way to think of this is whether a property universally applies to all all | |
experiences or only to current experience. When we load an album from Rdio, the | |
contents of the album's tracks and their implicit order are identical for everyone. | |
I may wish, when interacting with the album, to sort its songs alphabetically so I | |
can more easily find the track I want. This sorting is personal to me and my current | |
interaction experience and doesn't affect the underlying data and certainly shouldn't | |
affect the sort order when *you* load the album on your computer. | |
Rdio.HeavyRotationController = Ember.ArrayController.extend({ | |
content: [ | |
Ember.Object.create({}), | |
/* ... more objects ... */ | |
Ember.Object.create({}), | |
] | |
}) | |
Change the HeavyRotationView's template to loop through actual data: | |
<div class='albums'> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
<!-- many more albums --> | |
</div><!-- end albums --> | |
to | |
<div class='albums'> | |
{{#each album in controller}} | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
{{/each}} | |
</div> | |
Reloading will show the same static template repeated for each item you | |
added as the content property of HeavyRotationController. | |
{{each is handlebars}} http://handlebarsjs.com/ | |
Ember adds some helpers to handlebars. Ember's each replaces the base handlebars | |
each and makes it tie into the Ember view system. | |
the inner block of the each helper will become the a template for the view. It will | |
have access to the album keyword to refer to the particular item as we loop. | |
We can begin to replace the static template with handlebars calls to properties on the album: | |
{{#each album in controller}} | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img {{bindAttr src="album.coverImageUrl"}}> | |
</div> | |
<div class='album-info'> | |
<div class='album-artist'>{{album.artist.name}}</div> | |
<div class='album-title'>{{album.name}}</div> | |
<div class='album-songs-count'>{{album.tracks.length}} songs</div> | |
<div class='user-avatar-icon'><img {{bindAttr src="album.avatarImageUrl"}} /></div> | |
</div> | |
</div> | |
{{/each}} | |
http://www.emberist.com/2012/04/06/bind-and-bindattr.html' | |
If you reload, you'll see that now we have no data. Inspect source with WK inspector | |
and you'll see that Ember wraps sections where dynamic content will be rendered in script | |
tags: | |
Ember has set up observers for you so that if these data change the view will automatically | |
track those changes. Perhaps most importantly, it will also tear down these observations | |
appropriately so you don't leak memory or have leave unwanted events laying around. | |
Using another framework and haven't even done this clean up yourself? You might be | |
writing buggy, leaky apps. It's why their code is so small and their demos | |
are misleadingly simple: | |
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ | |
"Apps that tend to throw away their views and models at the same time don't ever run into this issue." | |
But the class of apps Ember is targeting tends to be long-running. It's not atypical for a user to | |
have the rdio app open for hours. I hope one day I, too, write apps this useful. | |
If you go looking through Rdio's code, you'll see they are diligent about cleaning up after themselves: | |
destroy: function () { | |
var c = this; | |
this.unbind(); | |
try { | |
this._element.pause(), | |
this._element.removeEventListener("error", this._triggerError), | |
this._element.removeEventListener("ended", this._triggerEnd), | |
this._element.removeEventListener("canplay", this._triggerReady), | |
this._element.removeEventListener("loadedmetadata", this._onLoadedMetadata), | |
_.each(b, function (a) { | |
c._element.removeEventListener(a, c._bubbleProfilingEvent) | |
}), | |
_.each(a, function (a) { | |
c._element.removeEventListener(a, c._logEvent) | |
}), | |
this._element = null, | |
this.trigger("destroy") | |
} catch (d) {} | |
}, | |
Backbone's documentation doesn't mention it (and I've never seen a tutorial that includes it) because | |
https://github.com/documentcloud/backbone/issues/231 | |
Go ahead supply some dummy attributes to your Ember.Objects inside content | |
## Let's apply the work to the TopCharts template and controller: | |
adding fake data and changing to ArrayController | |
Rdio.TopChartsController = Ember.ArrayController.extend({ | |
content: [ | |
... | |
] | |
}) | |
.... | |
Later, we'll connect this part of the application to live data. | |
Replace the view with hbars. | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
<!-- many more albums --> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
You'll notice we now have templates with lots of duplication. This means for now we can | |
probably make the template for both TopChartsView and HeavyRotationView makes calls to the view helper | |
and render a view | |
{{view Rdio.AlbumItemView albumBinding="album"}} | |
reload: Unable to find view at path 'Rdio.AlbumItemView' | |
Add the view and template: | |
Rdio.AlbumItemView = Ember.View.extend({ | |
templateName: 'albumItem' | |
}) | |
This view isn't back by a controller, so we don't need to create an AlbumItemController class. | |
### Going Deeper Down a State Path | |
So far we've talked only about siblings states, now child states: | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img {{bindAttr src="album.coverImageUrl"}}> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>{{album.name}}</div> | |
<div class='album-artist' {{action showArtist}}>{{album.artist.name}}</div> | |
<div class='album-songs-count'>{{album.tracks.length}} songs</div> | |
<div class='user-avatar-icon'><img {{bindAttr src="album.avatarImageUrl"}} /></div> | |
</div> | |
</div> | |
[2] | |
Click on the artist name, you get a complaint about the router not know what showArtist is. | |
Let's add it in heavyRotation state so we can transition to it: | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
showArtist: Ember.Route.transitionTo('artist'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}), | |
topCharts: Ember.Route.extend({ | |
showHeavyRotation: Ember.Route.transitionTo('heavyRotation'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('topCharts') | |
} | |
}) | |
}) | |
}) | |
Reload the app, click the link again, and you'll be informed there is not state 'artist'. | |
Let's add it | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
showArtist: Ember.Route.transitionTo('artist'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}), | |
topCharts: Ember.Route.extend({ | |
showHeavyRotation: Ember.Route.transitionTo('heavyRotation'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('topCharts') | |
} | |
}), | |
artist: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('artist') | |
} | |
}) | |
}) | |
}) | |
Reload, click again, and you'll be warned that you're missing an ArtistView and ArtistController. | |
Let's add these. | |
Rdio.ArtistView = Ember.View.extend({ | |
templateName: 'artist' | |
}) | |
Rdio.ArtistController = Ember.Controller.extend(); | |
Reload, this time you'll be warned we cannot find a template. Let's add one, just using static | |
HTML: | |
<div class='play-button'></div> | |
<div class='name'>Metric</div> | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
<div class='biography-teaser'> | |
Metric are a band with an eclectic, adventurous outlook, whose music encompasses | |
elements of synth pop, new wave, dance-rock, and electronica and whose hometown | |
has vacillated between Toronto, Montreal, New York, Los Angeles, and London over | |
the course of the group's existence. Metric's story began in | |
<a>More...</a> | |
</div> | |
<div class='top-albums'> | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Syntetica</div> | |
<div class='album-artist'>Metric</div> | |
<div class='album-songs-count'>11 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
<!-- many more --> | |
<div class='album-info'> | |
<div class='album-title'>Cradle Song</div> | |
<div class='album-artist'>Metric</div> | |
<div class='album-songs-count'>6 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
... | |
Reload the applicaiton we can now naviagte to state where an artist is showing. | |
At this point, evert artist will display the same static data, so we'll replace | |
that handlebars calls | |
<div class='play-button'></div> | |
<div class='name'>{{name}}</div> | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
<div class='biography-teaser'> | |
{{biographyTeaser}} | |
<a>More...</a> | |
</div> | |
<div class='top-albums'> | |
{{#each album in topAlbums}} | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img {{bindAttr src="coverImageUrl"}} /> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>{{album.title}}</div> | |
<div class='album-artist'>{{name}}</div> | |
<div class='album-songs-count'>{{album.tracks.length}} songs</div> | |
<div class='user-avatar-icon'><img {{bindAttr src="avatarImageUrl"}}/></div> | |
</div> | |
{{/each}} | |
</div> | |
Reload the app and attempt to navigate. You should see a template with lots of data | |
missing. We're missing the notion of *which* album we wanted to see. | |
Change the {{action}} to include a context: | |
{{action showArtist}} | |
becomes | |
{{action showArtist album.artist}} | |
artist refers to the particular item in the loop in the template. | |
This album will be passed through the router's transition as the context | |
and ends up on the connectOutlets call as the second argument. | |
change | |
artist: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('artist') | |
} | |
}) | |
to | |
artist: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlets: function(router, context){ | |
router.get('applicationController').connectOutlet('artist', context) | |
} | |
}) | |
SB: hasContext is a funky side effect of Routes most typical use case being | |
used with urls patterns – a topic we'll cover later. Usually the router | |
will know a route hasContext by the format of its url pattern. | |
In addition to changing the view hierachy at {{outlet}} to the album view | |
it will set the view's controller's content property to the specific | |
context we're talking about. Navigate back to this state | |
Still nothing. | |
Ember.Controller doesn't do anything special with a content proeperty, despite | |
it being assinged. | |
Change to Ember.ObjectController. | |
Rdio.ArtistController = Ember.Controller.extend(); | |
Rdio.ArtistController = Ember.ObjectController.extend(); | |
ObjectController proxies | |
{{name}} -> controller.name -> controller.content.name -> 'Metric' | |
allows us to keep the # of bindings created/destoyed to a minimum, allows us to transform raw data | |
into displayabe formats. e.g. we might calculate total play time of an album as a funciton that adds | |
the track times together if Rdio doesn't provide this data to us directly. | |
Spend some time filling out some of your fake objects with these addtional properties so your views | |
get filled with the approprite content. | |
##Linking to specific albums | |
From here we allow the user to navigate to specific alubms for this artist by using actions: | |
Here I've added `{{action showAlbum album}}` inside the loop for both the div that contains | |
the cover image and for the album's name. | |
<div class='top-albums'> | |
{{#each album in topAlbums}} | |
<div class='albums-album'> | |
<div class='album-cover' {{action showAlbum album}}> | |
<img {{bindAttr src="coverImageUrl"}} /> | |
</div> | |
<div class='album-info'> | |
<div class='album-title' {{action showAlbum album}}>{{album.title}}</div> | |
<div class='album-artist'>{{name}}</div> | |
<div class='album-songs-count'>{{album.tracks.length}} songs</div> | |
<div class='user-avatar-icon'><img {{bindAttr src="avatarImageUrl"}}/></div> | |
</div> | |
{{/each}} | |
</div> | |
Reload the app and navigate back to this state, and click on album cover or album name. Console | |
should warn you that the router doesn't know how to respond to showAlbum. Let's add that | |
transition and new state to our existing artist state. | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
showArtist: Ember.Route.transitionTo('artist'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}), | |
topCharts: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('topCharts') | |
} | |
}), | |
artist: Ember.Route.extend({ | |
hasContext: true, | |
showAlbum: Ember.Route.transitionTo('album'), | |
album: Ember.Route.extend({ | |
}), | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
} | |
}) | |
}) | |
}) | |
This introduces a slightly wrinkle in our state chart. Now that we've | |
added a substate to the `artist` state, when transitioning from | |
the heavyRotation state to the artist state via the showArtist action we're | |
stopping our transitioning on an intermediate state: | |
showArtist: Ember.Route.transitionTo('artist') | |
A state machine cannot stop its transition process on a state that has substates. | |
Think of state machines like a humorous flow chart: | |
http://xkcd.com/518/ | |
If there are possible connecting lines at a decision point you must continue | |
moving along one of them. Now that the `artist` state has states below it, | |
we can't stop transitioning here. To resovle this, we have to add a new | |
substate to represent "Seeing a summary of an artist" and add it as a substate | |
of 'artist': | |
artist: Ember.Route.extend({ | |
showAlbum: Ember.Route.transitionTo('album'), | |
album: Ember.Route.extend({ | |
}), | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
} | |
}) | |
becomes | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
showAlbum: Ember.Route.transitionTo('album'), | |
connectOutlets: function(router, context){ | |
router.get('applicationController').connectOutlet('artist', context) | |
} | |
}), | |
album: Ember.Route.extend({ | |
}) | |
}) | |
and we update the transition action in topCharts to reflect the transition to | |
the new substate: | |
showArtist: Ember.Route.transitionTo('artist') | |
becomes | |
showArtist: Ember.Route.transitionTo('artist.summary'), | |
Now we can flesh out the artist.album route: | |
album: Ember.Route.extend({ | |
connectOutlets: function(router, context){ | |
router.get('applicationController').connectOutlet('album', context); | |
} | |
}) | |
Reload the app and navigate to this state again. You'll see warnins about missing | |
AlbumView and AlbumController. Let's takes several steps at once and add | |
The view, the controller, and a static template. If you're not feeling comfortable | |
with Router just yet, feel free to makes these steps one at a time and verify | |
they worked by reloading the app and navigaing to the this state as we've been | |
doing previously. | |
View and Controller: | |
Rdio.AlbumView = Ember.View.extend({ | |
templateName: 'album' | |
}) | |
Rdio.AlbumController = Ember.ObjectController.extend(); | |
A static tempalte for an album based on Rdio: | |
<div class='album-info'> | |
<div class='album-image'> | |
<img src=''/> | |
</div> | |
<div class='album-name'>Synthetica</div> | |
<div class='album-artist'>Metric</div> | |
<div class='album-release-info'> | |
June 12, 2012 on Mom & Pop Music | |
</div> | |
</div> | |
<div class='album-tracks-count'> | |
11 songs (43:31) | |
</div> | |
<div class='album-tracks-list'> | |
<div class='album-track'> | |
<div class='album-track-number'>1</div> | |
<div class='album-track-title'>Artificial Nocture</div> | |
<div class='album-track-duration'>5:44</div> | |
</div> | |
<!-- many more --> | |
<div class='album-track'> | |
<div class='album-track-number'>11</div> | |
<div class='album-track-title'>Nothing But Time</div> | |
<div class='album-track-duration'>4:04</div> | |
</div> | |
</div> | |
I've skipped over some | |
Some points to revisit: | |
* we provided a templateName and static tempalte, which we'll flehs out next | |
* AlbumController is an ObjectController since we'll be proxying to an | |
object (an album in this case) | |
Navigate back to this state to verify everythign was added correctly. Then, update | |
the static template to include handelbars and update your stub data to include | |
any missing properties to verify contente gets filled. | |
<div class='album-info'> | |
<div class='album-image'> | |
<img {{bindAttr src="coverImageUrl"}}/> | |
</div> | |
<div class='album-name'>{{name}}</div> | |
<div class='album-artist'>{{artist}}</div> | |
<div class='album-release-info'> | |
{{releaseDate}} on {{recordLabel}} | |
</div> | |
</div> | |
<div class='album-tracks-count'> | |
{{tracks.length}} songs ({{duration}}) | |
</div> | |
<div class='album-tracks-list'> | |
{{#each track in tracks}} | |
<div class='album-track'> | |
<div class='album-track-number'>{{track.number}}</div> | |
<div class='album-track-title'>{{track.title}}</div> | |
<div class='album-track-duration'>{{track.duration}}</div> | |
</div> | |
{{/each}} | |
</div> | |
## Adding Consistency And Reducing Repetition | |
Looking at Rdio we see that you can navigate to the 'artist.album' state from many | |
differet states, not just from the 'artist.summary' state. Next let's add the ability | |
to transition directly from the 'heavyRotation' state to 'artist.album' state: | |
Return to the section of heavyRotation view where albums are looped through and | |
add 'showAlbum's for the album cover and the album name, remombering to provide | |
the correct context (in this case `album`, which refers to the current album | |
as we loop.) | |
<div class='albums-album'> | |
<div class='album-cover' {{action showAlbum album}}> | |
<img {{bindAttr src="album.coverImageUrl"}}> | |
</div> | |
<div class='album-info'> | |
<div class='album-title' {{action showAlbum album}}>{{album.name}}</div> | |
<div class='album-artist' {{action showArtist}}>{{album.artist.name}}</div> | |
<div class='album-songs-count'>{{album.tracks.length}} songs</div> | |
<div class='user-avatar-icon'><img {{bindAttr src="album.avatarImageUrl"}} /></div> | |
</div> | |
</div> | |
Reload the application and click the album cover image or the album name. WKC should tell | |
you the router doesn't know how to showAlbum. From within the heavyRotation state, there | |
is no transition named 'showAlbum'. We only have that transition defined inside of | |
the 'artist' state. | |
We could solve this by other adding an identical transition to the heavyRotation state. Go ahead | |
and try that now: | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
showArtist: Ember.Route.transitionTo('artist.summary'), | |
showAlbum: Ember.Route.transitionTo('artist.album'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}) | |
This leads to some reptitive code. Being able to define state-specfic transition | |
actions is a useful tool if we need slightly different transition behavior, but | |
because these two transition actions are identical we can reduce repition by moving | |
transition closer to the root of the nested states. Cut the `showAlbum` transition from | |
both the 'heavyRotation' and 'artist' states and add it to the root state, which will | |
look a bit like this: | |
root: Ember.Route.extend({ | |
showAlbum: Ember.Route.transitionTo('artist.album'), | |
}) | |
When the router cannot respond to an action it will walk up the state chart towards the | |
root route looking for a matching action only warning when it reaches the state managers | |
shallowest state and doesn't find the action. | |
Reload the application and click the an album cover or album name and you should enter | |
the 'artist.album' state. Feel free to begin making connections to this state on your | |
own where approprite in the application. | |
### Outlets within Outlets | |
So far we've only ever connected the {{outlet}} inside the application template, but | |
this basic stratgey can be repeated from with any template. | |
The artist page has four navigation items (Albums, Songs, Biography, and Related Artists) | |
when clicked in the actual Rdio, they redraw the entire area of the content div... this | |
is an artifact of Rdio's use of a framework that doesn't have composed views. We can do | |
one better. | |
Update the 'artist' template to replace the the top albums sections with a call to | |
to `{{outlet}}`: | |
' | |
<div class='play-button'></div> | |
<h3 class='name'>{{name}}</h3> | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li {{action showSongs songs}}>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
<div class='biography-teaser'> | |
{{biographyTeaser}} | |
<a>More...</a> | |
</div> | |
<div class='album-tracks-list'> | |
{{#each track in tracks}} | |
<div class='album-track'> | |
<div class='album-track-number'>{{track.number}}</div> | |
<div class='album-track-title'>{{track.title}}</div> | |
<div class='album-track-duration'>{{track.duration}}</div> | |
</div> | |
{{/each}} | |
</div> | |
to | |
<div class='play-button'></div> | |
<h3 class='name'>{{name}}</h3> | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li {{action showSongs songs}}>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
<div class='biography-teaser'> | |
{{biographyTeaser}} | |
<a>More...</a> | |
</div> | |
{{outlet}} | |
if you reload and navigate back to this state you'll find see the albums have been removed. | |
Let's extend summary to include substates for | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
showAlbum: Ember.Route.transitionTo('album'), | |
connectOutlets: function(router, context){ | |
router.get('applicationController').connectOutlet('artist', context) | |
} | |
}) | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
}, | |
albums: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlet: function(router, context){ | |
router.get('artistController').connectOutlet('albums', context) | |
} | |
}) | |
}), | |
provide a better transition where this exists | |
showArtist: Ember.Route.transitionTo('artist.summary'), | |
to: | |
showArtist: Ember.Route.transitionTo('artist.summary.albums'), | |
Update artist data to includes songs as array: | |
Update the navigation section of the 'artist' template to include | |
an action to a as-yet uncreated songs state | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
to | |
<ul class='artist-data-filter'> | |
<li>Albums</li> | |
<li {{action showSongs songs}}>Songs</li> | |
<li>Biography</li> | |
<li>Related Artists</li> | |
</ul> | |
add that action to the summary state: | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
}, | |
albums: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlet: function(router, context){ | |
router.get('artistController').connectOutlet('albums', context) | |
} | |
}) | |
}), | |
to | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
showSongs: Ember.Route.transitionTo('songs'), | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
}, | |
albums: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlet: function(router, context){ | |
router.get('artistController').connectOutlet('albums', context) | |
} | |
}) | |
}), | |
and add the new songs state as child state of summary: | |
from | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
showSongs: Ember.Route.transitionTo('songs'), | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
}, | |
albums: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlet: function(router, context){ | |
router.get('artistController').connectOutlet('albums', context) | |
} | |
}) | |
}), | |
to | |
artist: Ember.Route.extend({ | |
summary: Ember.Route.extend({ | |
hasContext: true, | |
showSongs: Ember.Route.transitionTo('songs'), | |
connectOutlets: function(router,context){ | |
router.get('applicationController').connectOutlet('artist',context) | |
}, | |
songs: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlets: function(router, context){ | |
router.get('artistController').connectOutlet('songs', context) | |
} | |
}), | |
albums: Ember.Route.extend({ | |
hasContext: true, | |
connectOutlet: function(router, context){ | |
router.get('artistController').connectOutlet('albums', context) | |
} | |
}) | |
}), | |
it has a context (the songs) so set hasContext true | |
reload, navigate to this state by trying to click the "Songs" sub navigation element | |
to get the familiar "The name you supplied songs did not resolve to a view" error. | |
Add SongsView and SongsController: | |
Rdio.SongsView = Ember.View.extend({ | |
templateName: 'songs' | |
}); | |
Rdio.SongsController = Ember.ArrayController.extend({}) | |
and a template for the songs: | |
<h3>Songs</h3> | |
<div class='artist-songs'> | |
{{#each song in controller}} | |
<div class='song'> | |
<div class='album-track-title'>{{song.title}}</div> | |
<div class='album-track-duration'>{{song.duration}}</div> | |
</div> | |
{{/each}} | |
</div> | |
Go ahead and the other navigation elements, their transitions, states, views, controller, | |
and templates if you feel like it. | |
## Connecting to Actual Data | |
So far we've used dummy data. It's time to being connecting to actual Rdio data. | |
* creaed a project for you | |
* node.js | |
* proxies to Rdio. CORS, JSON-P | |
change connectOutlets in heavyRotation state to include a context: | |
heavyRotation: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}), | |
to | |
heavyRotation: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation', Rdio.getHeavyRotation); | |
} | |
}), | |
This will set the `content` of the the shared instance of HeavyRotationController to the result of | |
Rdio.getHeavyRotation; | |
For now, let's just return an empty array: | |
Rdio.getHeavyRotation = function(){ | |
return []; | |
} | |
and remove the dummy content of HeavyRotationController entirely. | |
Rdio.HeavyRotationController = Ember.ArrayController.extend({ | |
content: [...] | |
}); | |
to | |
Rdio.HeavyRotationController = Ember.ArrayController.extend({}); | |
Reload the page and you'll see the albums no longer show albums. | |
Update the Rdio.getHeavyRotation to return real data using $.ajax: | |
Rdio.getHeavyRotation = function(){ | |
var heavyRotation = []; | |
$.ajax({ | |
type: 'post', | |
url: 'api/getHeavyRotation', | |
dataType: 'json', | |
context: heavyRotation, | |
success: function(data){ | |
console.log(data) | |
}, | |
}); | |
return heavyRotation; | |
}; | |
data is a JSON object with two keys: `status` and `result`. `result` is an array with 10 items. | |
success: function(data){ | |
this.addObjects(data.result); | |
}, | |
this is the passed context (the array) which has addObjects added to it. | |
reload the page, you'll see some data missing because we used some different property names | |
than the data that is returned. The easist solution now is to change the property names in | |
our views to match. Let's do it | |
what we called `album.coverImageUrl` is called `icon` in the returned data. | |
`album.tracks.length` is just 'album.length' | |
One particular data change will cause us some grief. Rdio returns the name of the | |
album's artist as string in the property artist, but we've been expecting the | |
the property `artist` to return an actual Ember.Object. | |
Go back and fix this property so the name displays and try to click through to | |
the artsit page. You'll get an error. | |
If we controlled both | |
server and client code, we could resolve this by selecting a differnet property | |
name on the server. Because we don't (and often, in real development, will not), | |
we'll have to fix this on the client. | |
Let's make our first legimate model classes: | |
Rdio.Album = Ember.Object.extend({ | |
artist: function(propName, value){ | |
return Ember.Artist.create({ | |
name: value | |
}); | |
}.property() | |
}); | |
Rdio.Artist = Ember.Object.extend(); | |
success: function(data){ | |
this.addObjects(data.result); | |
}, | |
to | |
success: function(data){ | |
data.result.forEach(function(albumData){ | |
this.addObject(Rdio.Album.create(albumData)) | |
}, this); | |
}, | |
And change the code that loads the data to typecast our data | |
## Serializing States | |
## Deserializing States | |
----- END -------- | |
<div class='albums-album'> | |
<div class='album-cover'> | |
<img src=''/> | |
</div> | |
<div class='album-info'> | |
<div class='album-title'>Gossamer</div> | |
<div class='album-artist'>Passion Pit</div> | |
<div class='album-songs-count'>12 songs</div> | |
<div class='user-avatar-icon'><img src='...' /></div> | |
</div> | |
</div> | |
</div> | |
Click the album, you get a complaint about the router not know what showAlbum is. | |
Let's add it: | |
Rdio.Router = Ember.Router.extend({ | |
enableLogging: true, | |
location: 'none', | |
root: Ember.Route.extend({ | |
heavyRotation: Ember.Route.extend({ | |
showTopCharts: Ember.Route.transitionTo('topCharts'), | |
showAlbum: Ember.Route.transitionTo('album'), | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('heavyRotation') | |
} | |
}), | |
topCharts: Ember.Route.extend({ | |
connectOutlets: function(router){ | |
router.get('applicationController').connectOutlet('topCharts') | |
} | |
}), | |
album: function(router){ | |
router.get('applicationController').connectOutlet('album') | |
} | |
}) | |
}) | |
we want to fill the main content area with the view for an albumn. Let's look at this | |
on Rdio and express it as a handlebars template: | |
<div class='album-info'> | |
<div class='album-image'> | |
<img src=''/> | |
</div> | |
<div class='album-name'>The Abandoned Lullaby - Instrumentals</div> | |
<div class='album-artist'>RJD2</div> | |
<div class='album-release-info'> | |
July 24, 2012 on RJ's Electrical Connections | |
</div> | |
</div> | |
<div class='album-tracks-count'> | |
12 songs (50:05) | |
</div> | |
<div class='album-tracks-list'> | |
<div class='album-track'> | |
<div class='album-track-number'>1</div> | |
<div class='album-track-title'>Charmed Life (Instrumental Version)</div> | |
<div class='album-track-duration'>3:41</div> | |
</div> | |
<!-- many more --> | |
<div class='album-track'> | |
<div class='album-track-number'>12</div> | |
<div class='album-track-title'>Find Yourself (Instrumental Version)</div> | |
<div class='album-track-duration'>4:16</div> | |
</div> | |
</div> | |
Let's make a this a template and a view: | |
Rdio.AlbumView = Ember.View.extend({ | |
templateName: 'album' | |
}) | |
Rdio.AlbumController = Ember.Controller.extend({}) | |
OK, but now we're just loading the same album static template for everyone. | |
Let's turn it into handelbars: | |
<div class='album-info'> | |
<div class='album-image'> | |
<img {{bindAttr="coverImageUrl"}}/> | |
</div> | |
<div class='album-name'>{{name}}</div> | |
<div class='album-artist'>{{artist}}</div> | |
<div class='album-release-info'> | |
{{releaseDate}} on {{recordLabel}} | |
</div> | |
</div> | |
<div class='album-tracks-count'> | |
{{tracks.length}} songs ({{duration}}) | |
</div> | |
<div class='album-tracks-list'> | |
{{#each track in tracks}} | |
<div class='album-track'> | |
<div class='album-track-number'>{{track.number}}</div> | |
<div class='album-track-title'>{{track.title}}</div> | |
<div class='album-track-duration'>{{track.duration}}</div> | |
</div> | |
{{/each}} | |
</div> | |
Now clicking through shows no data. We're missing the notion of *which* | |
album we wanted to see. | |
Change the {{action}} to include a context: | |
{{action showAlbum}} | |
becomes | |
{{action showAlbum album}} | |
album refers to the particular item in the loop in the template. | |
This album will be passed through the router's transition as the context | |
and ends up on the connectOutlets call as the second argument. | |
change | |
album: function(router){ | |
router.get('applicationController').connectOutlet('album') | |
} | |
to | |
album: function(router, context){ | |
router.get('applicationController').connectOutlet('album', context) | |
} | |
In addition to changing the view hierachy at {{outlet}} to the album view | |
it will set the view's controller's content property to the specific | |
context we're talking about. Navigate back to this state | |
Still nothing. | |
Ember.Controller doesn't do anything special with a content proeperty, desptite | |
it being assinged. | |
Change to Ember.ObjectController. | |
ObjectController proxies | |
{{name}} -> controller.name -> controller.content.name -> 'The Abandoned Lullaby - Instrumentals' | |
allows us to keep the # of bindings created/destoyed to a minimum, allows us to transform raw data | |
into displayabe formats. e.g. we might calculate total play time as a funciton that adds | |
the track times together if Rdio doesn't provide this data to us directly. | |
Fill out some of your fake test with these additional properties. | |
[2] snipped | |
You'll notice they aren't <a>. They don't need to be. State isn't controlled by urls | |
urls reflect state. | |
[1] snipped | |
We know that these navigation elements will expressed as links, so I'll go a head and | |
wrap the <li> text in <a> tags. We'll give these empty ("#") hrefs for now. The <a> tag | |
is referred to as an "anchor" tag, but in Ember it might be more helpful to think of | |
it "a" as standing for "action". | |
<div class='navigation'> | |
<h3>Browse</h3> | |
<ul> | |
<li><a href="#">Heavy Rotation</a></li> | |
<li><a href="#">Recent Activity</a></li> | |
<li><a href="#">Top Charts</a></li> | |
<li><a href="#">New Releases</a></li> | |
</ul> | |
<h3>Your Music</h3> | |
<ul> | |
<li><a href="#">Collection</a></li> | |
<li><a href="#">History</a></li> | |
<li><a href="#">Queue</a></li> | |
</ul> | |
</div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment