Skip to content
Kent C. Dodds edited this page Jun 27, 2014 · 3 revisions

Table of Contents:

How Tos:

How Tos are solutions to common problems. They may not be the best for every circumstance (or any circumstance), so take them with a grain of salt.


How to: deal with child resources

Say you have two resources, users and posts. Let's also say that the server returns the post resource with a user property that's actually the user object. The problem is that now you have two resources loaded, but only one injected (the post). And if you are using the methods property when you defined your user resource, the post.user will not be of type User and you can't invoke any of the methods on that user.

Angular-data gives you the afterInject hook which can help to resolve this issue. The concept is pretty simple. After the post is injected, you tell DS to inject the user and replace the reference to the user in the post to the new injected user. You can do this like so:

DS.defineResource({
  name: 'user',
  endpoint: 'users',
  methods: {
    sayHi: function sayHi() {
      console.log('Hi! My name is ' + this.name);
    }
  }
});

DS.defineResource({
  name: 'post',
  endpoint: 'posts',
  afterInject: function afterInject(resourceName, attrs) {
    attrs.user = DS.inject('user', attrs.user);
  };
});

Now you can do post.user.sayHi() and everything will work splendidly.

If you have several resources with this same issue, you may consider implementing something more generic. Like this:

DS.defaults.afterInject = function defaultAfterInject(resourceName, attrs) {
  findAndInjectChildResources(attrs);
};

// All resources should be defined at this point
var resourceNames = Object.keys(DS.definitions);

function findAndInjectChildResources(value, key, parent) {
  if (angular.isObject(value)) {
    if (value.id && key) {
      if (_.contains(resourceNames, key)) { // using lodash
        parent[key] = DS.inject(key, parent[key]);
      } else if (angular.isArray(value) && _.contains(resourceNames, pluralize.singular(key))) { // using pluralize, to handle cases like "mouse" as a single resource or "mice" as an array of resources
        parent[key] = angular.forEach(value, function(val, index) {
          parent[key][index] = DS.inject(key, val);
        });
      }
    } else {
      value = angular.forEach(value, function(val, k) {
        value[k] = findAndInjectChildResources(val, k, value);
      });
    }
  }
  if (parent && key) {
    return parent[key];
  } else {
    return value;
  }
}

Note: if the service you're using is sending back author on the post instead of user, the generic solution above wont work and you may need to one-off that piece.

Another Note: if the service is sending back only part of the child resource (like a "basic - this is all you need in this context" type thing) then you shouldn't be injecting the resource at all because you'll run into issues with caching and you could instead alter the previous solution like so:

// All resources should be defined at this point
var resourceNames = Object.keys(DS.definitions);
angular.forEach(resourceNames, function(resourceName) {
  // get the Resource's Class (if it has one, otherwise, just use Object) and use it to create new instances.
  var ResourceClass = DS.definitions[resourceName][DS.definitions[resourceName].class] || Object;
  DS.definitions[resourceName].meta = DS.definitions[resourceName].meta || {};
  DS.definitions[resourceName].meta.createInstance = function(attrs) {
    var instance = new ResourceClass(); // note you have a reference to the resource definition using "this" here, so you could also do: new this[this.class]();
    return angular.extend(instance, attrs);
  };
});

Then you alter the above example to call createInstance on the definition rather than injecting the resource into the datastore.

Clone this wiki locally