English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing an Ember.js Edge special code

11. Building EmberTrackr - Adding Tickets

What good is an ticket system without tickets? Adding tickets to our app involves several steps. As before, we’re going to drive this feature out using tests. Let’s start with a high-level test for listing tickets.

This first test goes something like this:

Given there are tickets in the store
When a user visits EmberTrackr
And clicks "Tickets"
Then they see the tickets listed

Using this given-when-then syntax helps us identify our initial conditions, the actions the user performs, and the expected result.

First things first: Given there are tickets in the store.

We’re using DS.FixtureAdapter, so our fixture data can be defined on App.Tickets.FIXTURES. We don’t have a model yet, so let’s generate it:

$ ember generate -m ticket
   created:     js/models/ticket.js

Now we’ll define its attributes, and define some fixtures:

var Ticket = DS.Model.extend({
  title: DS.attr('string'),
  description: DS.attr('string'),
  status: DS.attr('string')
});

Ticket.FIXTURES = [{
  id: 1,
  title: 'Ticket 1',
  description: 'Sed posuere consectetur est at lobortis.',
  status: 'New'
}];

module.exports = Ticket;

With our fixture data in place, let’s move on to the test itself, which we’ll define in test/tickets_test.js. The format should be familiar from the previous chapter.

module('Tickets', {
  setup: function() {
    App.reset();
  }
});

test('listing tickets', function() {
  visit('/')
  .click('a:contains("Tickets")')
  .then(function() {
    ok(find('li:contains("Ticket 1")').length,
       'expected to find Ticket 1');
  });
});

The test fails with an informative message, giving us a starting point to implement our feature:

Element a contains tickets not found

Let’s add a “Tickets” link to our main menu in js/templates/application.hbs:

<header class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      {{#link-to "index" class="navbar-brand"}}EmberTrackr{{/link-to}}
    </div>
    <ul class="nav navbar-nav">
      <li>{{#link-to "index"}}Home{{/link-to}}</li>
      <li>{{#link-to "tickets"}}Tickets{{/link-to}}</li>
    </ul>
  </div>
</header>

This gives us a new error:

Routing error

We’re told:

The attempt to link-to route 'tickets.index' failed. The router did not find 'tickets.index' in its possible routes: 'index'

This is another example of the clarity of Ember’s error messages. We’re trying to link to a route called “tickets” but we’ve not yet defined such a route, so let’s do that now in js/config/routes.js:

var App = require('./app');

App.Router.map(function() {
  this.resource('tickets');
});

Now we just have our failing assertion to deal with:

Expected to find Ticket 1

Our test is asserting things about the UI, so let’s start with the template that describes that UI. We don’t yet have a “tickets” template, so let’s make one:

$ ember generate -t tickets
   created:     js/templates/tickets.hbs

Now we’ll add some simple markup:

<ul>
  {{#each}}
    <li>{{title}}</li>
  {{/each}}
</ul>

In this template, we iterate over each ticket in the controller, rendering an li containing the ticket’s title.

This doesn’t make our test pass, but it does give us a new error pointing us in the right direction:

generated tickets controller

The value that #each loops over must be an Array. You passed (generated tickets controller)

Our template was expecting to be able to iterate over an array (or something like an array) but instead it’s been passed an automatically-generated instance of Ember.Controller. Ember knows which type of controller to generate based on the route’s model. Right now, we don’t have a route class, let alone a model for it, so let’s rectify that:

$ ember generate -r tickets
   created:     js/routes/tickets_route.js

Defining our model hook is pretty straightforward:

var TicketsRoute = Ember.Route.extend({
  model: function() {
    return this.get('store').findAll('ticket');
  }
});

module.exports = TicketsRoute;

With all the pieces connected, our tests go green:

Tests go green

Let’s take a look at what we’ve built so far…

Our app so far

It works, but let’s spruce it up a little. First up, let’s fix a nagging little issue with the navbar. Bootstrap assumes the li elements that contain our main menu links will take the active class, but Ember’s link-to helper handles that for us automatically, so let’s add a little CSS to fix this:

.navbar-inverse .navbar-nav > li > a.active,
.navbar-inverse .navbar-nav > li > a:hover,
.navbar-inverse .navbar-nav > li > a:focus {
  color: #fff;
  background-color: #080808;
}

With our nav looking better, let’s give our tickets layout some love. We’ll start by using Bootstrap’s list-group to smarten up our list of tickets.

In js/templates/tickets.hbs:

<ul class="list-group">
  {{#each}}
    <li class="list-group-item">{{title}}</li>
  {{/each}}
</ul>

Let’s also add some additional fixtures to get a better idea of how the app will look with tickets. In js/models/ticket.js:

var Ticket = DS.Model.extend({
  title: DS.attr('string'),
  description: DS.attr('string'),
  status: DS.attr('string')
});

Ticket.FIXTURES = [{
  id: 1,
  title: 'Ticket 1',
  description: 'Sed posuere consectetur est at lobortis.',
  status: 'New'
}, {
  id: 2,
  title: 'Ticket 2',
  description: 'Sed posuere consectetur est at lobortis.',
  status: 'New'
}, {
  id: 3,
  title: 'Ticket 3',
  description: 'Sed posuere consectetur est at lobortis.',
  status: 'New'
}];

module.exports = Ticket;

Improved ticket list

We want our Tickets UI to be a master-detail view with tickets listed on the left and ticket details on the right, so let’s adjust our template in preparation using Bootstrap’s grid system:

<div class="row">
  <div class="col-md-4">
    <ul class="list-group">
      {{#each}}
        <li class="list-group-item">{{title}}</li>
      {{/each}}
    </ul>
  </div>
  <div class="col-md-8">
    {{outlet}}
  </div>
</div>

Grid system in effect

The space over to the right looks a little bare, so let’s add some introductory text. For this we’ll need a new template: tickets/index. We can generate it as follows:

$ ember generate -t tickets/index
   created:     js/templates/tickets/index.hbs

Then we’ll add some markup:

<div class="panel panel-info">
  <div class="panel-heading"><h3 class="panel-title">Tickets</h3></div>
  <div class="panel-body">Select a ticket from the list on the left.</div>
</div>

Index routes are only generated for routes with sub-routes. We’ll be adding a sub-route shortly, so we’ll make way for it now in js/config/routes.js:

var App = require('./app');

App.Router.map(function() {
  this.resource('tickets', function() {

  });
});

Tickets index template

Now we’re getting somewhere. EmberTrackr is starting to look like a real app.

Adding The Ticket Detail View

We can see a basic list of tickets, so let’s add the ability to see the details of a ticket. We’ll start by adding another test to 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');
  });
});

In this test, we visit the home route, click on Tickets then on Ticket 1 then assert that the ticket description has been rendered. The tests run and we see a failure:

Ticket 1 link not found

We’ve not yet turned the list of tickets into links, so let’s start in js/templates/tickets.hbs:

<div class="row">
  <div class="col-md-4">
    <ul class="list-group">
      {{#each}}
        <li class="list-group-item">
          {{#link-to "ticket" this}}{{title}}{{/link-to}}
        </li>
      {{/each}}
    </ul>
  </div>
  <div class="col-md-8">
    {{outlet}}
  </div>
</div>

Now we have a failure telling us the ticket route doesn’t exist:

No ticket route

We’ll define this missing route in js/config/routes.js:

var App = require('./app');

App.Router.map(function() {
  this.resource('tickets', function() {
    this.resource('ticket', { path: ':ticket_id' });
  });
});

Excellent, now we’re getting the correct failure:

Expected to find ticket description

Now we need to add the description property to the appropriate template which, in this case, is js/templates/ticket.hbs. We could just go ahead and create this file by hand, but let’s use the generator to be consistent:

$ ember generate -t ticket
   created:     js/templates/ticket.hbs

We’ll use a bare-bones definition list for the moment:

<dl>
  <dt>Description</dt>
  <dd>{{description}}</dd>
</dl>

The tests runs and… all pass!

5 tests complete

We quickly check in the browser and sure enough:

Ticket on display

That’s very cool but it kinda feels like we skipped a step. Didn’t we need a Route class and a model hook last time?

This is Ember’s convention-driven philosophy in full effect. Let’s step through what happens as the test runs:

  1. visit('/') takes us into the application route, which renders the application template backed by the application controller.
  2. click('a:contains("Tickets")') transitions us into the tickets route, which calls its model hook to fetch tickets then renders the tickets template into the application template’s main {{outlet}} backed by the auto-generated tickets array controller.
  3. click('a:contains("Ticket 1")') transitions us into the ticket route passing along ‘Ticket 1’ as the model (this in {{#link-to "ticket" this}}). The route renders the ticket template into the tickets template’s main {{outlet}} backed by the auto-generated ticket object controller.

If we refresh the page now, everything still works as expected. We didn’t pass the model through from a {{#link-to}} though, so how did Ember find it? Let’s step through again to see, but this time from the point of view of a fresh page load:

  1. The app loads with #/tickets/1 already in the URL bar.
  2. It matches this to the ticket route and begins activating routes as defined in App.Router.
  3. It activates the application route which renders the application template backed by application controller.
  4. It activates the tickets route then calls its model hook to fetch tickets and renders the tickets template into application template’s {{outlet}} backed by the auto-generated tickets array controller.
  5. It activates the ticket route then calls its model hook to fetch the ticket with ID 1 and renders the ticket template into tickets template’s main {{outlet}} backed by the auto-generated ticket object controller.

Note that in step 5, we didn’t write the model hook, since it happens automatically.
The presence of the dynamic component :ticket_id in the route definition for ticket tells Ember how to find the model to match the ID.

Effectively, this route definition...

this.resource('ticket', { path: ':ticket_id' });

…translates to a model hook that looks something like this:

model: function(params) {
  return this.get('store').find('ticket', params.ticket_id);
}

Back to CSS-land, and we realize list-group styling works just fine without ul and li so we can update our test and template accordingly.

Update the selector in the test from li:contains("Ticket 1") to a:contains("Ticket 1"):

test('listing tickets', function() {
  visit('/')
  .click('a:contains("Tickets")')
  .then(function() {
    ok(find('a:contains("Ticket 1")').length,
       'expected to find Ticket 1');
  });
});

And adjust js/templates/tickets.hbs to better use Bootstrap’s styles:

<div class="row">
  <div class="col-md-4">
    <nav class="list-group">
      {{#each}}
        {{#link-to "ticket" this class="list-group-item"}}
          {{title}}
        {{/link-to}}
      {{/each}}
    </nav>
  </div>
  <div class="col-md-8">
    {{outlet}}
  </div>
</div>

Tickets nav looks better

Let’s add some visual structure to the ticket detail view, and make some use of that status attribute. We’ll use Bootstrap’s panel component again, placing the ticket title and status in the heading and description in the body.

In js/templates/ticket.hbs:

<div class="panel panel-primary">
  <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>

We’ll add a small tweak to the CSS so that our status badge is the correct color:

.panel-primary > .panel-heading .badge {
  color: #428bca;
  background-color: #fff;
}

And let’s add status badges to the list of tickets too, in js/templates/tickets.hbs:

{{#each}}
  {{#link-to "ticket" this class="list-group-item"}}
    <span class="badge pull-right">{{status}}</span>
    {{title}}
  {{/link-to}}
{{/each}}

Styled ticket detail view

Editing Tickets

At this point we can see a list of tickets and their details, but what good is a ticket system if you cannot create and edit tickets? In this section we’re going to get familiar with handling actions.

Let’s start with a test for editing a ticket. We don’t know exactly what our UI will be yet, but let’s say when a user navigates to a ticket’s details they can click an “Edit” button, which will allow them to adjust all details of ticket and then click a "Done" button to leave edit mode. We don’t need to go into more detail yet, so let’s add a new test in test/tickets_test.js:

test('editing ticket details', function() {
  visit('/tickets/1')
  .click('button:contains("Edit")')
  .fillIn('input[name="title"]', 'Foo Bar')
  .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');
  });
});

We are asserting that the ticket’s title updates both in the detail view and also in the master list. Right now, though, we’re just getting an error telling us there is no “Edit” button.

Edit button not found

Let’s add one to js/templates/ticket.hbs:

<div class="panel panel-primary">
  <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="panel-footer">
    <button {{action "edit"}}>Edit</button>
  </div>
</div>

Now Ember is telling us nothing handles the "Edit" event.

Nothing handled event "edit"

We have a decision to make as to where we handle this action. We could create TicketController and handle it there, or we could let the action bubble up to TicketRoute and handle it there. At this stage it doesn’t make a lot of difference, but we’re going to opt for the route.

$ ember generate -r ticket
   created:     js/routes/ticket_route.js

We have our route, now let’s add the handler:

var TicketRoute = Ember.Route.extend({
  actions: {
    edit: function() {
      this.set('controller.isEditing', true);
    }
  }
});

module.exports = TicketRoute;

Our action is handled and now we have a new error:

No input named title

Let’s modify js/templates/ticket.hbs to accommodate the new isEditing mode:

<div class="panel panel-primary">
  <div class="panel-heading">
    <h3 class="panel-title">
      {{#if isEditing}}
        {{input value=title name="title"}}
      {{else}}
        <span class="badge pull-right">{{status}}</span>
        {{title}}
      {{/if}}
    </h3>
  </div>
  <div class="panel-body">
    {{description}}
  </div>
  <div class="panel-footer">
    <button {{action "edit"}}>Edit</button>
  </div>
</div>

We’ll leave adding other editable fields for a moment, because we have a new error:

Done button not found

There is no done button, so let’s quickly fix that in the template:

<div class="panel panel-primary">
  <div class="panel-heading">
    <h3 class="panel-title">
      {{#if isEditing}}
        {{input value=title name="title"}}
      {{else}}
        <span class="badge pull-right">{{status}}</span>
        {{title}}
      {{/if}}
    </h3>
  </div>
  <div class="panel-body">
    {{description}}
  </div>
  <div class="panel-footer">
    {{#if isEditing}}
      <button {{action "done"}}>Done</button>
    {{else}}
      <button {{action "edit"}}>Edit</button>
    {{/if}}
  </div>
</div>

Nothing handles done

And it looks like we need to handle that action too, so back to the route:

var TicketRoute = Ember.Route.extend({
  actions: {
    edit: function() {
      this.set('controller.isEditing', true);
    },

    done: function() {
      this.set('controller.isEditing', false);
      this.modelFor('ticket').save();
    }
  }
});

module.exports = TicketRoute;

And with that addition, our tests go green:

Green tests

EmberTrackr with edit functionality

We also want ticket status and description to be editable, so let’s add some extra actions and assertions to our test:

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')
  .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');
  });
});

Select named status not found

There is no select with name status found, so let’s add an Ember.Select to our ticket.hbs template:

<h3 class="panel-title">
  {{#if isEditing}}
    {{view Ember.Select
           selectionBinding="status"
           contentBinding="statuses"
           name="status"
           class="pull-right"}}
    {{input value=title name="title"}}
  {{else}}
    <span class="badge pull-right">{{status}}</span>
    {{title}}
  {{/if}}
</h3>

Notice that we’re binding the input’s content to statuses, but right now that property doesn’t exist anywhere. The ticket controller seems like a reasonable place to add this knowledge, but we don’t yet have a class for it, so let’s make one:

$ ember generate -c ticket
-> What kind of controller: object, array, or neither? [o|a|n]: o
   created:     js/controllers/ticket_controller.js

We’ve created a subclass of Ember.ObjectController — the type Ember has been generating for us automatically up until now. All we need to add is the statues property.

var TicketController = Ember.ObjectController.extend({
  statuses: ['New', 'Open', 'Closed']
});

module.exports = TicketController;

On to the next error and it looks like we’re missing a textarea named "description".

Textarea named description missing

Let’s add that into ticket.hbs:

<div class="panel-body">
  {{#if isEditing}}
    {{textarea value=description name="description"}}
  {{else}}
    {{description}}
  {{/if}}
</div>

…and we’re green again.

Green again

But this UI could do with some attention:

Lacklustre editing UI

Starting with the title bar, we’ll use bootstrap’s grid and form-control features to improve the layout:

<h3 class="panel-title">
  {{#if isEditing}}
    <div class="row">
      <div class="col-md-9">
        {{input value=title
                name="title"
                class="form-control"}}
      </div>
      <div class="col-md-3">
        {{view Ember.Select
               selectionBinding="status"
               contentBinding="statuses"
               name="status"
               class="form-control"}}
      </div>
    </div>
  {{else}}
    <span class="badge pull-right">{{status}}</span>
    {{title}}
  {{/if}}
</h3>

Let’s also use the form-control class on our description textarea and give it an appropriate number of rows:

<div class="panel-body">
  {{#if isEditing}}
    {{textarea value=description
               rows="12"
               name="description"
               class="form-control"}}
  {{else}}
    {{description}}
  {{/if}}
</div>

Finally, let’s make that button consistent:

<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>

After those few tweaks, our UI is looking more polished:

New look

Creating Tickets

Now all that's missing is a create ticket page. Using our given-when-then format from before to sketch this test, we’re after something like this:

Given a user is on the tickets page
When they click "New Ticket"
And fill in the ticket details
And click "Save"
Then they see the ticket appear in the master list
And they see the ticket detail view

Codified as a test:

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.')
  .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');
  });
});

This leads us to our first error:

First error

We need to add a “New Ticket” button somewhere, so let’s add it to our tickets.hbs template after the list:

<div class="row">
  <div class="col-md-4">
    <nav class="list-group">
      {{#each}}
        {{#link-to "ticket" this class="list-group-item"}}
          <span class="badge pull-right">{{status}}</span>
          {{title}}
        {{/link-to}}
      {{/each}}
    </nav>
    {{#link-to "tickets.new" class="btn btn-primary btn-block"}}
      New Ticket
    {{/link-to}}
  </div>
  <div class="col-md-8">
    {{outlet}}
  </div>
</div>

Missing route error

The tests tell us we’re missing a route, so let’s add it to 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');
  });
});

Note we’re using route to define tickets.new rather than resource. In purely conventional terms, this follows the rule that resource should be used for nouns and route for verbs (or adjectives, as in this case) modifying those noun resources. In more practical terms, if we used this.resource('new') we’d end up with a top-level new route, which doesn’t make a lot of sense. We want this new route nested within tickets and we want the nesting to be explicit and refernced as tickets.new, which is why we use route rather than resource.

Missing name=title

The tests tell us no element with name “title” can be found, which makes sense since we’ve not yet created a template.

$ ember generate -t tickets/new
   created:     js/templates/tickets/new.hbs

The UI for new tickets is pretty much identical to the UI for editing tickets and it would be good to re-use what we already have. Partials provide a convenient way to achieve this, so let’s split out of the editable version of our ticket UI into its own template. First we’ll create a new template file (note that underscore):

$ ember generate -t tickets/_form
   created:     js/templates/tickets/_form.hbs

Now we’ll pull over the editable fields from js/templates/ticket.hbs, leaving it looking something like this:

<div class="panel panel-primary">
  {{#if isEditing}}
    {{partial "tickets/form"}}
  {{else}}
    <div class="panel-heading">
      <h3 class="panel-title">
        <span class="badge pull-right">{{status}}</span>
        {{title}}
      </h3>
    </div>
    <div class="panel-body">
      {{description}}
    </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>

And js/templates/tickets/_form like this:

<div class="panel-heading">  <h3 class="panel-title">
    <div class="row">
      <div class="col-md-9">
        {{input value=title
                name="title"
                class="form-control"}}
      </div>
      <div class="col-md-3">
        {{view Ember.Select
               selectionBinding="status"
               contentBinding="statuses"
               name="status"
               class="form-control"}}
      </div>
    </div>
  </h3>
</div>
<div class="panel-body">
  {{textarea value=description
             rows="12"
             name="description"
             class="form-control"}}
</div>

The tests still report only our one known failure, so we can be fairly certain this change has not inadvertently broken other behavior.

Now let’s use this partial in our js/templates/tickets/new.hbs template:

<div class="panel panel-primary">
  {{partial "tickets/form"}}
  <div class="panel-footer">
    <button {{action "save"}} class="btn btn-primary pull-right">Save</button>
  </div>
</div>

That seemed to work, and now we have a new failure:

Nothing responded to save

We need to handle to save action. The ideal place to do this is in the route so let’s generate a route for this purpose:

$ ember generate -r tickets/new
   created:     js/routes/tickets/new_route.js

Before we implement this in full we’ll simply add an empty handler to check that we get a new failure:

var TicketsNewRoute = Ember.Route.extend({
  actions: {
    save: function() {

    }
  }
});

module.exports = TicketsNewRoute;

And sure enough:

The right failure

Now we’re down to just our failing assertions, so let’s implement this handler. Its job is fairly simple: it needs to create a ticket record in the store with properties gathered from the controller. In practice, this looks a bit like this:

var TicketsNewRoute = Ember.Route.extend({
  actions: {
    save: function() {
      var attrs = this.get('controller').getProperties(
        'title',
        'status',
        'description'
      );

      this.get('store').createRecord('ticket', attrs);
    }
  }
});

module.exports = TicketsNewRoute;

This gets us down to one failing assertion:

One failing assertion

Our new ticket is appearing in the listings view, but not in the detail view. This is because we’re not actually doing anything extra after we create our record. In fact, we’re not even saving it! The createRecord method has created the record in memory, but it stills needs to be persisted. So to really complete this process we want to save the ticket and then transition the user to the new ticket’s detail view. Ember Data’s save method returns a promise, and Ember’s router knows all about promises, so we can just hand it straight over:

var TicketsNewRoute = Ember.Route.extend({
  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);
    }
  }
});

module.exports = TicketsNewRoute;

And with that, our tests go green:

7 for 7

With our high-level behaviour verified, let’s open the app in the browser and see how we’re looking.

Slightly broken app

Okay, so there a few things that need our attention. Let’s start with the ticket status select control. We didn’t write an explicit test, and now we notice it’s not being populated with options. Let’s add an assertion to verify it:

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.')
  .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"');
  });
});

Failing status test

So what exactly is going wrong? Well, looking at the template we see that the contentBinding property of the select is set to statuses. In our ticket route, that property comes from TicketController:

var TicketController = Ember.ObjectController.extend({
  statuses: ['New', 'Open', 'Closed']
});

In our new ticket route, however, ticket controller isn’t around, so we need a new controller for this case.

$ ember generate -c tickets/new
-> What kind of controller: object, array, or neither? [o|a|n]: o
   created:     js/controllers/tickets/new_controller.js

This new controller seems to have sent things a bit haywire:

Haywire

Hmm, the failure is:

Cannot delegate set('title', My New Ticket) to the 'content' property of object proxy <App.TicketsNewController:...>: its 'content' is undefined

So it would seem our TicketsNewController is expecting its content property to be defined in order to do anything useful. This makes sense, as it is a subclass of Ember.ObjectController and is designed to proxy all properties to whatever object happens to be bound as content. To fully illuminate the situation, it may help to know that content in this context really means model. We know the place to define a model is in the route, so let’s go ahead and add an empty object to stand in for our new ticket model (in js/routes/tickets/new_route.js):

var TicketsNewRoute = Ember.Route.extend({
  model: function() {
    return {};
  },

  // ...

});

Phew, we’re back to our original assertion failure, but now we have a controller to contain our statuses:

var TicketsNewController = Ember.ObjectController.extend({
  statuses: ['New', 'Open', 'Closed']
});

module.exports = TicketsNewController;

Green again

Status field

And we’re green again!

Having statuses defined in two separate controllers may seem like unnecessary duplication (and it’s certainly not the only way to achieve what we want), but it does have one useful property: if we wanted to display a different set of status options in the new ticket UI, we already have a perfect place to do so. In fact, this probably is something we want to do. It doesn’t make much sense to add a new ticket whose status is ‘Closed’, so let’s remove that ‘Closed’ option for new tickets:

var TicketsNewController = Ember.ObjectController.extend({
  statuses: ['New', 'Open']
});

module.exports = TicketsNewController;

This may seem like a fluke, but in fact solutions such as this naturally present themselves as a consequence of following Ember’s design patterns.

Let’s see what else we can improve about the new ticket UI.

Those empty fields aren’t very informative, let’s open up js/templates/tickets/_form.hbs and add some placeholders:

<div class="panel-heading">
  <h3 class="panel-title">
    <div class="row">
      <div class="col-md-9">
        {{input value=title
                name="title"
                placeholder="Title"
                class="form-control"}}
      </div>
      <div class="col-md-3">
        {{view Ember.Select
               selectionBinding="status"
               contentBinding="statuses"
               name="status"
               class="form-control"}}
      </div>
    </div>
  </h3>
</div>
<div class="panel-body">
  {{textarea value=description
             rows="12"
             name="description"
             placeholder="Description…"
             class="form-control"}}
</div>

And let’s tweak js/templates/tickets/new.hbs to ensure the panel footer contains its buttons correctly:

<div class="panel panel-primary">
  {{partial "tickets/form"}}
  <div class="panel-footer clearfix">
    <button {{action "save"}} class="btn btn-primary pull-right">Save</button>
  </div>
</div>

Looking better

That’s a little better. It would be cool if that title field were focused by default. Let’s try adding that attribtue:

{{input value=title
        name="title"
        placeholder="Title"
        autofocus=true
        class="form-control"}}

Hmm… this doesn’t seem to have any effect. It would appear that Ember’s TextField component doesn’t yet know to bind autofocus to the element’s attribute. Luckily, Ember’s class system is relaxed enough that we can add this sort of functionality without too much fuss. On this occasion, we’ll be making use of the reopen method to extend the class definition for Ember.TextField. There’s not an especially obvious place to put this bit of code, but as it pertains to a view, let’s place it in a file called js/views/ember/text_field.js:

Ember.TextField.reopen({
  attributeBindings: ['autofocus']
});

This does the trick, and now our title field is focused automatically.

For those familiar with class systems in other languages, it may seem odd that we can extend the attributeBindings array like this without clobbering the existing definition. Shouldn’t we be pushing autofocus onto the array? In this regard, Ember has our back thoroughly covered. Two elegant aspects of Ember’s low-level design come into play.

Firstly, reopen is smart enough to preserve the existing definition and mixin/superclass hierarchy. In effect, we’re mixing in a new definition alongside all existing definitions.

Secondly, Ember views collate their attribute bindings by walking said hierarchy. Thanks to this neat bit of framework design, we get a super-simple interface for extending existing view classes.

To round off our ticket-adding functionality, let’s add a “Cancel” button to our new ticket view. In the test, we’ll simply assert that ‘title’ field is no longer present:

test('cancelling ticket creation', function() {
  visit('/tickets/new')
  .click('button:contains("Cancel")')
  .then(function() {
    equal(find('[name="title"]').length, 0,
          'expected not to find title field');
  });
});

No cancel button

As expected, our test complain that no ‘Cancel’ button could be found. Let’s add one to js/templates/tickets/new.hbs:

<div class="panel panel-primary">
  {{partial "tickets/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>

Nothing handled cancel

The next failure is also to be expected, we’re not handling the cancel action anywhere, so let’s do so in the route:

var TicketsNewRoute = Ember.Route.extend({
  model: function() {
    return {};
  },

  actions: {
    // ...

    cancel: function() {
      this.transitionTo('tickets');
    }
  }
});

module.exports = TicketsNewRoute;

And with that, our tests go green.

8 for 8

Wrapping Up

In this chapter we’ve implemented the fundamentals of listing, viewing, editing and creating tickets in EmberTrackr with a suite of acceptance tests to keep us honest.

In the next chapter we’ll move on to user management, authentication and object relationships.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.