Skip to content

tobymarsden/ember-data.model-fragments

 
 

Repository files navigation

Ember Data: Model Fragments Build Status

This package provides support for sub-models that can be treated much like belongsTo and hasMany relationships are, but whose persistence is managed completely through the parent object.

Example

App.Person = DS.Model.extend({
  name      : DS.hasOneFragment('name'),
  addresses : DS.hasManyFragments('address'),
  titles    : DS.hasManyFragments()
});

App.Name = DS.ModelFragment.extend({
  first  : DS.attr('string'),
  last   : DS.attr('string')
});

App.Address = DS.ModelFragment.extend({
  street  : DS.attr('string'),
  city    : DS.attr('string'),
  region  : DS.attr('string'),
  country : DS.attr('string')
});

With a JSON payload of:

{
  "id": "1",
  "name": {
    "first": "Tyrion",
    "last": "Lannister"
  },
  "addresses": [
    {
      "street": "1 Sky Cell",
      "city": "Eyre",
      "region": "Vale of Arryn",
      "country": "Westeros"
    },
    {
      "street": "1 Tower of the Hand",
      "city": "King's Landing",
      "region": "Crownlands",
      "country": "Westeros"
    }
  ],
  "titles": [ "Imp", "Hand of the King" ]
}

The name attribute can be treated similar to a belongsTo relationship:

var person = store.getById('person', '1');
var name = person.get('name');

person.get('isDirty'); // false
name.get('first'); // 'Tyrion'

name.set('first', 'Jamie');
person.get('isDirty'); // true

person.rollback();
name.get('first'); // 'Tyrion'

The addresses attribute can be treated similar to a hasMany relationship:

var person = store.getById('person', '1');
var address = person.get('addresses.lastObject');

person.get('isDirty'); // false
address.get('country'); // 'Westeros'

address.set('country', 'Essos');
person.get('isDirty'); // true

person.rollback();
address.get('country'); // 'Westeros'

The titles attribute can be treated as an Ember.Array:

var person = store.getById('person', '1');
var titles = person.get('titles');

person.get('isDirty'); // false
titles.get('length'); // 2

titles.pushObject('Halfman');
titles.get('length'); // 3
person.get('isDirty'); // true

person.rollback();
titles.get('length'); // 2

Polymorphism

Ember Data: Model Fragments has support for reading polymorphic fragments. To use this feature, pass an options object to hasOneFragment or hasManyFragments with polymorphic set to true. In addition the typeKey can be set, which defaults to 'type'.

The typeKey's value must be the lowercase name of a class that is assignment-compatible to the declared type of the fragment attribute. That is, it must be the declared type itself or a subclass.

In the following example the declared type of animals is animal, which corresponds to the class App.Animal. App.Animal has two subclasses: App.Elephant and App.Lion, so to typeKey's value can be 'animal', 'elephant' or 'lion'.

App.Zoo = DS.Model.extend({
  name: DS.attr("string"),
  city: DS.attr("string"),
  animals: DS.hasManyFragments("animal", { polymorphic: true, typeKey: '$type' }),
});

App.Animal = DS.ModelFragment.extend({
  name: DS.attr("string"),
});

App.Elephant = Animal.extend({
  trunkLength: DS.attr("number"),
});

App.Lion = Animal.extend({
  hasManes: DS.attr("boolean"),
});

The expected JSON payload is as follows:

{
  "Zoo" : {
    "id" : "1",
    "name" : "Winterfell Zoo",
    "city" : "Winterfell",
    },
    "animals" : [ {
      "$type" : "lion",
      "name" : "Simba",
      "hasManes" : false
    }, {
      "$type" : "lion",
      "name" : "Leonard",
      "hasManes" : true
    }, {
      "$type" : "elephant",
      "name" : "Trunky",
      "trunkLength" : 10
    }, {
      "$type" : "elephant",
      "name" : "Snuffles",
      "trunkLength" : 9
    } ]
  }
}

Serializing the typeKey back to JSON is currently not supported.

Limitations

Conflict Resolution

There is a very good reason that support for id-less embedded records has not been added to Ember Data: merging conflicts is very difficult. Imagine a scenario where your app requests a record with an array of simple embedded objects, and then a minute later makes the same request again. If the array of objects has changed – for instance an object is added to the beginning – without unique identifiers there is no reliable way to map those objects onto the array of records in memory.

This plugin handles merging fragment arrays by swapping out the data of existing fragments. For example, when a record is fetched with a fragment array property, a fragment model is created for each object in the array. Then, after the record is reloaded via reload or save, the data received is mapped directly onto those existing fragment instances, adding or removing from the end when necessary. This means that reordering the array will cause fragment objects' data to swap, rather than simply reordering the array of fragments in memory. The biggest implication of this behavior is when a fragment in a fragment array is dirty and the parent model gets reloaded. If the record is then saved, the change will likely affect the wrong object, causing data loss. Additionally, any time a reference to a model fragment is held onto, reloading can give it a completely different semantic meaning. If your app does not persist models with fragment arrays, this is of no concern (and indeed you may wish to use the DS.EmbeddedRecordMixin instead).

Filtered Record Arrays

Another consequence of id-less records is that an ID map of all fragment instances of a given type is not possible. This means no store.all('<fragment_type>'), and no ability to display all known fragments (e.g. names or addresses) without iterating over all owner records and manually building a list.

Relationships to Models

Currently, fragments cannot have normal DS.belongsTo or DS.hasMany relationships. This is not a technical limitation, but rather due to the fact that relationship management in Ember Data is in a state of flux and would require accessing private (and changing) APIs.

Testing

Building requires Grunt and running tests requires Test 'Em, which can both be installed globally with:

$ npm install --global grunt testem

Then install NPM packages, build the plugin, and start the development test server:

$ npm install
$ grunt build
$ testem

Contributing

When reporting an issue, follow the Ember guidelines. When contributing features, follow Github guidelines for forking and creating a new pull request. All existing tests must pass (or be suitably modified), and all new features must be accompanied by tests to be considered.

About

Add on to Ember Data to support nested JSON documents

Resources

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
license.js

Stars

Watchers

Forks

Packages

No packages published