3
votes

I am trying to filter the contents of a ember-data 'hasMany' field. My model has some subquestions, which I want to filter into a property 'childOptions' on my controller and display in the template using

{{#each childOptions}}stuff{{/each}}

When I put this on my controller, it works and the each iterates over appropriate values:

childOptions: Ember.computed.filterBy('model.subquestions', 'surveyQuestionType.name', 'childOption'),

However when I do this, nothing is shown.

childOptions: Ember.computed.filter('model.subquestions', function(subquestion) {
    return subquestion.get('surveyQuestionType.name') === 'childOption';
}),

'surveyQuestionType' is a DS.belongsTo that exists on the model for 'subquestions', and it has a 'name' property.

I want to understand why the 'filterBy' method works, while the 'filter' method does not (so that I can use 'filter' for more complex queries in the future). I think it has something to do with promises, and the subquestion.get('property') syntax that I am using inside the filter function.

EDIT:

This is the model:

App.SurveyQuestion = DS.Model.extend(Ember.Validations.Mixin, {
    surveyQuestionType: DS.belongsTo('surveyQuestionType', { async: true }),
    display: DS.belongsTo('surveyQuestionDisplay', { async: true, inverse: 'surveyQuestion' }),
    sortOrder: DS.attr('number'),
    parent: DS.belongsTo('surveyQuestion', { async: true, inverse: 'subquestions' }),
    parentDependencyCriteria: DS.attr('string'),
    required: DS.attr('boolean'),
    surveySections: DS.hasMany('surveySectionQuestion', { async: true, inverse: 'surveyQuestion' }),
    subquestions: DS.hasMany('surveyQuestion', { async: true, inverse: 'parent' })
});
2
subquestion.get('surveyQuestionType.name') returns undefined, which I am pretty sure is the problem, but I don't know how to fix it! If I do a simple {{#each subquestions}}{{surveyQuestionType.name}}{{/each}} in my template I can see the property just fine.leejt489

2 Answers

5
votes

I have spent more time discovering this category of issue with my own work than I care to admit, but fortunately the solution is straightforward. In your DS.Model definition, is surveyQuestionType a belongsTo relationship with {async: true}? If so, that's your issue.

Whenever you set a relationship in your DS.Model as {async: true}, you can think of it as actually setting a promise that you will eventually get that property. This makes sense and becomes intuitive, but it's not documented very well!

Promises are especially tricky for beginners because your Handlebars template will transparently handle {{surveyQuestionType.name}} whether surveyQuestionType is a concrete value or a promise. That confuses beginners because you can't tell at first blush if Handlebars is rendering a concrete value or a promise.

When you're dealing with a promise, you can directly access what that promise resolves to in its content property. In fact, you can even set that content property, too. But be careful with this because reading/writing directly to the content property has no effect on any pending operations that promise may have. So if the promise is still pending while you write to its content value, your write will get overwritten once it resolves.

I write directly to the content property when I'm adding a new entity and need to populate a promise relationship. It makes sense there, but if I were reading a value, I would need to find some way to guarantee for myself that the promise had already resolved when I was reading the content property...or, I could let Handlebars handle this directly, knowing that for pure display logic I don't care about a delay of 150 ms.

Anyway, if everything above is applicable to your issue, here's how you can edit your code to get this working:

childOptions: Ember.computed.filter('model.subquestions', function(subquestion) {
    return subquestion.get('surveyQuestionType.content.name') === 'childOption';
})

UPDATE #1: I believe I just violated my own recommendation by not accessing content thoughtfully. See my comments for info on debugging, and you might also try this:

childOptions: Ember.computed.filter('model.subquestions', function(subquestion) {
    return subquestion.get('surveyQuestionType').then( function( model ) {
       return model.get('name') === 'childOption';
    });
})

UPDATE #2: See my comments below and in particular https://github.com/emberjs/data/issues/1865. this turns out to be a trickier issue, and I would welcome the input of others to clarify the best approach here.

I have solved this problem in my own code by handling promises upstream so that I can directly access the content property or not deal with promises at all in my filters.

2
votes

As of ember-data 1.0.0-beta.11, promises seem to be working smoother and I had success with this (I actually went with a different approach for my original problem, but used this code in a similar situation):

childOptions: function() {
    var subquestions = this.get('subquestions');
    if (subquestions) {
        return subquestions.filter(function(subquestion) {    
            var surveyQuestion = subquestion.get('surveyQuestion');
            return (surveyQuestion && surveyQuestion.get('name') === 'childOptions');
        });
    }
}.property('[email protected]')

The .property('[email protected]') causes the property to be updated when the surveyQuestion promise resolves. If you put some output, you will see that initially the property gets called and subquestions will be null.