Skip to content

Instantly share code, notes, and snippets.

@trek
Created November 27, 2012 14:18
Show Gist options
  • Save trek/4154434 to your computer and use it in GitHub Desktop.
Save trek/4154434 to your computer and use it in GitHub Desktop.
original notes
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