/ Ember.JS

Using Ember.js Lesson 1

This will be the first in a series of articles in developing a client manager app using Ember.js.

For this tutorial I am using the latest ember-cli v2.11. For installing the latest version you will want to follow the getting started section at https://ember-cli.com/user-guide/#getting-started.

Generate and Setup Project

Navigate to the directory you plan on installing the new app and run the following ember command to create the new project.
$ ember new client-manager

Once the install is complete go into the new project directory and start the ember server.

$ ember server

Once the server starts open a browser and go to the default url http://localhost:4200. You should see a "Congratulations, you made it" page.

Lets start by removing the congratulations page. Open app/templates/application.hbs

/*app/templates/application.hbs*/
{{!-- The following component displays Ember's default welcome message. --}}
{{welcome-page}}
{{!-- Feel free to remove this! --}}

{{outlet}}

Remove the {{welcome-page}} and the two comments before and after. Lets also remove the ember add-on ember-welcome-page.

$ npm uninstall ember-welcome-page --save-dev

Acceptance Test

With the project base now ready lets start on our first feature, displaying a list of customers. We will be using a top down test driven approach when creating new features. So lets create our first acceptance test.
$ ember g acceptance-test clients/index

The above command will generate a new acceptance test at tests/acceptance/clients/index-test.js. The g argument is just a short cut for generate.

An acceptance test allows you to test the application in the same manner as a user traversing the site, by clicking links and filling out forms. The downside to this is that each acceptance test loads up your ember app increasing the time for your tests to complete.

Lets take a look at what is currently in tests/acceptance/clients/index-test.js

//tests/acceptance/clients/index-test.js
import { test } from 'qunit';
import moduleForAcceptance from 'client-manager/tests/helpers/module-for-acceptance';

moduleForAcceptance('Acceptance | clients/index');

test('visiting /clients/index', function(assert) {
  visit('/clients/index');

  andThen(function() {
    assert.equal(currentURL(), '/clients/index');
  });
});

The generate command created a sample test 'visiting /clients/index'. Lets make the follow changes to that test.

//tests/acceptance/clients/index-test.js
test('show client list', function(assert) {
  visit('/');
  click('a[href="/clients"]');

  andThen(function() {
    assert.equal(find('.client').length, 3, "The number of clients did not match.");
  });
});

Lets take apart what the test does.

  • The visit('/'); line calls the visit helper which goes to the specified route /, which is the root of our application.
  • The click('a[href="/clients"]' line calls the 'click' helper which clicks an element on the page, which for our test will click a hyperlink that has a href="/clients"
  • The andThen helper waits for the previous asynchronous helpers to complete and than proceeds with to call to assert.equal which checks if the find('.client').length equals 3.
  • The find helper looks for the specified element on the page which for our example is looking for a class called client and the length method returns how many where found.

Lets run our test.

$ ember test --server

This will run our tests on every change and display them in the browser.

As you can see our test failed on

Error: Element a[href="/clients"] not found.
Expected: true
Result: false

This failed because we don't have the client link on our root page. Lets fix this by running the following ember generator.

Add Route

$ ember g route clients/index
  • This will create a template file at app/templates/clients/index.hbs.
  • A route file at app/routes/clients/index.js
  • And add a route entry in app/router.js.

Lets take a look at what was added to the app/router.js.

this.route('clients', function(){
});

This will add a route for our client which also includes the index path.

In the app/templates/application.hbs file add the following to the top of the file.

{{link-to "Clients" "clients"}}

The link-to is a helper function that will create a hyperlink. In our examples the first argument is the link text and the second argument is the route.

With the changes we just made it now looks like our test is now failing at.

The number of clients did not match.
Expected: 3
Result: 0

Its failing since its looking for a list of 3 clients on the clients/index page which is currently empty.

Add Model

To get the new error to pass lets first create our client model.

$ ember g model client firstName:string lastName:string

We are passing two string fields as arguments to create the firstName and lastName attributes. The will create a model file at app/models/client.js

//app/models/client.js
import DS from 'ember-data';

export default DS.Model.extend({
  firstName: DS.attr('string'),
  lastName: DS.attr('string')
});

Now that we have our client model we can add the model to our client route handler.

Make the following changes to the app/routes/clients/index.js

// app/routes/clients/index.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.store.findAll('client');
  }
});

this.store.findAll('client') will call a GET request to /clients. But at the moment we don't have a backend that serves that request and you should see a second new error.

Error: Ember Data Request GET /clients returned a 404

Install ember-cli-mirage

To fix our request error we will use the ember add-on ember-cli-mirage. Mirage will allow us to mock our API calls.
$ ember install ember-cli-mirage

Mirage will have created a config file at mirage/config.js. Add the following changes to mock our API routes.

//mirage/config.js
export default function() {
  this.namespace = '/api';

  this.get('/clients');
  this.post('/clients');
  this.get('/clients/:id');
  this.put('/clients/:id');
  this.del('/clients/:id');
}

Next will create an adapter.

$ ember g adapter application

And modify app/adapters/application.js to add our namespace.

//app/adapters/application.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  namespace: 'api'
});

Next will generate our mirage client model

$ ember generate mirage-model client

Which generates mirage/models/client.js

//mirage/models/client.js
import { Model } from 'ember-cli-mirage';

export default Model.extend({
});

This model is used in a in-memory database.

The last step is to create a factory that we can use to seed some test data for our tests.

$ ember g mirage-factory client

This will have generated a new file at mirage/factories/client.js. Make the following changes.

//mirage/factories/client.js
import { Factory } from 'ember-cli-mirage';

export default Factory.extend({
  firstName: 'User',
  lastName(i) {
    return i;
  }
});

We are assigning User for the firstName attribute and for the lastName attribute we our dynamically assigning the value using the i argument from the function.

If you are currently running your tests with auto refresh
you might want to stop and start it up again for some of mirage changes to take affect.

Get Acceptance Test to Pass

Now that we have our factory we can modify our show client list test to add server.createList('client', 3); which will seed our client model with 3 records.

//tests/acceptance/clients/index-test.js
test('show client list', function(assert) {
  server.createList('client', 3);

  visit('/');
  click('a[href="/clients"]');

  andThen(function() {
    assert.equal(find('.client').length, 3, "The number of clients did not match.");
  });
});

The last step is to modify the app/templates/clients/index.hbs and add the following changes to display our client records.

/*app/templates/clients/index.hbs*/
<ul>
  {{#each model as |client|}}
    <li class="client">{{client.firstName}} {{client.lastName}}</li>
  {{/each}}
</ul>

Hooray our test should be passing now! Our next step should be to refactor the code we just added to show the client list app/templates/clients/index.hbs into its own component, which will do in the next tutorial.

You can view the source to the current project on GitHub.