Working With Nested Data In Ember Data Models

Friday, January 29th, 2016

In today’s post, I’d like to share a few ways of how you can work with nested data in Ember Data models.

1. Nested Objects

Let’s say you have the following JSON for a user:

{
  "id": 5,
  "name": "David",
  "address": {
    "street": "123 Main St.",
    "zip": 90003
  }
}

You might not need address to be its own model. If that’s the case, don’t specify a transform when declaring the address attribute on the model:

// app/models/user.js
export default DS.Model.extend({
  name: DS.attr('string'),
  address: DS.attr()
});

By not using a transform for address (nothing is passed to DS.attr()), Ember Data will just pass through the data and set it on the model. To change a specific property on the address, you can do:

model.set('address.street', '1234 New St.');

2. Nested Arrays

Now let’s say you have the following JSON for a user.

{
  "id": 5,
  "name": "David",
  "history": [
    { "url": "http://google.com", "time": "2015-10-01T20:12:53Z" },
    { "url": "http://apple.com",  "time": "2014-10-01T20:12:53Z" },
    { "url": "http://yahoo.com",  "time": "2013-10-01T20:12:53Z" }
  ]
}

This time it has a history property containing an array of items. The model won’t use any transform so the history data will be passed through and set on the model.

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

Similar to before, maybe each history item doesn’t need to be its own model. If that’s the case, there are two ways to work with the data. You might think you could modify a history item like below and expect the UI to update, especially if you are coming from the Angular world:

model.get('history')[0].url = 'http://amazon.com';

However, this won’t work. If you need to modify a specific history item, you will need to use Ember.set. For example:

let googleItem = model.get('history')[0];
Ember.set(googleItem, 'url', 'http://amazon.com');

Using Ember.set() will change the property and notify Ember to rerender.

Alternatively, you can change the entire array, like if you were using map() or filter() on Array.prototype, and reassign the history attribute.

model.set('history', modifiedHistory);

3. Embedded Records

The last approach to work with nested data is with embedded records using the mixin DS.EmbeddedRecordsMixin. Let’s assume the JSON now looks like this:

{
  "id": 5,
  "name": "David",
  "skills": [
    { "id": 2, "name": "JavaScript" },
    { "id": 6, "name": "PHP" },
    { "id": 9, "name": "Ember" }
  ]
}

Now you want each object under skills to be its own model and there to be a hasMany relationship between user and skill.

// app/models/user.js
export default DS.Model.extend({
  name: DS.attr('string'),
  skills: DS.hasMany('skill')
});

To have Ember Data create the hasMany relationship, use the DS.EmbeddedRecordsMixin in your serializer.

// app/serializers/user.js
export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    skills: { embedded: 'always' }
  }
});

In the attrs property, set skills to { embedded: 'always' }. This also works for a belongsTo relationship. This example is using the JSONSerializer but the same technique can apply to an API based on the RESTSerializer. However, this does not work with the JSONAPISerializer at the time of this writing when I used Ember Data 2.3.3.

The EmbeddedRecordsMixin also works with nested data inside of nested data! For example, let’s say each skill now has an embedded category model:

{
  "id": 5,
  "name": "David",
  "skills": [
    {
      "id": 2,
      "name": "Teaching",
      "category": {
        "id": 3,
        "name": "Education"
      }
    },
    {
      "id": 9,
      "name": "Ember",
      "category": {
        "id": 8,
        "name": "Technology"
      }
    }
  ]
}

Similar to above, create a category model and specify the relationship:

// app/models/skill.js
export default DS.Model.extend({
  name: DS.attr('string'),
  category: DS.belongsTo('category', { async: false })
});
// app/models/category.js
export default DS.Model.extend({
  name: DS.attr('string')
});

Then, create a skill serializer and use the EmbeddedRecordsMixin:

// app/serializers/skill.js
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    category: { embedded: 'always' }
  }
});

Interested in learning more about Ember Data and how to use it with any API? Check out my book Ember Data in the Wild - Getting Ember Data to Work With Your API.


Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.

comments powered by Disqus