In this chapter, we’ll look at two ways of handling nested data in API payloads. The first approach involves defining a model attribute without a transform. The second approach makes uses of embedded records. Let's dive in!
Declaring Attributes Without Transforms
Let’s say we have the following JSON for a
contact resource with
address being a nested object:
{
"id": 1,
"name": "Richard Hendrix",
"address": {
"street": "123 Main St.",
"zip": "90003"
}
}
In Chapter
1 – Ember Data Overview, we learned about the four different
transforms:
string,
number,
boolean, and
date. There is no object transform. In order to have an
address attribute on the model that contains an object, we can declare the attribute without a transform:
app/models/contact.js
import Model, { attr } from '@ember-data/model';
export default class ContactModel extends Model {
@attr('string') name;
@attr address;
}
When we don’t specify a transform, Ember Data will just pass through the value and set it on the model.
To change a specific property on
address, use dot notation with
model.set()
:
model.set('address.street', '1234 New St.');
Now let’s say we have the following JSON for a
contact resource
:
{
"id": 1,
"name": "Richard Hendricks",
"history": [
{ "url": "http://piedpiper.com", "time": "2015-10-01T20:12:53Z" },
{ "url": "http://hooli.com", "time": "2014-10-01T20:12:53Z" },
{ "url": "http://endframe.com", "time": "2013-10-01T20:12:53Z" }
]
}
There is a
history property containing an array of URLs. We won’t specify a transform on the model so the history data will be passed through and set on the record:
app/models/contact.js
import Model, { attr } from '@ember-data/model';
export default class ContactModel extends Model {
@attr('string') name;
@attr history;
}
Let’s see how we can work with the
history
attribute. You might think we could modify a history item and expect the UI to update:
model.history[0].url = 'http://amazon.com';
However, this won’t work. If we need to modify a specific history item, we will need to use
set, for example:
import { set } from '@ember/object';
let googleItem = model.history[0];
set(googleItem, 'url', 'http://amazon.com');
Using
set() will change the property and notify Ember to rerender. Alternatively, we can create a new array reference and reassign the
history attribute:
let modifiedHistory = [...model.history, 'http://amazon.com'];
model.set('history', modifiedHistory);
Embedded Records
Nested objects with an
id can also be treated as
records
using the mixin
EmbeddedRecordsMixin. Let’s assume the JSON now looks like this:
{
"id": 1,
"name": "Richard Hendricks",
"skills": [
{ "id": 1, "name": "Compression" },
{ "id": 2, "name": "Java" },
{ "id": 3, "name": "Algorithms" }
]
}
We can turn each object under
skills into records with a
hasMany relationship
established between
contact and
skill:
app/models/contact.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class ContactModel extends Model {
@attr('string') name;
@hasMany('skill') skill;
}
To have Ember Data establish the
hasMany relationship, we can use the
EmbeddedRecordsMixin in our
serializer:
app/serializers/contact.js
import JSONSerializer from '@ember-data/serializer/json';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class ContactSerializer extends JSONSerializer.extend(
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. Note that EmbeddedRecordsMixin does not work with the JSONAPISerializer.
EmbeddedRecordsMixin
also works with nested data inside of nested data! For example, let’s say each
skill now has an embedded
category model
:
{
"id": 1,
"name": "Richard Hendricks",
"skills": [
{
"id": 1,
"name": "Compression",
"category": {
"id": 3,
"name": "Technology"
}
},
{
"id": 2,
"name": "Algorithms",
"category": {
"id": 6,
"name": "Technology"
}
}
]
}
Similar to the preceding code, create a
category model
and specify the relationship:
app/models/skill.js
import Model, { attr, belongsTo } from '@ember-data/model';
export default class SkillModel extends Model {
@attr('string') name;
@belongsTo('category', { async: false }) category;
}
app/models/category.js
import Model, { attr } from '@ember-data/model';
export default class CategoryModel extends Model {
@attr('string') name;
}
Next, create a
skill serializer that uses
EmbeddedRecordsMixin
:
app/serializers/skill.js
import RESTSerializer, {
EmbeddedRecordsMixin
} from '@ember-data/serializer/rest';
export default class SkillSerializer extends RESTSerializer.extend(
EmbeddedRecordsMixin
) {
attrs = {
category: { embedded: 'always' }
};
}
Nested models can recursively use the EmbeddedRecordsMixin to handle records nested in records.
Summary
In this chapter, we looked at a few different ways of handling nested data and embedded records. If we need nested data to be turned into a record, use the EmbeddedRecordsMixin. Otherwise, declare the attribute without a transform.
Up until now, we’ve looked at how to work with successful API responses. In the next chapter, we will look at how to handle error responses.