cult3

Adding related models to an Ember Application

Jul 13, 2015

Table of contents:

  1. Writing the Test
  2. Updating the Template
  3. Creating the Models
  4. Creating a Serializer
  5. Updating the Mock Server
  6. Create the Reply Factory and Fixtures
  7. Conclusion

Last week we introduced Ember CLI Mirage to mock the API server. By mocking the API server it means we can effectively work with the Ember application in isolation during development and testing.

When it comes to shipping the application to production, we can just switch out the mock for the real server.

Last week we started to get the data from the “mock” server. An important part of the application we are building is the related “replies” to a task.

In today’s tutorial we will be looking at how we can use TDD to get to the point where we are also returning the replies for each task.

Writing the Test

We are using TDD to develop this application and so the first thing we need to do is to write the test:

test("task has replies", function (assert) {
  server.createList("task", 3);
  server.createList("reply", 2, { task_id: 1 });

  visit("/");
  andThen(function () {
    var replies = find(".task-list .task:eq(0) .task-replies");
    assert.equal(replies.text(), 2);
  });
});

In this test we’re grabbing the first task from the task list and then asserting that it has the correct number of replies.

If you run the test it should fail.

Updating the Template

The first thing we need to do is to update the template to display the number of task replies. To do this we will add a new span tag:

<span class="task-replies"></span>

Inside the span tag I’m going to access the replies property and then call the length method.

<span class="task-replies">{{task.replies.length}}</span>

In theory this will work because replies should be an array of replies as it should represent the Has Many relationship between Tasks and Replies.

However, if you run the test, it should still fail.

Creating the Models

So to access the replies of a Task, we need to update the Ember Data model with the relationship:

import DS from "ember-data";

export default DS.Model.extend({
  title: DS.attr("string"),
  status: DS.attr("string"),
  replies: DS.hasMany("reply"),
});

As you can see, we can implement the relationship using DS.hasMany(). If this is new to you, check out my tutorial on Ember Data.

Next we need to generate the Reply model:

ember g model reply

For now I’m just going to implement the reverse side of the relation to keep things simple:

import DS from "ember-data";

export default DS.Model.extend({
  task: DS.belongsTo("task"),
});

As a side note, you will need to update the unit tests to make them aware of the related models. For example, you need to update this block in the task-test.js unit file:

moduleForModel("task", "Unit | Model | task", {
  // Specify the other units that are required for this test.
  needs: ["model:reply"],
});

Creating a Serializer

In order to include replies as part of the task JSON, I’m going to use the JSON API approach of embedding the ids.

Ember Data uses serialisers as a way of providing an agnostic interface that can then be customised. In order to tell Ember Data to expect the replies, we need to generate a Task Serializer:

ember g serializer task

This will create a file called task.js under app/serializers.

Next we need to update the standard Serializer to look like this:

import DS from "ember-data";

export default DS.RESTSerializer.extend({
  attrs: {
    replies: { serialize: "ids" },
  },
});

Updating the Mock Server

Last week we looked at using Ember CLI Mirage to return data from the mock server. In order to also return the replies, we’re going to have to tweak the work from last week a bit.

So for each task, we also need to grab the replies. I’m going to assume that whenever a task is returned, the replies should also be returned:

export default function () {
  this.get("/api/tasks", function (db, request) {
    var tasks, replies, related;

    tasks = db.tasks.where(request.queryParams);

    replies = [];

    for (var i = 0; i < tasks.length; i++) {
      related = db.replies.where({ task_id: tasks[i].id });

      tasks[i].replies = related.map(function (reply) {
        return reply.id;
      });

      replies = replies.concat(related);
    }

    return { tasks: tasks, replies: replies };
  });
}

In this updated endpoint we grab all of the tasks as we did last week. Next we iterate over the task list and grab the replies for each task. Finally we return the tasks and replies as a JSON object.

You might want to do something fancy with a the included query parameter to only return the replies if they are requested. I’m going to keep it simple and just return them regardless.

Create the Reply Factory and Fixtures

Finally we also need to create the Factory and Fixtures files to make hitting the endpoint in development and during testing actually work.

First we can create the fixtures file as we did with the tasks fixtures:

export default [
  {
    id: 1,
    comment: "Hello world",
    task_id: 1,
  },
  {
    id: 2,
    comment: "Hello world",
    task_id: 1,
  },
  {
    id: 3,
    comment: "Hello world",
    task_id: 1,
  },
];

As you can see this is simply an array of JSON objects.

Next we can create the Factory:

import Mirage from "ember-cli-mirage";

export default Mirage.Factory.extend({
  task_id: function (i) {
    return i;
  },
});

Again this is fairly simple at this stage. Here I’m setting the task_id to be i. When I actually use this Factory during the tests I will be more than likely overriding this value anyway.

Finally we can use the Factory to create the data during the test:

Now if you run the test again you should see it pass!

Conclusion

Returning all of the replies with a task could be inefficient if the number of replies for each task becomes a big number. It might be better to simply return an integer value, and then only get the replies for a single task.

In either case, we’ve covered a lot to do with related models and returning related data from Ember Mirage.

Hopefully this was a good example of building upon the basic Ember Data things you see in tutorials to see how it would actually work in real life.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.