Building the RocknRollCall Prototype: Controllers, Views, Data Binding, and Events - Building Web Apps with Ember.js (2014)

Building Web Apps with Ember.js (2014)

Chapter 6. Building the RocknRollCall Prototype: Controllers, Views, Data Binding, and Events

We’ve figured out templates. We’ve figured out routers and routes. We’ve figured out models. What’s left?

Well, we need a place to keep track of moment-to-moment state data: the kind of stuff you absolutely need to know right now but you probably don’t need to write to a database. This is the kind of state that you expect to reset when you leave and come back to a site. We also need a place for more complicated rendering logic. We need a way to coordinate this fancy renderer and our moment-to-moment state data manager.

For the transient state data, we need controllers. We need views for the extended rendering control. We need events to connect controllers and views, and, finally, we need data binding to keep our data in-sync across all these pieces.

When we’ve got all these in place, we’ll have a working prototype. Let’s do it!

Controllers

Yes, you’ve gotten all the way to Chapter 6 in a book about an MVC framework without really talking about views or controllers (or really-real models, for that matter). Don’t worry. You’ll get your money’s worth.

In Ember, controllers have these four most important jobs (among others):

§ Manipulate the data within the application’s models

§ Store transient data, whether standalone or made up of data retrieved from models

§ Listen to events dispatched by and dispatch events intended for other controllers, views, and templates

§ Instigate the transition from one route to another

As you can see, this is critical stuff. Without controllers, your application wouldn’t amount to much.

Backing up a bit in our application’s development, we never made it possible to get from the IndexRoute to the SearchResultsRoute. Typing “Tom Waits” into the search box at the top of our application and hitting Enter doesn’t do anything, yet. Let’s fix that. Add this very simpleApplicationController to your app.js:

RocknrollcallYeoman.ApplicationController = Em.ObjectController.extend({

searchTerms: '',

applicationName: "Rock'n'Roll Call",

actions: {

submit: function() {

this.transitionToRoute('search-results',this.get('searchTerms'));

}

}

});

Starting on the second line in the newest block of code, we’ve created a transient variable local to our controller called searchTerms, a real home for our application name (you can go ahead and delete the line in which we hung a variable of the same name off of the App global variable), followed by an actions object, which will be covered in a few paragraphs.

Also, we still have a leftover IndexController from a simple demonstration of actions in Chapter 4, so let’s remove app/scripts/controllers/index_controller.js because we won’t be using it.

See the changes in this commit.

We can now plug in a reference to searchTerms in our template, just as we did with model data in previous chapters. Here’s where we left the search section within our application.hbs template:

<ul class="nav navbar-nav search-lockup">

<li class="search-group">

{{input type="text" class="search-input" placeholder="Search for artists or

song names"}}

<button class="btn btn-primary"><i class="glyphicon glyphicon-play"></i>

</button>

</li>

</ul>

We’ve seen a couple examples of putting variables from models into the text node—the space between the opening and closing tags of a typical HTML element. In this case, though, we want to bind our variable to the value property of our input helper. The input allows its value to be bound via a valueBinding property on the helper, like so:

<li class="search-group">

{{input class="search-input" placeholder="Search for artists or song names"

valueBinding="controller.searchTerms"}}

<button class="btn btn-primary"><i class="glyphicon glyphicon-play"></i>

</button>

</li>

Notice that we declared controller.searchTerms. Had we not specified the controller was our source, Ember would have assumed it should find the searchTerms variable within the model, and, indeed, if there was also a variable by that name within the model, its value would have been injected into the template.

If you were to reload your application now, you would not be blown away, exactly. Although we have bound the value of the input and our searchTerms value, we’re not doing anything with that value. We defined an actions block in our controller. Let’s see how you reference anaction from a template. It looks like this:

<li class="search-group">

{{input action="submit" type="text" class="search-input" placeholder="Search for artists or song names" valueBinding="controller.searchTerms"}}

<button {{action "submit"}} class="btn btn-primary">

<i class="glyphicon glyphicon-play"></i>

</button>

</li>

We added two action references there, in case you didn’t notice. Typically speaking, the action helper adds a “click” event listener to the element to which it is added. You could say adding an action to an element “makes it clickable.” In the case of our Play button, that’s exactly what we’re looking for. The user should be able to type in a search term and click the Play button to search. In the case of our Ember.TextField, though, Ember knows you probably don’t want to click your input tag to fire the event, but rather that the submit event of HTML forms is the one you’re looking for. In most browsers, this is what happens if you type into the form element and hit Enter. This is the event to which Ember will bind an action reference within an input helper.

The actions object we defined within our controller defines event handlers for events fired by our templates. In our templates, we defined an action called “submit,” so Ember fires a submit event when those elements are triggered (by click or by keystroke). Our controller then looks for a handler of matching name within its actions block and fires the handler.

Save the changes we’ve just made and reload your page. Type in a search term and click the Play button. It worked (hopefully!)! You’ve just navigated from one view to another!

See the change in this commit.

Following along with the code, it’s not too hard to see the trail. In your application.hbs template you added an action attribute to the button tag. When the template was rendered, Ember, therefore, added a click handler to the button, which, when fired, looked for a handler calledsubmit within the actions object in your controller and executed that handler. In our case, that handler used the transitionToRoute method of the Ember.Controller class, which tells (or asks) the Router to navigate to a new route—the one passed as the first argument to thetransitionToRoute method.

Pay special attention to what we pass as that second argument: the search terms the user entered. That should be no surprise, but it should set your mind to wondering how what we pass there gets turned into the dynamic segment. You did notice that, right? Whatever is typed into the search box gets tacked onto the URL (of the new /search path) after you click the Play button. Give yourself a minute to ponder how that might be happening based on what you’ve read so far.

Did you guess it? This is serialization. The argument you pass to transitionToRoute is usually a model that you want to pass to the route, although it can be, and is, in this case, just a string. You have the opportunity, in your route, to define a serialize method that accepts what is passed in here, and turn it into a path string, which it returns. So, in other words, the serialize method takes data in and outputs a URL—it serializes the data. Sometimes, as in our case, the data that you’re passing in the first place is already a string, the very string you want to use as your path—no transformation necessary.

We haven’t written a serialize method, yet, though. How, then, is our application already serializing that argument?

Oh, right: “Because this pattern is so common, it is the default.” If we don’t define a serialize method of our own, the default implementation passes the argument right on through if it happens to already be a string.

While you have that template open, it would be a good time to swap out this line:

{{#link-to "index" class="navbar-brand"}}{{RocknrollcallYeoman.applicationName}}{{/link-to}}

with this:

{{#link-to "index" class="navbar-brand"}}{{applicationName}}{{/link-to}}

This simply replaces our global variable anti-pattern with our ApplicationController reference to our application’s name.

See the change in this commit and this commit.

Computed Properties

Computed properties are one of the biggest selling points of Ember. In most training materials, they’re one of the first concepts covered. We’ve saved it until now in hopes that the added context will make it easier to appreciate just how useful they are.

We’ve named our application “Rock’n’Roll Call,” a play on the concept of roll call, wherein someone stands at the front of a room and rattles off names from a list, one by one, and attendees of whatever meeting is being called respond, “Present!” when their name is called. So, for a little moment of delight, let’s have our site’s name walk through this familiar interaction. When you first arrive at the site, let’s have our application name proudly displayed. When you search for something, “Tom Waits,” for instance, let’s change that text to “Tom Waits???”

We can do all of that with a computed property. Let’s do an easy one, first. Replace your applicationName variable in App.ApplicationController with this one:

applicationName: function() {

var st = this.get('searchTerms');

if (st) {

return st + "???"

} else {

return "Rock'n'Roll Call"

}

}.property('searchTerms'),

Let’s start with the last line of that block: the property function that we call on what gets returned from applicationName. That property function takes the name of one or more properties on which your computed property (applicationName, in this case) should depend. We’ve specified here that applicationName depends on the property searchTerms, a property of our controller. You can specify properties of other objects—other controllers, models, etc. Because we’ve specified this dependency, Ember will now watch our searchTerms property for changes. Any time it changes, the function we’ve defined as our applicationName will be fired, returning a new value for our applicationName variable. In this example, if the searchTerms property has a truthy value, we return that and a few question marks, a la “Tom Waits???” If it has a falsy value, we return our application’s name. Computed properties can exist on anything that extends Ember.Object, which includes controllers, models, and more. You can read all about computed properties at the source, Emberjs.com’s Ember.ComputedProperty class API documentation.

Save your changes and watch what happens. The application name updates in real time as you type into the input field! Ember’s data binding at work.

See the change in this commit.

WHEN TO USE .GET() AND .SET()

If you look at enough Ember tutorials, you’re bound to see someone referencing and even assigning properties on Ember objects (models, controllers, etc.) with a simple reference. For instance, if we had tried to do so in our applicationName computed property, it might have looked like this:

applicationName: function() {

var st = this.searchTerms;

if (st) {

return st + "???"

} else {

return "Rock'n'Roll Call"

}

}.property('searchTerms'),

It’s generally a good idea to use the .get() and .set() methods of Ember objects, instead. These methods do more than simply reading and assigning values to properties; they are part of the Ember.Observable mixing, and, as such, they are triggering property change events when you set the value and listening for property change events when you read the property. Using .get()will ensure you have the most current value of the property, even if it’s been very recently changed. Using .set() will ensure that any other Ember objects that are observing the property you’re about to change get notified when it changes.

The Power of Promises and the model Method

We have a RocknrollcallYeoman.Artist model. We need a RocknrollcallYeoman.Song, too. Here it is:

RocknrollcallYeoman.Song = Em.Object.extend({

id: null,

enid: null,

title: null,

artist_name: null,

artist_id: null,

audio_md5: null,

audio_summary: null,

hotttnesss: null,

track: null,

types: null

});

See the change in this commit.

Our search-results route doesn’t currently have much of a model to hand off to the SearchResultsController we’re probably going to need to build. We’ll need to address that.

THIS IS KIND OF A BIG DEAL

There’s a big lesson to be learned here: if it feels like you’re writing a lot of code—too much code—to accomplish something with Ember, you’re probably doing it wrong.

In this case, we made use of setupController (a method we haven’t talked about yet) to do this next part, which is a method within your route that gets passed a fully baked controller and the model returned from your route’s model method. Sounds like a really convenient place to write code that depends on those two pieces, right? The trick is, generally speaking, you probably don’t need to and shouldn’t depend on populated models and fully baked controllers. You should write loosely coupled code that leans on Ember’s data binding and events. After all, that model data can change very quickly. Your controller can be extended with new properties and methods after the fact, too (see Ember.Object.reopenClass).

Our first draft of this prototype was a little spaghetti-like in this section. This next step is a little tricky. We want to take what the user typed in as a search term expression, do two searches against The Echo Nest’s database with those terms (one search assuming the user is looking for an artist and one assuming the user is looking for a song), and then we need to operate on the results from those searches in order to create and populate some models, which we’ll return to our controller.

We originally thought the best way to do this was to make use of the setupController method of our SearchResultsRoute. setupController is a method within your routes that is called after your controller is instantiated and your model method has returned content to the controller and you’re passed a reference to each as an argument to setupController. From one controller object, it’s pretty easy to get access to other controllers. The most obvious solution to us, on our first run through, was to create two methods in our SearchResultsControllerthat each knew how to query The Echo Nest’s API to create their respective models: Artist and Song.

This resulted in some confusing code. Our SearchResultsRoute fired its setupController method, which then called those two methods of the SearchResultsController, which made the queries against The Echo Nest, and then, with the results, created models and updated a property on the SearchResultsController. There’s a lot of reaching around there, and it’s also telling that we didn’t even use the model method.

So we re-factored. Here’s the saner version of SearchResultsRoute, which makes use of RSVP:

RocknrollcallYeoman.SearchResultsRoute = Ember.Route.extend({

model: function (query) {

return Promise.all([

$.getJSON("http://developer.echonest.com/api/v4/artist/search?api_key=

<YOUR-API-KEY>&format=json&results=10&bucket=images&bucket=hotttnesss

&bucket=biographies&bucket=id:musicbrainz", { name: query.term }),

$.getJSON("http://developer.echonest.com/api/v4/song/search?api_key=

<YOUR-API-KEY>&format=json&results=10&bucket=id:7digital-US&bucket=

audio_summary&bucket=song_hotttnesss&bucket=tracks&bucket=song_type",

{ title: query.term })

]).then(function(jsonArray){

var artistResults = jsonArray[0].response.artists,

songResults = jsonArray[1].response.songs,

artists = [],

songs = [],

i = 0, entry = null;

for (i = 0; i < artistResults.length; i++) {

var entry = artistResults[i];

artists.push(RocknrollcallYeoman.Artist.create({

id: i + 1,

type: 'artist',

name: entry.name,

hotttnesss: entry.hotttnesss,

enid: entry.id

}));

}

entry = null;

for (i = 0; i < songResults.length; i++) {

entry = songResults[i];

songs.push(RocknrollcallYeoman.Song.create({

id: i + 1,

title: entry.title,

enid: entry.id,

type: 'song',

artist_id: (entry.artist_id) ? entry.artist_id : null,

artist_name: entry.artist_name,

hotttnesss: entry.song_hotttnesss,

audio_summary: entry.audio_summary

}));

}

return {artists: artists, songs: songs}

});

}

});

Walking, briskly, through that model method, we return a Promise.all object, which only resolves and calls its then method when all functions passed to it have successfully resolved. We pass two functions, each of which use jQuery’s getJSON to query The Echo Nest (once for song results and once for artist results). Our then method gets an array of JSON response objects, corresponding to the two queries (songs and artists), each of which are arrays of objects—the search results. We loop over each, building our own arrays of Song and Artist models. We then return an object that contains those two arrays.

So, our model method will return a promise which, when fulfilled, will return an object with song models and artist models corresponding to the search results appropriate for the search terms the user entered before hitting Enter or clicking the Play button.

This may still look somewhat complex. Believe us when we say it’s a lot prettier than the previous version.

NOTE

You may have noticed <YOUR-API-KEY> in the XMLHttpRequest to the Echo Nest server:

$.getJSON("http://developer.echonest.com/api/v4/artist/

search? api_key=<YOUR-API-KEY>...

You’ll need to replace this with your own API key for the Echo Nest. To do this, visit https://developer.echonest.com/account/register in your browser, fill out the form, and follow the directions. Once you’ve activated your account, find your API key on your profile page.

See the change in this commit.

Now that we have a SearchResultsRoute, we can move our HTML from the app/templates/index.hbs template to a new template. First create a new file, app/templates/search-results.hbs, and update it with the following:

<div class="search-results-wrapper clearfix">

<div class="search-facets col-md-2">

<h3>Show:</h3>

<ul class="facets">

<li>

<label>Artists</label>

{{view Ember.Checkbox checkedBinding="artistsIsChecked"}}

</li>

<li>

<label>Songs</label>

{{view Ember.Checkbox checkedBinding="songsIsChecked"}}

</li>

</ul>

</div>

<div class="results col-md-10">

{{#if artistsIsChecked}}

{{#if artists.length}}

<h3>Artists</h3>

<ul class="search-results artists">

{{#each artists}}

<li>{{#link-to 'artist' enid}}{{name}} {{/link-to}}</li>

{{/each}}

</ul>

{{/if}}

{{/if}}

{{#if songsIsChecked}}

{{#if songs.length}}

<h3>Songs</h3>

<ul class="search-results songs">

{{#each songs}}

<li>{{#link-to 'song' enid}}"{{title}}," by {{artist_name}}

{{/link-to}}</li>

{{/each}}

</ul>

{{/if}}

{{/if}}

</div>

</div>

Running through those changes, we replaced our input tags with Ember.Checkbox helpers, which will render input tags whose checked value will be bound to whatever we pass to checkedBinding (don’t worry if you don’t recognize what we passed). We added a few #if blocks, which conditionally show whole sections of the page based on the current value of the same thing we passed to those Ember.Checkbox helpers and whether the search results arrays actually have any content. If the user unchecks “songs” or if the search results array for songs is empty, we don’t render that section.

Finally, we added a link-to to our search result li, linking to the routes artist and song, and passing the enid to each.

See the change in this commit.

So, we bound the checkboxes to variables called artistsIsChecked and songsIsChecked, but we havent’ defined those anywhere, yet. These, dear reader, are a perfect example of the transient data that belongs in a controller—let’s create a SearchResultsController:

RocknrollcallYeoman.SearchResultsController = Em.ObjectController.extend({

artistsIsChecked: true,

songsIsChecked: true

});

I hope that one didn’t blow your mind.

See the change in this commit.

All we need now is a SongRoute to match our ArtistRoute and templates for each. Here’s our SongRoute:

RocknrollcallYeoman.SongRoute = Ember.Route.extend({

model: function(params) {

//find the song byId

var url = "http://developer.echonest.com/api/v4/song/profile?api_key=

<YOUR-API-KEY>&format=json&bucket=audio_summary&bucket=song_hotttnesss

&bucket=tracks&bucket=song_type&bucket=id:7digital-US",

obj = {"id": params.enid}

return Ember.$.getJSON(url, obj)

//returns Promise object

.then(function(data) {

var entry = data.response.songs[0];

var track = null;

if (entry.tracks.length) track = entry.tracks[0];

return RocknrollcallYeoman.Song.create({

enid: entry.id,

title: entry.title,

hotttnesss: entry.song_hotttnesss,

track: track,

types: entry.song_type,

audio_summary: entry.audio_summary,

artist_id: entry.artist_id,

artist_name: entry.artist_name

});

});

}

});

See the change in this commit.

Now, here’s our artist.hbs that we added back in Chapter 4:

<div class="entity-artist page-container">

<div class="artist-bio-lockup clearfix">

{{#if model.image}}

{{#if model.license}}

{{#if model.license.url}}

<a {{bind-attr href="model.license.url"}}>

<img {{bind-attr src="model.image.url"}} class="pull-right">

</a>

{{else}}

<img {{bind-attr src="model.image.url"}} class="pull-right">

{{/if}}

{{else}}

<img {{bind-attr src="model.image.url"}} class="pull-right">

{{/if}}

{{/if}}

<h3 class="fancy">{{model.name}}</h3>

<h4>

{{hotttnesss-badge model.hotttnesss}}

</h4>

<p class="bio pull-left">Biography(from {{model.biography.site}}):

{{model.biography.text}}</p>

<a {{bind-attr href="model.biography.url"}} class="pull-left">Read more</a>

</div>

{{#if model.videos.length}}

<div class="videos">

<h5>Videos</h5>

{{#each video in videos}}

<a {{bind-attr href="video.url"}}><img {{bind-attr

src="video.image_url"}} class="video-thumbnail"></a>

{{/each}}

</div>

{{/if}}

</div>

And here’s our new song.hbs:

<div class="entity-song page-container">

<div class="song-lockup clearfix">

{{#if model.track}}

{{#if model.track.release_image}}

<img {{bind-attr src="model.track.release_image"}} class="pull-right">

{{/if}}

{{/if}}

<h3 class="fancy">{{model.title}}</h3>

<h4>

Artist: {{#link-to "artist" artist_id}}{{artist_name}}{{/link-to}}

</h4>

<h4>

Tags:

{{#each types}}

<span class="badge">{{.}}</span>

{{/each}}

</h4>

<h4>

Duration: <span id="song-duration" {{bind-attr data-duration=

"model.audio_summary.duration"}}></span>

</h4>

<h4>

Tempo: {{model.audio_summary.tempo}} <abbr>BPM</abbr>

</h4>

</div>

</div>

Hopefully by now you can read those pretty easily.

Save your changes, and tool around. You have a web application!

See the change in this commit.

“But wait,” you say, “we haven’t even talked about views!”

Views

You’re absolutely right. Pretty interesting, no? We’re six chapters and a working web application into this book, and we haven’t talked about Ember’s View. Think back on what we’ve accomplished with computed properties and data binding, though, and you’ll see why. How much of your view code with other application stacks deals with updating views based on changes in data? For me, at least, I think it’s safe to say almost all of it. And look at the checkbox interaction we just built. We built a simple but useful faceted, search-like interaction widget without writing any view code or declaring any events, using just data binding and the #if flow control in our template.

But there is one thing left we’d like to do that our template doesn’t make very simple. The Echo Nest returns a song’s duration in seconds. That’s great for doing math, but it’s not how humans are used to reading duration. Our song.hbs template is currently pushing the song duration into a data attribute because its value isn’t human friendly. We can’t easily convert seconds to “hh:mm:ss” with template logic, but we can use some view code.

Yes, technically, this would also be a good candidate for a computed property. Your Song model could have a computed property that does the exact same thing we’re about to do. If you do this with view code, though, you could—we won’t—do something interesting, like visualize the duration by spinning the hands of a clock up to the time signature, animating the numbers in flip-clock style or the like.

First, create a SongView in app/scripts/views/song_view.js:

RocknrollcallYeoman.SongView = Em.View.extend({});

See the change in this commit.

Now, let’s add a didInsertElement method. didInsertElement is, to an Ember View, not unlike $().ready() is to jQuery document. It is a handler that is called once a View has been rendered and injected into the page, a safe time to assume the DOM is ready for manipulation:

RocknrollcallYeoman.SongView = Em.View.extend({

didInsertElement: function() {

$('#song-duration').text(function(){

var $this = $(this);

var origSeconds = $this.attr('data-duration');

var minutes = Math.floor(origSeconds/60);

var seconds = Math.floor(origSeconds % 60);

return minutes + ":" + seconds

});

}

});

See the change in this commit. Views can also respond to browser events, such as “click,” like so:

RocknrollcallYeoman.SongView = Em.View.extend({

click: function(jQueryEvent) {

console.log(jQueryEvent.target);

}

});

You can also specify the events you wish to respond to with a different syntax and a different approach to context, like so:

RocknrollcallYeoman.SongView = Em.View.extend({

eventManager: Ember.Object.create({

click: function(jQueryEvent, view) {

console.log(view);

}

})

});

With this syntax, you get two arguments: the jQuery event and a View. Don’t forget that you can nest Views inside one another. In our first click handler, declared right on the View, our context within our handler would always be the View (in this case, SongView). In our second version, if there was a nested View—rendered to an outlet or similar helper—and it was the target of the click event, that nested view would be given to our handler in its second argument, giving us access to both contexts.

Views get really interesting when you start looking at helpers, components, and the like, which we’ll do in Chapter 9.

Wrapping Things Up

Are you tearing up a little? I am! We just built your first Ember application! We built our own router, routes, templates, models, controllers, and views, and we learned how to keep them synchronized and working together with data binding and events. We even learned how to build a fairly complex model comprised of data from multiple queries against a third-party database! And best of all, we learned that Tom Waits’ birthday is December 7!

Consider how much you’ve learned to do in so little code, in so little time. This is “developer ergonomics” at work.

In the remaining chapters, we will prepare our prototype for production.