javascriptember.jsember-data

ember.js, filtering PromiseManyArray in the controller


I am trying to define a computed property that consists of a filtered hasMany relationship. When I loop over the items of the PromiseManyArray, I get undefined when trying to access the attribute I want to filter on. On later calls to this computed property, everything works fine.

This is a simplified version of my controller code:

export default Ember.Controller.extend({

    availableModules: function () {
        let thisModule = this.get('model')

        console.log(thisModule.get('library.modules')) // This logs <DS.PromiseManyArray:ember604>

        // loop over siblings
        return thisModule.get('library.modules').filter(mod => {
            // mod.classification is undefined
            return mod.get('classification') !== 'basis'
        })

   }.property('model')
})

For the Module model we can assume that it has a classification attribute, and it belongs to a Library object, and the Library model hasMany modules.

I have tried something like this, and it logs properly the attribute classification, but I don't know how to return anything so that the template can render it.

availableModules: function () {
    let thisModule = this.get('model')

    thisModule.get('library.modules').then(mods => {
        mods.forEach(mod => {
            console.log(mod.get('classification'))
        })
    })
}.property('model')

So the problem seems to be that inside of the PromiseManyArray.filter method, the attributes of the found objects are not yet resolved... How can I create a promise that will return all filtered objects once those have been resolved? I don't know how to get my head around this. Thanks.


Solution

  • Inspired by Bloomfield's comment, and with help of this thread in the ember forum, I have found an acceptable solution. Basically it consists of resolving all the relationships in the route, so that when the controller is called, you don't have to deal with promises.

    Solution:

    1. In the model hook of the route, return a hash of promises of all the needed information
    2. Define a custom setupController, and inside of it, store the model and the extra data in the controller

    The route code looks like this:

    export default Ember.Route.extend({
        model(params) {
            let module = this.store.findRecord('module', params.mod_id)
    
            return Ember.RSVP.hash({
                module: module,
                siblingModules: module.then(mod => mod.get('library.modules')),  // promise based on previous promise
            })
        },
    
        setupController(controller, hash) {
            controller.set('model', hash.module)
            controller.set('siblingModules', hash.siblingModules)
        },
    })
    

    Note: for the route to still work properly, the {{#link-to 'route' model}} have to explicitly use an attribute, like the id: {{#link-to 'route' model.id}}

    Extra info

    Bloomfield's approach consisted of using the afterModel hook to load the extra data in an attribute of the Route object, and then in the setupController, set the extra data in the Controller. Something like this:

    export default Ember.Route.extend({
        model(params) {
            return this.store.findRecord('module', params.mod_id)
        },
    
        afterModel(model) {
            return model.get('library.modules').then(modules => {
                this.set('siblingModules', modules)
            })
        },
    
        siblingModules: null,  // provisional store
    
        setupController(controller, model) {
            controller.set('model', model)
            controller.set('siblingModules', this.get('siblingModules'))
        },
    })
    

    But this feels like a hack. You have to return a promise in afterModel, but you can't access the result. Instead the result has to be accessed via .thenand then stored in theRoute` object... which is not a nice flow of information. This has however the advantage that you don't have to specify any attribute for the links in the template.

    There are more options like using PromiseProxyArray, but that's too complicated for a newcomer like me.