Polymorphic URLs in Ember

By: Brian Cardarella
routes

Ember makes it easy to consume polymorphic records, assuming you have the following data retrieved from your server:

{
  "data": [
    {
      "id": 1,
      "type": "hooman",
      "relationships": {
        "pet": {
          "data": {
            "type": "cat",
            "id": 1
          }
        }
      }
    }, {
      "id": 2,
      "type": "hooman",
      "relationships": {
        "pet": {
          "data": {
            "type": "dog",
            "id": 3
          }
        }
      }
    }
  ]
}

Let’s put aside my contrived data model for a minute, the point is that in this set the pet relationship for the hooman can be of type cat or dog. We can handle this in Ember Data by first creating a pet model:

import DS from 'ember-data';

const {
  attr,
  belongsTo,
  Model
} = DS;

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

Then in the hooman model:

import DS from 'ember-data';

const {
  attr,
  belongsTo,
  Model
} = DS;

export default Model.extend({
  pet: belongsTo('pet', { polymorphic: true })
});

This will instruct the relationship to look at the type property to determine the model type. You can then access through the pet property:

get(hooman, 'pet') // <cat-model>

Ok, this was probably review for some people. I recently ran into an issue where I needed to do a link-to for a polymorphic relationship. Using our same example, I would have done:

{{link-to hooman.pet.name 'pet' hooman.pet}}

The router would be setup as:

// you should make this one of the last routes
this.route('pet', { path: '/:pet_type/:pet_id' });

When we are using link-to there really isn’t an issue as Ember will just use the model passed in as the model. But we want a nicely formatted URL. Ideally if we are passing in a cat pet we would end up with the url: /cats/2 and if we are passing in a dog pet we’d get /dogs/5 and both would be processed by the pet route and template (of course we’d have to make sure they both respond to the same properties).

To get this we can override the serialize function in our pet route. serialize is used to parse information out of your model to render the URL associated. It is simple enough to do what we want:

import Ember from 'ember';

const {
  get,
  Route
} = Ember;

export default Route.extend({
  serialize(model) {
    return {
      pet_type: model.constructor.modelName,
      pet_id: get(model, 'id')
    };
  }
});

Now the link-to will render how we’d expect. Well, mostly. With this we end up with singular route names: /dog/5 instead of /dogs/5 for example. We need to pluralize.

Because we’re using Ember Data the ember-inflector library is pulled in automatically. Let’s use that:

import Ember from 'ember';

const {
  get,
  Route,
  String: { pluralize }
} = Ember;

export default Route.extend({
  serialize(model) {
    return {
      pet_type: pluralize(model.constructor.modelName),
      pet_id: get(model, 'id')
    };
  }
});

Now we get /dogs/5. If you are using a different type that does not pluralize nicely (for example person => persons instead of people) then check out the ember-inflector README for details on how to add the pluralized types you need.

This is great, except if someone visits the URL directly instead of clicking on a link. Let’s add support for that.

import Ember from 'ember';

const {
  get,
  Route,
  String: { pluralize, singularize }
} = Ember;

export default Route.extend({
  model(params) {
    return this.store.find(singularize(params.pet_type), params.pet_id);
  },
  serialize(model) {
    return {
      pet_type: pluralize(model.constructor.modelName),
      pet_id: get(model, 'id')
    };
  }
});

There we go! Because the URL segment was pluralized we used singularize to force the type back to what the store expects. I hope this helps you.