Recently, I needed a way to standardize how we were calculating properties such as isAnythingDirty or isAnythingSaving across several Ember.js controllers. Our templates then use those properties to render components in different states.

Introducing isAnyPath

Our solution to calculating the isAnythingDirty and isAnythingSaving checks is basically a utility function where we can pass an array of paths to model collections or individual models along with a single property to check for. If one of those models within any one of those paths contains a true value of the property we're checking for, the entire method returns true. So instead of writing something like this:

isAnythingDirty: Ember.computed(  
  'model.isDirty',
  'anotherModel.isDirty',
  'modelArray.@each.isDirty',
  function() {
    // Logic to calculate whether anything is dirty
  }
)

You can now write it this way to abstract the logic for the actual calculations:

isAnythingDirty: isAnyPath('isDirty', [  
  'model',
  'anotherModel',
  'modelArray.@each'
])

The function isAnyPath returns a computed property, so there is no need to write Ember.computed(). Also, note that this method requires the list of model paths to be entered as a javascript array []. Without further ado, here is the method in all its glory:

import Ember from 'ember';

/**
 * Returns true if the object or anything in it contains a true value for the given property name.
 * @param flag {String} - The Ember Data Model Flag to check if anything is
 * @param modelPaths {String[]} - The paths to all your models. (e.g. ['model.filter', 'model.audiences.@each'])
 * @returns {Ember.ComputedProperty}
 */
export default function(flag, modelPaths) {  
  var dependentKeys = modelPaths.map(function(arg) { return arg + '.' + flag; });
  dependentKeys.push(function() {
    return modelPaths.some(function(modelPath) {
      if (modelPath.indexOf('@each') !== -1) {
        return this.get(modelPath.replace(/\.?@each/, '')).some(function(model) {
          return model.get(flag);
        });
      }
      else {
        return this.get(modelPath + '.' + flag) === true;
      }
    }, this);
  });
  return Ember.computed.apply(Ember, dependentKeys);
}

Let's break this down to see what's happening under the hood.

var dependentKeys = modelPaths.map(function(arg) { return arg + '.' + flag; });  

This line basically shortcuts having to add the common property name (e.g. .isSaving, .isDirty), to the model paths by creating a copy of the model path list but with the flag appended to each one.

dependentKeys.push(function() {  
  return modelPaths.some(function(modelPath) {
    if (modelPath.indexOf('@each') !== -1) {
      return this.get(modelPath.replace(/\.?@each/, '')).some(function(model) {
        return model.get(flag);
      });
    }
    else {
      return this.get(modelPath + '.' + flag) === true;
    }
  }, this);
});

This block injects a function that loops through each of the model paths and checks whether any of the individual models has a true value assigned to the property specified by the flag variable.

return Ember.computed.apply(Ember, dependentKeys);  

Apply calls a function with a given this value, in this case Ember, and takes an array of arguments. This is helpful for creating computed functions since we can bind new ones to our instance of Ember. It's also possible to instead use JavaScript's call method, which accepts the arguments as multiple parameters instead of the array of parameters that apply takes.

Here is a workng JSFiddle to show you the isAnyPath method in action.

A Simpler Method for Simpler Cases

Let's say, for example, that you need a similar method only within a single controller, and the model paths are the same every time. One way you could shorten this up is to put the computed property itself in a variable and then have your properties just call the function where it needs to, like this:

/**
 * Returns true if models in the modelCollection contains a true value for the given property name.
 * @param flag {String} - The Ember Data Model Flag to check if anything is
 * @returns {Ember.ComputedProperty}
 */
 var filterModelsByProperty = function(param) {
  return Ember.computed('modelCollection.@each.' + param, function() {
      return this.get('modelCollection').filterBy(param, true);
  })
}

This returns a subset of models from the modelCollection that have a true value for the property whose name is param. As an example, you can call it this way:

completedModels: filterModelsByProperty('isCompleted'),  
dirtyModels: filterModelsByProperty('isDirty')  

Here's another JSFiddle for your enjoyment.

If you have any suggestions for improvement, please feel free to let us know in the comments below.