Created by Luke Galea / @lukegalea
Nutrition and fitness coaching
"We use ED Polymorphic assocations a lot." - Me
What are polymorphic associations?
Person = DS.Model.extend({
name: attr('string'),
neckbeard: true
});
Average ember developer
Person = DS.Model.extend({
name: attr('string'),
neckbeard: true,
pets: hasMany('pet', { polymorphic: true })
});
Person = DS.Model.extend({
name: attr('string'),
neckbeard: true,
pets: hasMany('pet', { polymorphic: true })
});
Pet = DS.Model.extend({
owner: belongsTo('person')
});
Cat = Pet.extend({
lives: attr('number', {defaultValue: 9})
});
App.Person = DS.Model.extend({
name: attr('string'),
neckbeard: true,
pets: hasMany('pet', { polymorphic: true })
});
Pet = DS.Model.extend({
owner: belongsTo('person')
});
Cat = Pet.extend({
lives: attr('number', {defaultValue: 9})
});
Dog = Pet.extend({
fleas: attr('number', {defaultValue: 0})
});
App.Person = DS.Model.extend({
name: attr('string'),
neckbeard: true,
pets: hasMany('pet', { polymorphic: true })
});
Pet = DS.Model.extend({
owner: belongsTo('person')
});
Cat = Pet.extend({
lives: attr('number', {defaultValue: 9})
});
Dog = Pet.extend({
fleas: attr('number', {defaultValue: 0})
});
HoneyBadger = Pet.extend({
viciousness: attr('number', {defaultValue: 0})
});
As of Ember-Data 1.0b11
a common base class is required
App.Pet = DS.Model.extend({
...
});
App.Cat = App.Pet.extend({
...
});
App.Dog = App.Pet.extend({
...
});
There's a Pull Request open to allow unrelated polymorphic types
Assuming that covers the ember side, how do you actually wire it up with a backend?
{
"drawing":{
"id":1,
"title":"A house",
"shapes":[
{
"id":1,
"type":"Rectangle"
},
{
"id":2,
"type":"Rectangle"
},
{
"id":3,
"type":"Triangle"
}
]
}
}
Or for a belongsTo
{
"dog":{
...
"owner": {
"id":1,
"type":"Human"
}
}
class DrawingSerializer < ActiveModel::Serializer
attributes :id, :title, :shapes
def shapes
object.shapes.map { |e| { id: e.id, type: e.type } }
end
end
belongsTo
class DogSerializer < ActiveModel::Serializer
...
def owner
{ id: object.owner.id, type: object.owner.type } if object.owner
end
end
There's a guide that shows how to backport polymorphic true from 0.9 to 0.8
Assuming you've wired it all up, everything should be good!
Cool Stuff
Composing views using polymorphic models
A student curriculum is composed of many types of Cards.
A Card is composed of many primitive elements.
A drawing is made of many shapes:
App.Drawing = DS.Model.extend({
shapes: belongsTo('shape', { polymorphic: true })
});
App.Shape = DS.Model.extend({
x: DS.attr('number'),
y: DS.attr('number'),
color: DS.attr('string')
});
App.Rectangle = App.Shape.extend({
height: DS.attr('number'),
width: DS.attr('number')
});
App.Circle = App.Shape.extend({
radius: DS.attr('number')
});
Shape = DS.Model.extend({
shapeType: function() {
this.constructor.typeKey
}.property()
});
Rendering views dynamically based on type
<p>
<label>{{name}}</label>
{{view view.elementViewType}}
</p>
A helper to dynamically render a component
var H = Em.Handlebars;
H.registerHelper('renderComponent',
function(componentPath, options) {
var component = H.get(this, componentPath, options),
helper = H.resolveHelper(options.data.view.container, component);
helper.call(this, options);
}
);
Polymorphic Endpoints
this.store.find('shapes').then(function(shapes) {
// ?
});
class RectangleSerializer < ActiveModel::Serializer
attributes ..., :type
end
Store = DS.Store.extend({
push: function(type, data, _partial) {
var dataType, modelType, oldRecord, oldType;
modelType = oldType = type;
dataType = data.type;
if (dataType && (this.modelFor(oldType) !== this.modelFor(dataType))) {
modelType = dataType;
if (oldRecord = this.getById(oldType, data.id)) {
this.dematerializeRecord(oldRecord);
}
}
return this._super(this.modelFor(modelType), data, _partial);
}
});
Bootstrap Endpoints
this.store.find('bootstrap')
Different adapters per type
API can serve references to other APIs
Contrived Example!
{
"user":{
...
"avatar": {
"id":"luke@precisionnutrition.com",
"type":"gravatar"
}
}
{
"user":{
...
"avatar": {
"id":"luke@precisionnutrition.com",
"type":"s3"
}
}
The Badness
Tight coupling between API and Ember-Data
{
"drawing":{
"id":1,
"title":"A house",
"shapes":[
{
"id":1,
"type":"Rectangle"
},
{
"id":2,
"type":"Rectangle"
},
{
"id":3,
"type":"Triangle"
}
]
}
}
Map type key to ember model name
typeMap = {
quadrangle: 'rectangle',
rectangle: 'quadrangle'
};
mapType = function(key) {
return typeMap[key.underscore()] || key;
};
Endpoint Mapping
this.store.find('quadrangle', 1);
/quadrangles/1 → /rectangles/1
Custom Adapter
ApplicationAdapter = DS.ActiveModelAdapter.extend({
pathForType: function(type) {
return this._super( mapType(type) );
}
});
↑
/quadrangles/1 → /rectangles/1
JSON Mapping
{
"rectangle": {
"id": "1",
....
}
}
Custom Serializer
ApplicationSerializer = DS.ActiveModelSerializer.extend({
typeForRoot: function(key) {
return this._super( mapType(key) );
},
serializeIntoHash: function(data, type, record, options) {
var key, typeKey;
typeKey = type.typeKey;
key = mapType(typeKey);
return data[key] = this.serialize(record, options);
}
});
↑ JSON → Model ↑ Model → JSON
Mission Accomplished?
{
"drawing":{
"id":1,
"title":"A house",
"shapes":[
{ "id":1,
"type":"Rectangle"
},
{ "id":4,
"type":"Circle"
}
]
}
}
3 seperate requests
({Number of types} + 1)
This can add up pretty quickly
True story
But at least they are executed concurrently
Sideload
class DrawingSerializer < ActiveModel::Serializer
.......
has_many :triangles, include: true, embed: :ids
has_many :rectangles, include: true, embed: :ids
end
{
"drawing":{
......
"triangle_ids":[3], // Redundant
"rectangle_ids":[1, 2] // Redundant
},
"triangles":[
{
"id":3,
"x":0,
"y":0,
"fill":"blue",
"width":"200",
"height":"100",
"drawing_id":1
}
],
"rectangles":[
.....
]
}
STI?
https://github.com/Bestra/ember-data-sti-guide
Ember.MODEL_FACTORY_INJECTIONS = true;
doubleplusungood
Ember.assert(..., record instanceof this.type);
rectified
App.Shape.detect(myRectangle.constructor)
Presentation
http://lukegalea.github.io/ember_data_polymorphic_presentation
Demo App
https://github.com/PrecisionNutrition/ember-data-polymorphic-full-stack-example