In this chapter we’re going to add users to EmberTrackr. In many ways, adding users is similar to adding tickets. A user entity has a few string-based properties, that can be viewed in a list or in detail, and a new user can be added, or an existing user can be modified.
Before we dive into the sort of high-level behavior tests that we saw in the previous chapter, we’ll sketch out a couple of useful computed properties on the user model using unit tests. The tests will drive creation of the model.
Users will have a first and last name, but we’d also like a computed property that will glue to the two together. We’ll call it displayName and we’ll write a test for it in tests/user_test.js:
module('User'); test('displayName', function() { var user = App.User.create({ firstName: 'Tom', lastName: 'Dale' }); equal(user.get('displayName'), 'Tom Dale'); });
This leads us to the expected failure:
Cannot call method 'create' of undefined.
We’ve not yet created our user model, so let’s go ahead and do that now:
$ ember generate -m user created: js/models/user.js
Well that’s… different. Apparently we’re not allowed to call create on our Ember Data model classes directly. Instead we’re told to use store.createRecord, but where are we going to get store from? In previous tests, we cheated a bit and took a look inside the magic __container__ property of a running app to get hold of an instance of the correct store. But this a unit test and it would defy the point if we were relying on the rest of the application being present just to test this small piece. Don’t be deterred — this gives us an opportunity to play with Ember’s Container class.
Let’s set the scene: The Ember Data store is an object responsible for interacting with an API adapter and managing local instances of models. This requires an ability find model classes dynamically. Instead of hard-coding the store to look at a global App object, Ember uses a container pattern to load classes. Ember implements this pattern with the Ember.Container class, though applications actually use Ember.DefaultContainer.
A container’s job is two-fold: factory lookup (finding a class) and dependency injection. When a class is managed by the container, we more accurately call it a factory. It is something that generates instances.
We use the register method to explicitly inform a container about a factory. Factories have a type and a name. So we might reference the user model as model:user. Since the store is unique (we only plan to have a single store in a container), we will give it a type of its own and the name main: store:main.
Once a factory is registered, it can accept dependencies or become a dependency. The inject method handles making one factory a dependency of another. Earlier in our application development, we used the store property on controllers to work with Ember Data. This is an example of a dependency injection: The store had been injected (as the property name store) onto all instances of the controller type.
Finally, a factory is useless without generating things. The lookup method generates instances from factories in the container, with their dependencies applied. Importantly, any item we lookup from the container will have the container itself injected as the container property.
When we accessed App.__container__ in earlier tests, we accessed the container for a running application. In a unit test there is no running application, but Ember Data still expects one to resolve model classes.
How does this knowledge help us construct our unit test for displayName? Well, we know that Ember Data’s models are designed to be accessed via a store instance, and that the store requires a container to find a model. If we create a container, then register a store and our model, we should be able to interact with the user as we do in controllers.
module('User'); test('displayName', function() { var container = new Ember.Container(); container.register('store:main', DS.Store); container.register('model:user', App.User); var store = container.lookup('store:main'); var user = store.createRecord('user', { firstName: 'Tom', lastName: 'Dale' }); equal(user.get('displayName'), 'Tom Dale'); });
How did that work out?
It looks like createRecord is known to have to asynchronous side effects, so we need to wrap it in a run loop to ensure they’re all played out before we get to our assertion.
module('User'); test('displayName', function() { var container = new Ember.Container(); container.register('store:main', DS.Store); container.register('model:user', App.User); var store = container.lookup('store:main'), user; Ember.run(function() { user = store.createRecord('user', { firstName: 'Tom', lastName: 'Dale' }); }); equal(user.get('displayName'), 'Tom Dale'); });
Now the part of our test that expects a runloop has one. We have resolved all errors in the test, and make it to our failing assertion:
All that remains now is to implement displayName. Over to js/models/user.js:
var User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), displayName: function() { return this.get('firstName') + ' ' + this.get('lastName'); }.property('firstName', 'lastName') }); module.exports = User;
And we’re green!
You’d be forgiven for thinking that was quite a lot of setup for a simple unit test, so we want to take a moment to impress upon the reader that most unit tests in Ember won’t be this complex. In fact, for the most part unit testing the behavior of an Ember object is no different than testing a native javascript object. The complexity in this case is a somewhat unavoidable compromise of solving a hard problem (asynchronous data modeling and persistence) in an elegant and robust manner. A model adrift of its store is likely to fall out of sync pretty quickly, so we sacrifice testing simplicity for a big gain in developer happiness in the running codebase.
We’d quite like to add more unit tests for User, so rather than repeating ourselves let’s hoist some of that container/store setup into the module:
var container, store; module('User', { setup: function() { container = new Ember.Container(); container.register('store:main', DS.Store); container.register('model:user', App.User); store = container.lookup('store:main'); } }); test('displayName', function() { var user; Ember.run(function() { user = store.createRecord('user', { firstName: 'Tom', lastName: 'Dale' }); }); equal(user.get('displayName'), 'Tom Dale'); });
That’s made the test a lot cleaner, so let’s add another.
We’d like a gravatarURL property which builds a URL string for the user’s gravatar image. For those who may not be familiar with gravatar, it's an online service that allows anyone to associate their email address with an avatar picture. The benefit of that is for apps like EmberTrackr that have a user’s email address, so you can render a picture of them without having to implement an upload and storage mechanism. Additionally, when a user wants to update their profile picture across multiple sites, they only have to do so in one location.
The URL is constructed from the MD5 sum of the email address. It also expects a size param, which we’ll assume to be a predefined 80. We’ll add the test immediately after our displayName test:
test('gravatarURL', function() { var user; Ember.run(function() { user = store.createRecord('user', { email: 'tom@example.com' }); }); equal(user.get('gravatarURL'), 'http://www.gravatar.com/avatar/e4f7cd8905e896b04425b1d08411e9fb.jpg?s=80'); });
This gives us the expected failure so let’s go about implementing gravatarURL in our User model. First we’ll add the email property:
var User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), email: DS.attr('string'), // ... });
Then the computed property definition:
var User = DS.Model.extend({ // ... gravatarURL: function() { var email = this.get('email'), computedMD5 = md5(email); return 'http://www.gravatar.com/avatar/%@.jpg?s=80'.fmt(computedMD5); }.property('email') });
NOTE: Notice that %@ in that string? We are using the String#fmt method, a handy utility that Ember adds. For more info, check out the documentation.
There is no built-in md5 function, so we’ll need to introduce a new dependency to provide one. We could use Node and NPM to require this dependency, but seeing as the rest of our dependencies live in js/vendor we’ll stick with that approach for consistency. First we’ll download an md5 library:
$ curl https://raw.github.com/blueimp/JavaScript-MD5/master/js/md5.js > js/vendor/md5.js
Then require it in the User model:
var md5 = require('../vendor/md5').md5; var User = DS.Model.extend({ // ... }); module.exports = User;
That seems to be enough to turn our test green.
Before we move on from this unit test, let’s cover the case of a blank or undefined email. Gravatar has a special URL for such cases, so let’s test we generate that when appropriate.
test('gravatarURL with blank email', function() { var user; Ember.run(function() { user = store.createRecord('user', { email: '' }); }); equal(user.get('gravatarURL'), 'http://www.gravatar.com/avatar/00000000000000000000000000000000.jpg?s=80'); }); test('gravatarURL with undefined email', function() { var user; Ember.run(function() { user = store.createRecord('user'); }); equal(user.get('gravatarURL'), 'http://www.gravatar.com/avatar/00000000000000000000000000000000.jpg?s=80'); });
Fixing this is relatively straightforward:
var User = DS.Model.extend({ // ... gravatarURL: function() { var email = this.get('email'), computedMD5; if (Ember.isEmpty(email)) { computedMD5 = '00000000000000000000000000000000'; } else { computedMD5 = md5(email); } return 'http://www.gravatar.com/avatar/%@.jpg?s=80'.fmt(computedMD5); }.property('email') });
And we’re green again:
Before we add any more files to our test directory, let’s organize things a little better. We’ll move our model test into test/models, and our acceptance test into test/acceptance. We’ve also got store_test from the introductory chapter. It doesn’t really fit with the acceptance tests—which should really treat the application as a black box—so we’ll place this one in test/integration.
$ mkdir test/{acceptance,integration,models} $ mv test/application_test.js test/acceptance/ $ mv test/store_test.js test/integration/ $ mv test/tickets_test.js test/acceptance/ $ mv test/user_test.js test/models/
Our test directory should now look something like this:
test |-- acceptance | |-- application_test.js | `-- tickets_test.js |-- integration | `-- store_test.js |-- models | `-- user_test.js `-- setup.js
In the previous chapter, we started by listing tickets. Let’s take the same approach with users. Let’s add some fixtures to js/models/user.js so we actually have something to list:
var User = DS.Model.extend({ // ... }); User.FIXTURES = [{ id: 1, firstName: 'Yehuda', lastName: 'Katz', email: 'wycats@gmail.com' }, { id: 2, firstName: 'Tom', lastName: 'Dale', email: 'tom@tomdale.net' }];
With our two fixtures in place, let’s get to the tests. In test/acceptance/users_test.js:
module('Users', { setup: function() { App.reset(); } }); test('listing users', function() { visit('/') .click('a:contains("Users")') .then(function() { ok(find('a:contains("Tom Dale")').length, 'expected to find Tom Dale'); }); });
Next, we’ll add that missing link to the application template:
<li>{{#link-to "index"}}Home{{/link-to}}</li> <li>{{#link-to "tickets"}}Tickets{{/link-to}}</li> <li>{{#link-to "users"}}Users{{/link-to}}</li>
We’ll need a route to go with that link:
var App = require('./app'); App.Router.map(function() { this.resource('tickets', function() { this.resource('ticket', { path: ':ticket_id' }); this.route('new'); }); this.resource('users'); });
And now we have our assertion failure:
So let’s create a template:
$ ember generate -t users created: js/templates/users.hbs
And add the relevant markup:
<div class="row"> <div class="col-md-4"> <nav class="list-group"> {{#each}} <a>{{displayName}}</a> {{/each}} </nav> </div> <div class="col-md-8"> {{outlet}} </div> </div>
NOTE: We’re using an a tag rather than link-to in this case because we want to concentrate on the test at hand and we know that link-to will complain about a missing route.
And create the users route:
$ ember generate -r users created: js/routes/users_route.js
And implement the model hook, just as we did with tickets:
var UsersRoute = Ember.Route.extend({ model: function() { return this.get('store').findAll('user'); } }); module.exports = UsersRoute;
Now let’s step swiftly on to viewing individual users. Back in our test file we’ll add a test to assert that when we click on a user we see their gravatar:
test('viewing user details', function() { visit('/') .click('a:contains("Users")') .click('a:contains("Tom Dale")') .then(function() { ok(find('img[src^="http://www.gravatar.com/avatar/9bf3a766e037b9d5a4da0a6f9d0f4f68.jpg"]').length, 'expected to find gravatar image'); }); });
Note that we’ve calculated Tom’s gravatar URL in advance and we’re asserting that an img with src attribute that begins with that URL (omitting the size param) should appear somewhere in the page. Hopefully this will prove to be the right level of precision for a high-level acceptance test.
Remember in the users template we used plain a tag rather than link-to? Time to rectify that:
<div class="row"> <div class="col-md-4"> <nav class="list-group"> {{#each}} {{#link-to "user" this class="list-group-item"}} {{displayName}} {{/link-to}} {{/each}} </nav> </div> <div class="col-md-8"> {{outlet}} </div> </div>
This leads to a familiar error:
With a familiar fix in js/config/routes.js:
App.Router.map(function() { this.resource('tickets', function() { this.resource('ticket', { path: ':ticket_id' }); this.route('new'); }); this.resource('users', function() { this.resource('user', { path: ':user_id' }); }); });
This brings us back to our failing assertion, so let’s create a user template:
$ ember generate -t user created: js/templates/user.hbs
We’ll borrow most of the markup from ticket.hbs, but place the user’s displayName in the panel title and an img tag with its src bound to the user’s gravatarURL in the panel body:
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> <img {{bind-attr src=gravatarURL alt=displayName}}> </div> </div>
And we’re green!
That’s a pretty minimal user page. Let’s add the user’s email as a link in a dl:
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> <img {{bind-attr src=gravatarURL alt=displayName}}> <dl> <dt>Email</dt> <dd>{{email}}</dd> </dl> </div> </div>
It would be neat if that email were a clickable link, but how exactly would we do that? A mail-to helper seems ideal:
<div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> <img {{bind-attr src=gravatarURL alt=displayName}}> <dl> <dt>Email</dt> <dd> {{#mail-to email=email}} {{email}} {{/mail-to}} </dd> </dl> </div> </div>
Unfortunately, no such helper exists:
There are two ways to implement mail-to: with a helper or with a component. If we want our mail-to to behave like link-to and accept a block, component will prove the more amenable approach.
First we’ll generate the component:
$ ember generate -p mail-to created: js/components/mail_to_component.js created: js/templates/components/mail-to.hbs
We know we want this component to render an a tag with its href set to mailto: plus the value of email, so let’s codify that behavior in a unit test. We’ll add our test in a file called test/components/mail_to_component_test.js:
module('MailToComponent'); test('href', function() { var component = App.MailToComponent.create({ email: 'test@example.com' }); equal(component.get('href'), 'mailto:test@example.com'); });
Making this test pass is pretty straightforward: we’ll just define that computed property in js/components/mail_to_component.js. We’ll also set tagName to a and attributeBindings to ['href']:
var MailToComponent = Ember.Component.extend({ tagName: 'a', attributeBindings: ['href'], href: function() { return 'mailto:' + this.get('email'); }.property('email') }); module.exports = MailToComponent;
This test my seem like testing for the sake of testing, but it does demonstrate how nicely encapsulated the concept of mail-to is by our component. If we wanted to add extra features in future, for example the ability to handle params like subject and body, then we know exactly where to define and test them.
While we’re on the subject of components, let’s revisit our gravatar implementation:
<img {{bind-attr src=gravatarURL alt=displayName}}>
There’s something not quite right about this implementation. First, the size of the avatar image can only be determined in the User model, and second: is this something the user model should really know about at all? Let’s refactor this and create a new gravatar-image component to contain this logic.
We could dive straight into the user template and drive the component out that way, but instead let’s consider this component in isolation. Let’s create a unit test for the component and pretty much copy across the assertions we used early in User. In test/components/gravatar_image_component_test.js:
module('GravatarImageComponent'); test('src with valid email', function() { var component = App.GravatarImageComponent.create({ email: 'tom@example.com' }); equal(component.get('src'), 'http://www.gravatar.com/avatar/e4f7cd8905e896b04425b1d08411e9fb.jpg?s=80'); }); test('src with blank email', function() { var component = App.GravatarImageComponent.create({ email: '' }); equal(component.get('src'), 'http://www.gravatar.com/avatar/00000000000000000000000000000000.jpg?s=80'); });
This leads to the expected failure… Cannot call method 'create' of undefined:
Time to define the component:
$ ember generate -p gravatar-image created: js/components/gravatar_image_component.js created: js/templates/components/gravatar-image.hbs
And copy over the implementation from User, setting up a few other aspects of the component for good measure:
var md5 = require('../vendor/md5').md5; var GravatarImageComponent = Ember.Component.extend({ tagName: 'img', attributeBindings: ['src', 'alt'], src: function() { var email = this.get('email'), computedMD5; if (Ember.isEmpty(email)) { computedMD5 = '00000000000000000000000000000000'; } else { computedMD5 = md5(email); } return 'http://www.gravatar.com/avatar/%@.jpg?s=80'.fmt(computedMD5); }.property('email') }); module.exports = GravatarImageComponent;
We can also remove the {{yield}} from js/templates/components/gravatar-image.hbs.
That’s a good start, but we really want to have control over the size, so let’s add another test for that in test/components/gravatar_image_component_test.js:
test('src with size set', function() { var component = App.GravatarImageComponent.create({ email: 'tom@example.com', size: 512 }); equal(component.get('src'), 'http://www.gravatar.com/avatar/e4f7cd8905e896b04425b1d08411e9fb.jpg?s=512'); });
Let’s try modifying src to take size into account:
var GravatarImageComponent = Ember.Component.extend({ // ... src: function() { var email = this.get('email'), computedMD5; if (Ember.isEmpty(email)) { computedMD5 = '00000000000000000000000000000000'; } else { computedMD5 = md5(email); } return 'http://www.gravatar.com/avatar/%@.jpg?s=%@'.fmt(computedMD5, this.get('size')); }.property('email') });
That gets our new test passing, but breaks the other two:
This is easily remedied by defining a default value for size:
var GravatarImageComponent = Ember.Component.extend({ // ... size: 80, // ... });
And we’re green again. Let’s try plugging our new component into js/templates/user.hbs:
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> {{gravatar-image email=email alt=displayName size=200}} <dl> <dt>Email</dt> <dd> {{#mail-to email=email}} {{email}} {{/mail-to}} </dd> </dl> </div> </div>
The tests all pass and the logic of generating gravtar images is neatly contained within the new component. Let’s complete this little refactoring by removing the unwanted tests and logic relating to user.
We’ll open up test/models/user_test.js and remove all the gravtar related tests, leaving the file looking considerably cleaner:
var container, store; module('User', { setup: function() { container = new Ember.Container(); container.register('store:main', DS.Store); container.register('model:user', App.User); store = container.lookup('store:main'); } }); test('displayName', function() { var user; Ember.run(function() { user = store.createRecord('user', { firstName: 'Tom', lastName: 'Dale' }); }); equal(user.get('displayName'), 'Tom Dale'); });
And perform the same surgery on js/models/user.js:
var User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), email: DS.attr('string'), displayName: function() { return this.get('firstName') + ' ' + this.get('lastName'); }.property('firstName', 'lastName') });
Our test suite is green and our User model is much simpler. Refactorings like this (extracting a complex method into a dedicated class or component) are easy in Ember, thanks to the clearly defined roles and separation of concerns between objects in the framework.
With our refactoring complete, let’s get back to business.
Editing a user will work exactly like editing a ticket: click “Edit”, adjust the details, click “Done”. Let’s add a test to that effect in test/acceptance/users_test.js:
test('editing user details', function() { visit('/users/1') .click('button:contains("Edit")') .fillIn('input[name="firstName"]', 'Tomhuda') .fillIn('input[name="lastName"]', 'Katzdale') .fillIn('input[name="email"]', 'tomster@emberjs.com') .click('button:contains("Done")') .then(function() { ok(find('.list-group-item:contains("Tomhuda Katzdale")').length, 'expected title in master list to update'); ok(find('.panel-title:contains("Tomhuda Katzdale")').length, 'expected title in detail view to update'); ok(find('img[src^="http://www.gravatar.com/avatar/0cf15665a9146ba852bf042b0652780a.jpg"]').length, 'expected gravatar to update'); }); });
This leads us to a familiar failure:
So let’s add the footer control from the ticket template to js/templates/user.hbs:
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> {{gravatar-image email=email alt=displayName size=200}} <dl> <dt>Email</dt> <dd> {{#mail-to email=email}} {{email}} {{/mail-to}} </dd> </dl> </div> <div class="panel-footer"> {{#if isEditing}} <button {{action "done"}} class="btn btn-default">Done</button> {{else}} <button {{action "edit"}} class="btn btn-default">Edit</button> {{/if}} </div> </div>
Another familiar failure:
We’ll create UserRoute to handle the edit action:
$ ember generate -r user created: js/routes/user_route.js
…and implement that edit action:
var UserRoute = Ember.Route.extend({ actions: { edit: function() { this.set('controller.isEditing', true); } } }); module.exports = UserRoute;
Now we’re told the expected inputs cannot be found:
Let’s take the same approach as with tickets and split the user form into its own partial js/templates/users/_form.hbs:
$ ember generate -t users/_form created: js/templates/users/_form.hbs
Which we’ll setup in a similar manner to tickets/_form.hbs:
<div class="panel-heading"> <h3 class="panel-title"> <div class="row"> <div class="col-md-6"> {{input value=firstName name="firstName" placeholder="First name" autofocus=true class="form-control"}} </div> <div class="col-md-6"> {{input value=lastName name="lastName" placeholder="Last name" class="form-control"}} </div> </div> </h3> </div> <div class="panel-body"> {{gravatar-image email=email alt=displayName size=200}} <dl> <dt>Email</dt> <dd> {{input value=email name="email" placeholder="Email" class="form-control"}} </dd> </dl> </div>
Then we’ll adjust js/templates/user.hbs to make use of this partial:
<div class="panel panel-primary"> {{#if isEditing}} {{partial "users/form"}} {{else}} <div class="panel-heading"> <h3 class="panel-title"> {{displayName}} </h3> </div> <div class="panel-body"> {{gravatar-image email=email alt=displayName size=200}} <dl> <dt>Email</dt> <dd> {{#mail-to email=email}} {{email}} {{/mail-to}} </dd> </dl> </div> {{/if}} <div class="panel-footer"> {{#if isEditing}} <button {{action "done"}} class="btn btn-default">Done</button> {{else}} <button {{action "edit"}} class="btn btn-default">Edit</button> {{/if}} </div> </div>
This gets us past the missing inputs failures and on to a new failure which tells us nothing handles the “done” action:
Let’s add that handler to js/routes/user_route.js:
var UserRoute = Ember.Route.extend({ actions: { edit: function() { this.set('controller.isEditing', true); }, done: function() { this.set('controller.isEditing', false); this.modelFor('user').save(); } } });
And we’re green:
Everything seems to be working nicely and our gravatar-image component even automatically updates while editing a user’s email address. Not bad.
This feature starts, as always, with an acceptance test:
test('creating a user', function() { visit('/users') .click('a:contains("New User")') .fillIn('[name="firstName"]', 'Peter') .fillIn('[name="lastName"]', 'Wagenet') .fillIn('[name="email"]', 'peter@tilde.io') .click('button:contains("Save")') .then(function() { ok(find('.list-group-item:contains("Peter Wagenet")').length, 'expected new user to appear in master list'); ok(find('.panel-title:contains("Peter Wagenet")').length, 'expected to see user in the detail view'); ok(find('img[src^="http://www.gravatar.com/avatar/dc9c0271686d50337151a0f862edf3c2.jpg"]').length, 'expected to see gravatar image in detail view'); }); });
Here we’re testing that if we click “New User”, fill in name and email, then click “Save” then we’ll see them appear in the sidebar and see their details in the main view.
The first failure is a straightforward one:
Let’s add the missing link to js/templates/users.hbs:
<div class="row"> <div class="col-md-4"> <nav class="list-group"> {{#each}} {{#link-to "user" this class="list-group-item"}} {{displayName}} {{/link-to}} {{/each}} </nav> {{#link-to "users.new" class="btn btn-primary btn-block"}} New User {{/link-to}} </div> <div class="col-md-8"> {{outlet}} </div> </div>
The next failure informs us, predictably, that we haven’t yet defined the users.new route:
In js/config/routes.js:
var App = require('./app'); App.Router.map(function() { this.resource('tickets', function() { this.resource('ticket', { path: ':ticket_id' }); this.route('new'); }); this.resource('users', function() { this.resource('user', { path: ':user_id' }); this.route('new'); }); });
Our tests complain that the expected input cannot be found:
Let’s generate a template to go with our route:
$ ember generate -t users/new created: js/templates/users/new.hbs
We’ll use our users/form partial and add a couple of action buttons:
<div class="panel panel-primary"> {{partial "users/form"}} <div class="panel-footer clearfix"> <button {{action "save"}} class="btn btn-primary pull-right">Save</button> <button {{action "cancel"}} class="btn btn-default">Cancel</button> </div> </div>
The tests tell us nothing handles save:
So now it’s time to generate UsersNewRoute:
$ ember generate -r users/new created: js/routes/users/new_route.js
We’ll implement save in a similar manner to the save handler in TicketsNewRoute:
var UsersNewRoute = Ember.Route.extend({ model: function() { return {}; }, actions: { save: function() { var attrs = this.get('controller').getProperties( 'firstName', 'lastName', 'email' ); var user = this.get('store').createRecord('user', attrs); var promise = user.save(); this.transitionTo('user', promise); } } }); module.exports = UsersNewRoute;
This completes our implementation and our tests are green again:
Our user creation UI behaves exactly like our using editing UI, complete with auto-updating gravatar:
To round things off, we’ll implement the cancel functionality, starting with a test:
test('cancelling user creation', function() { visit('/users/new') .click('button:contains("Cancel")') .then(function() { equal(find('[name="firstName"]').length, 0, 'expected not to find firstName field'); }); });
The test tells us nothing handle cancel yet:
We switch over to js/routes/users/new_route.js and implement the required handler:
var UsersNewRoute = Ember.Route.extend({ model: function() { return {}; }, actions: { save: function() { var attrs = this.get('controller').getProperties( 'firstName', 'lastName', 'email' ); var user = this.get('store').createRecord('user', attrs); var promise = user.save(); this.transitionTo('user', promise); }, cancel: function() { this.transitionTo('users'); } } });
And our tests are green:
There is a pretty clear functional relationship that exists between users and tickets in every ticket system we’ve ever worked with. Typically a user has either reported a ticket, or is assigned a ticket. This is a simplified view, but it’s enough to get us started for EmberTrackr.
Defining this relationship begins at the model layer. In both cases, if a user reports a ticket or a user is assigned a ticket, we will use DS.belongsTo to describe that relationship. We’ll start by defining those relationships in our ticket model (js/models/ticket.js):
var Ticket = DS.Model.extend({ title: DS.attr('string'), description: DS.attr('string'), status: DS.attr('string'), creator: DS.belongsTo('user', { async: true, inverse: 'ticketsCreated' }), assignee: DS.belongsTo('user', { aysnc: true, inverse: 'ticketsAssigned' }) });
NOTE: We set the async: true option. This tells Ember Data that we’re expecting these related objects to be lazily-loaded. In a lazy-loaded relationship the related object is fetched from the server only when its needed. The alternative is that the data for the related record is guaranteed to be available immediately via side-loading JSON or some other mechanism. We’re not expecting to side-load this data, so we use async: true to ensure these relationships are handled correctly. The fixture adapter actually simulates network latency when loading relationship data, so even though no network requests are being made, we’ll still have to account for this latency up-front.
Now we’ll set these properties in the Ticket fixture data:
Ticket.FIXTURES = [{ id: 1, title: 'Ticket 1', description: 'Sed posuere consectetur est at lobortis.', status: 'New', creator: 1, assignee: 2 }, { id: 2, title: 'Ticket 2', description: 'Sed posuere consectetur est at lobortis.', status: 'New', creator: 2, assignee: 1 }, { id: 3, title: 'Ticket 3', description: 'Sed posuere consectetur est at lobortis.', status: 'New', creator: 1 }];
For things to work correctly, Ember Data requires that we also add the inverse relationship and fixtures. In js/models/user_model.js:
var User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), email: DS.attr('string'), ticketsCreated: DS.hasMany('ticket', { async: true, inverse: 'creator' }), ticketsAssigned: DS.hasMany('ticket', { async: true, inverse: 'assignee' }), displayName: function() { return this.get('firstName') + ' ' + this.get('lastName'); }.property('firstName', 'lastName') }); User.FIXTURES = [{ id: 1, firstName: 'Yehuda', lastName: 'Katz', email: 'wycats@gmail.com', ticketsCreated: [1, 3], ticketsAssigned: [2] }, { id: 2, firstName: 'Tom', lastName: 'Dale', email: 'tom@tomdale.net', ticketsCreated: [2], ticketsAssigned: [1] }];
A quick glance at the fixtures tells us that Ticket 1 should now be created by Yehuda Katz and assigned to Tom Dale. Let’s add a test to see if we can get that information rendering. Since we don’t know exactly what sort of markup to expect yet, we’ll simply verify that the ticket detail view contains links to both creator and assignee.
We already have a test for “viewing tickets” so we’ll add our extra assertions there (test/acceptance/tickets_test.js):
test('viewing ticket details', function() { visit('/') .click('a:contains("Tickets")') .click('a:contains("Ticket 1")') .then(function() { ok(find('*:contains("Sed posuere consectetur est at lobortis.")').length, 'expected to find ticket description'); ok(find('a:contains("Yehuda Katz")').length, 'expected to find ticket creator'); ok(find('a:contains("Tom Dale")').length, 'expected to find ticket assignee'); }); });
We have our assertions in place, so let’s edit the template. We’ll start with the simplest thing that could get the tests passing (js/templates/ticket.hbs):
<div class="panel-body"> {{description}} <h5>Reported by:</h5> {{#link-to "user" creator}}{{creator.displayName}}{{/link-to}} <h5>Assigned to:</h5> {{#link-to "user" assignee}}{{assignee.displayName}}{{/link-to}} </div>
This does indeed get our test passing, but it’s not the most sophisticated UI ever created:
Let’s try using some of Bootstrap’s CSS components to separate these two UI elements and align them on the grid. We may as well include their gravatars too:
<div class="panel-heading"> <h3 class="panel-title"> <span class="badge pull-right">{{status}}</span> {{title}} </h3> </div> <div class="panel-body"> {{description}} </div> <div class="list-group"> <div class="list-group-item"> <div class="row"> <div class="col-md-6"> <h5>Reported by:</h5> {{#link-to "user" creator}} {{gravatar-image email=creator.email size=40 class="img-circle"}} {{creator.displayName}} {{/link-to}} </div> <div class="col-md-6"> <h5>Assigned to:</h5> {{#link-to "user" assignee}} {{gravatar-image email=assignee.email size=40 class="img-circle"}} {{assignee.displayName}} {{/link-to}} </div> </div> </div> </div>
Definitely an improvement. Let’s just edit css/application.css to remove that unwanted margin from the h5 elements:
.panel .list-group-item h5 { margin-top: 0; }
Perfect. Now we just need to make these relationships editable.
We already have an acceptance test for editing tickets, so let’s add an additional action and assertion for modifying user relationships. In our test we view ticket 1, click edit, change some values, click “Done”, then check our changes have stuck. We know from the fixtures that ticket 1 is assigned to Tom, so let’s have the test change the assignee to Yehuda (ID 2), and assert that the change stuck.
test('editing ticket details', function() { visit('/tickets/1') .click('button:contains("Edit")') .fillIn('input[name="title"]', 'Foo Bar') .fillIn('select[name="status"]', 'Open') .fillIn('textarea[name="description"]', 'New description') .fillIn('select[name="assignee"]', '1') .click('button:contains("Done")') .then(function() { ok(find('.list-group-item:contains("Foo Bar")').length, 'expected title in master list to update'); ok(find('.panel-title:contains("Foo Bar")').length, 'expected title in detail view to update'); ok(find('.list-group-item .badge:contains("Open")').length, 'expected status in master list to update'); ok(find('.panel-title .badge:contains("Open")').length, 'expected status in detail view to update'); ok(find('.panel-body:contains("New description")').length, 'expected description to update'); ok(find('a:contains("Yehuda Katz")').length === 2, 'expected Yehuda to be creator and assignee'); }); });
As before, we don’t really want to tie our test to overly specific markup, so we’ll simply assert that Yehuda’s name appears twice; as both creator and assignee.
Our test can’t find a select named “assignee” so let’s append these select controls to the end of js/templates/tickets/_form.hbs:
<div class="list-group"> <div class="list-group-item"> <div class="row"> <div class="col-md-6"> <h5>Reported by:</h5> {{gravatar-image email=creator.email size=40 class="img-circle"}} {{view Ember.Select valueBinding="creatorId" contentBinding="users" optionLabelPath="content.displayName" optionValuePath="content.id" name="creator"}} </div> <div class="col-md-6"> <h5>Assigned to:</h5> {{gravatar-image email=assignee.email size=40 class="img-circle"}} {{view Ember.Select valueBinding="assigneeId" contentBinding="users" optionLabelPath="content.displayName" optionValuePath="content.id" name="assignee"}} </div> </div> </div> </div>
Now we hit our assertion:
This requires a little more unpicking than previous failures. Consider the two properties we’re binding to our Ember.Select.
Value: At the time of writing, there is an incompatibility between Ember Data’s relationship accessors and Ember.Select. As a result, we can’t use the more obvious selection property. We sidestep the issue altogether by binding the select to as-yet-undefined creatorId and assigneeId properties. In order to get the test passing, we’ll need to implement these.
Content: We’re binding the content of these selects to users, which we want to be an array of all available users. We’ll also have to work out how to find these users and make them available to this template.
Let’s start by getting users to the template. We’ll use the needs feature of controllers to declare that TicketController needs UsersController. This makes the singleton instance of UsersController available as controllers.users which we’ll alias to simply users:
var TicketController = Ember.ObjectController.extend({ statuses: ['New', 'Open', 'Closed'], needs: ['users'], users: Ember.computed.oneWay('controllers.users') });
This gives us an interesting error:
Up until now, the users controller has been automatically inferred based on the model presented by our UsersRoute. In this context, we have not visited the users route yet, and Ember has no way to determine what sort of controller we want. To fix this, we simply define our UsersController explicitly:
$ ember generate -c users-> What kind of controller: object, array, or neither? [o|a|n]: a created: js/controllers/users_controller.js
This gets us past the error, but doesn’t answer where the users actually come from. The only place in our app that fetches users from the store right now is the users route, which we may not have visited yet. This is a perfect excuse to make use of Ember’s afterModel route hook. In js/routes/ticket_route.js, after we’ve fetched the model, we’ll fetch all users and assign them as the content for the users controller:
afterModel: function() { var usersController = this.controllerFor('users'); var promise = this.get('store').findAll('user').then(function(users) { usersController.set('model', users); }); return promise; }, actions: { edit: function() { this.set('controller.isEditing', true); }, done: function() { this.set('controller.isEditing', false); this.modelFor('ticket').save(); } } });
That get’s us our users, so now let’s implement creatorId and assigneeId in js/controllers/ticket_controller.js:
var TicketController = Ember.ObjectController.extend({ statuses: ['New', 'Open', 'Closed'], needs: ['users'], users: Ember.computed.oneWay('controllers.users'), creatorId: function(key, value) { if (arguments.length === 1) { return this.get('creator.id'); } else { var user = this.get('users').findBy('id', value); this.set('creator', user); } }.property('creator.id'), assigneeId: function(key, value) { if (arguments.length === 1) { return this.get('assignee.id'); } else { var user = this.get('users').findBy('id', value); this.set('assignee', user); } }.property('assignee.id') });
Our tests are passing again:
And our UI is starting to look pretty sharp:
The only thing left to do is ensure that this functionality is also available in the new-ticket UI. Let’s adjust the test in test/acceptance/tickets_test.js to drive this:
test('creating a ticket', function() ?{ visit('/tickets') .click('a:contains("New Ticket")') .fillIn('[name="title"]', 'My New Ticket') .fillIn('[name="status"]', 'Open') .fillIn('[name="description"]', 'Foo bar baz.') .fillIn('[name="creator"]', '2') .fillIn('[name="assignee"]', '1') .click('button:contains("Save")') .then(function() { ok(find('.list-group-item:contains("My New Ticket")').length, 'expected new ticket to appear in master list'); ok(find('.panel-title:contains("My New Ticket")').length, 'expected to see ticket in the details view'); ok(find('.panel-title:contains("Open")').length, 'expected ticket status to be "Open"'); ok(find('a:contains("Tom Dale")').length, 'expected creator to be Tom'); ok(find('a:contains("Yehuda Katz")').length, 'expected assignee to be Yehuda'); }); });
This fails in the right way:
So now let’s consider our options. We basically need to implement the exact same stuff — fetch the users in afterModel, make them available via needs and provide two computed properties, creatorId and assigneeId. We could just copy and paste all that stuff, but instead let’s move the logic into mixins. We’ll start by moving the afterModel hook into a mixing called PreloadUsers which we’ll mix in to both TicketRoute and TicketsNewRoute.
Create the directory js/mixins and within in it create preloads_users.js:
var PreloadsUsers = Ember.Mixin.create({ afterModel: function() { var usersController = this.controllerFor('users'); var promise = this.get('store').findAll('user').then(function(users) { usersController.set('model', users); }); return promise; } }); module.exports = PreloadsUsers;
Now we can include this mixin in both js/routes/ticket_route.js:
var PreloadsUsers = require('../mixins/preloads_users'); var TicketRoute = Ember.Route.extend(PreloadsUsers, { actions: { edit: function() { this.set('controller.isEditing', true); }, done: function() { this.set('controller.isEditing', false); this.modelFor('ticket').save(); } } });
And js/routes/tickets/new_route.js:
var PreloadsUsers = require('../../mixins/preloads_users'); var TicketsNewRoute = Ember.Route.extend(PreloadsUsers, { model: function() { return {}; }, actions: { save: function() { var attrs = this.get('controller').getProperties( 'title', 'status', 'description' ); var ticket = this.get('store').createRecord('ticket', attrs); var promise = ticket.save(); this.transitionTo('ticket', promise); }, cancel: function() { this.transitionTo('tickets'); } } });
Now we’ll create another mixin in js/mixins/needs_users.js:
var NeedsUsers = Ember.Mixin.create({ needs: ['users'], users: Ember.computed.oneWay('controllers.users'), creatorId: function(key, value) { if (arguments.length === 1) { return this.get('creator.id'); } else { var user = this.get('users').findBy('id', value); this.set('creator', user); } }.property('creator.id'), assigneeId: function(key, value) { if (arguments.length === 1) { return this.get('assignee.id'); } else { var user = this.get('users').findBy('id', value); this.set('assignee', user); } }.property('assignee.id') }); module.exports = NeedsUsers;
We’ll use it in js/controllers/ticket_controller.js:
var NeedsUsers = require('../mixins/needs_users'); var TicketController = Ember.ObjectController.extend(NeedsUsers, { statuses: ['New', 'Open', 'Closed'] }); module.exports = TicketController;
And js/controllers/tickets/new_controller.rb:
var NeedsUsers = require('../../mixins/needs_users'); var TicketsNewController = Ember.ObjectController.extend(NeedsUsers, { statuses: ['New', 'Open'] }); module.exports = TicketsNewController;
And finally we’ll edit js/routes/tickets/new_route.js and ensure that the creator and assignee properties are indeed passed on to the new model:
var TicketsNewRoute = Ember.Route.extend(PreloadsUsers, { // ... actions: { save: function() { var attrs = this.get('controller').getProperties( 'title', 'status', 'description', 'creator', // ensure these two are 'assignee' // passed to the model ); var ticket = this.get('store').createRecord('ticket', attrs); var promise = ticket.save(); this.transitionTo('ticket', promise); }, // ... } });
And we’re green again!
It doesn’t seem completely fair to have Yehuda always selected by default as both creator and assignee, so let’s make use of Ember.Select’s prompt option:
<div class="col-md-6"> <h5>Reported by:</h5> {{gravatar-image email=creator.email size=40 class="img-circle"}} {{view Ember.Select valueBinding="creatorId" contentBinding="users" optionLabelPath="content.displayName" optionValuePath="content.id" prompt="Select a user…" name="creator"}} </div> <div class="col-md-6"> <h5>Assigned to:</h5> {{gravatar-image email=assignee.email size=40 class="img-circle"}} {{view Ember.Select valueBinding="assigneeId" contentBinding="users" optionLabelPath="content.displayName" optionValuePath="content.id" prompt="Select a user…" name="assignee"}} </div>
In this chapter we’ve add users to EmberTrackr, established a relationship between users and tickets, and provided UI to manage all of that.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.