Juhana Kangaspunta
Juhana Kangaspunta

Reputation: 23

Ember: Fetching data for objects that depend on each other

I'm trying to build a front-end for a metrics tool with Ember. The code that I've written so far has been very much influenced by Eviltrout's emberreddit application

https://github.com/eviltrout/emberreddit

The goal is to have two classes that depend on each other: metrics and filters.

1) Once the application initializes, the filters, which are instances of the Filter-class, are loaded from the server. Once the filters have loaded, they are displayed as checkboxes on the screen. After that, the metrics objects should take the filters as parameters and query the server for data.

2) Once the user changes the checkboxes and thus updates the filter objects, the application should take the filters as parameters again and fetch new metrics data from the server.

My problem is that I don't know how to handle the dependencies between these two sets of objects with asynchronous ajax calls. At it's current state, my application doesn't finish loading the filters when it already starts loading the metrics. Therefore, the filters don't get passed as parameters for the metrics ajax-call.

My question is: What's the best way to do this ember? There surely has to be a way to handle the order of ajax calls. My intuition is that manually adding observers isn't the way to go.

Here are the models of my application:

//FILTER MODELS

var defaultFilters = ['dates', 'devices'];

//set Filter class. The Filter object will be multiplied for each filter.
App.Filter = Ember.Object.extend({

    //capitalize first letter to get title
    filterTitle: function() {
        return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
    }.property('id'),

    //set attribute to see if filter has loaded
    loadedFilter: false,

    //create method to load filter values from server
    loadValues: function() {
        var filter = this;
        return Ember.Deferred.promise(function (p) {  
            if (filter.get('loadedFilter')) { 
                p.resolve(filter.get('values'));
            } else {
                p.resolve($.getJSON("http://127.0.0.1:13373/options/" + filter.get('id')).then(function(response) {
                var values = Ember.A();
                response[filter.get('id')].forEach(function(value) {
                    values.push(value);
                });
                filter.setProperties({values: values, loadedFilter: true});
                return values;
                }))
            }})}
    }  
);



//reopen class to create "all" method which returns all instances of Filter class
App.Filter.reopenClass({
    all: function() {
        if (this._all) {return this._all; }
        var all = Ember.A();
        defaultFilters.forEach(function(id) {
            all.pushObject(App.Filter.create({id: id}));
        });
        this._all = all;
        return all;
}});

//Create a Filters array to store all the filters.
App.Filters = App.Filter.all();

//METRIC MODELS
App.Metric = Ember.Object.extend({

    metricTitle: function() {
        return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
    }.property('id'),

    loadedMetric: false,

    filtersBinding: 'App.Filters',

    loadValues: function() {
    var metric = this;
    var filters = metric.get('filters');
    if (filters.get('loadedFilters'))
    console.log('loading metrics'); 
    return Ember.Deferred.promise(function (p) {  
        if (metric.get('loadedMetric')) { 
            p.resolve(metric.get('values'));

        } else {
            p.resolve(
                console.log('sending ajax'),
                $.ajax({ 
                url: "http://127.0.0.1:13373/" + metric.get('id') + "/",
                data: JSON.stringify(metric.get('filters')),
                }).then(function(response) {
            var values = Ember.A();
            response[metric.get('id')].forEach(function(value) {
                values.push(value);
            });
            metric.setProperties({"values": values, "loadedMetric": true});
            return values;
            }))
        }})}


});

App.Metric.reopenClass({


    findByView: function(searchView) {
        if (this._metrics) {return this._metrics; }
        var metrics = Ember.A();
        defaultMetricsSettings.forEach(function(metric) {
            if (metric.view == searchView)
                metrics.pushObject(App.Metric.create({id: metric.id},{view: metric.view}, {calculation: metric.calculation}, {format: metric.format}, {width: metric.width}));
        });
        this._metrics = metrics;
        return metrics;
    }
});

And here are the routes:

App.ApplicationRoute = Ember.Route.extend({
    //set application routes model to all filters
    model: function() {
        return App.Filter.all();
    },

    //after filter has loaded, let's load its values
    afterModel: function(model) {
        return model.forEach(function(item) {
            item.loadValues();
        });
    },

    //create a controller called ApplicationController and pass the filter as its model
    setupController: function(controller, filter) {
        controller.set('model', filter);
    }

});

App.DashboardRoute = Ember.Route.extend({
    model: function() {
        return App.Metric.findByView('Dashboard');
    },

    afterModel: function(model) {
        return model.forEach(function(item) {
            item.loadValues();
        });
    },

    setupController: function(controller, metric) {
        controller.set('model', metric);
    }
});

Controllers:

App.ApplicationController = Ember.ArrayController.extend({
    //ApplicationController controls all the filters. Let's create a controller to handle each instance of a filter
    itemController: 'filter'
});

App.FilterController = Ember.ObjectController.extend({
    //this sets the titleId property that is used only for binding html attributes in template. Stupid way to do this.
    titleId: function() {
        return "#" + this.get('filterTitle');}.property('filterTitle')
});

Upvotes: 2

Views: 590

Answers (1)

Darshan Sawardekar
Darshan Sawardekar

Reputation: 5075

Your afterModel hook could do this in a sequence of dependent promises. The current implementation is returning immediately, instead you chain the promise and finally return the last promise as the result of the hook. The router will wait for the whole set of calls to complete before continuing to setupController.

afterModel: function(model) {
  var promise;
  model.forEach(function(item)) {
    if (promise) {
      promise = promise.then(function() {
        item.loadValues();
      });
    } else {
      promise = item.loadValues();
    }
  }

  return promise;
}

I'm not sure how many of the calls you have, but you may want to batch some of these together to reduce the number of HTTP requests.

Upvotes: 1

Related Questions