Justin Stayton
Justin Stayton

Reputation: 6291

Waiting for models to load before rendering app in Ember.js

I have a number of different application-level models — i.e., current user, current account, etc. — that I want to load before rendering my application. How and where should this be done? This question/answer helped a lot, but it doesn't cover the async aspect.

The following code accomplishes what I want, but loading the models in beforeModel (to take advantage of it waiting for the promise to resolve) doesn't seem right. Should I even be loading these models in ApplicationRoute?

App.ApplicationController = Ember.Controller.extend({
  currentAccount: null
});

App.ApplicationRoute = Ember.Route.extend({
  beforeModel: function () {
    var self = this;

    return App.Account.find(...).then(function (account) {
      self.controllerFor('application').set('currentAccount', account);
    });
  }
});

Thanks for your help!

Upvotes: 5

Views: 15427

Answers (3)

greg.arnott
greg.arnott

Reputation: 1642

You want to preload data/models to initialize your application, and feel beforeModel is incorrect?

Sounds like you need an application initializer!

Your friend in this instance:

App.deferReadiness(); // halt progress of application until all instances of this call (ie: multiple initializers) are matched by an instance the following call:

App.advanceReadiness(); // consider this to be equivalent to a promise resolve call.

1) From you looking up the user directly, modifying where mentioned to suit your app setup:

Ember.Application.initializer({
    name: 'loadUser',
    after: 'store',
    initialize: function(container, app) {
        // modify this following to suit how you're determining the account
        var url = 'user/' + currentAccount;
        // tell the app to pause loading until advanceReadiness is declared
        app.deferReadiness();

        // load from JSON 
        Ember.$.getJSON('url').then(function(json) {

            var store = container.lookup('store:main');
            store.load(app.User, json);

            // tell app to start progressing again
            app.advanceReadiness();
        });
    }
});

2) Through meta tag:

Ember.Application.initializer({
    name: 'currentUser'
    after: 'store',

    initialize: function(container, app) {
        app.deferReadiness();

        $(function() {
            // Look up an attribute in a meta tag
            var store      = container.lookup('store:main'),
                attributes = $('meta[name="current-user"]').attr('content');

            if (attributes) {
                var obj        = store.load(app.User, JSON.parse(attributes)),
                    user       = App.User.find(obj.id),
                    controller = container.lookup('controller:currentUser').set('content', user);

                container.typeInjection('controller', 'currentUser', 'controller:currentUser');
            }
            app.advanceReadiness();
        });
    }
});

3) Through Session data:

Ember.Application.initializer({
    name  : 'currentUser',
    after : 'session',
    initialize: function(container, app) {
        var controller = container.lookup('controller:currentUser');
        container.typeInjection('controller', 'currentUser', 'controller:currentUser');
    }
});

Upvotes: 6

Alex
Alex

Reputation: 12433

I managed to get this work by using nested Promises and the afterModel method in the ApplicationRoute.

App.ApplicationRoute = Ember.Route.extend({

    model: function() {

        // load the reservation (a globally needed model)
        return App.Reservation.fetch().then(function(reservations) {
            return reservations.get('firstObject');
        });

    },

    afterModel: function() {

        // Load all other globally needed models
        var self = this;
        return App.Gender.fetch().then(function(genders) {
            self.controllerFor('application').set('genders', genders);
            return App.FilterAttribute.fetch().then(function(filterAttributes) {
                self.controllerFor('application').set('filterAttributes', filterAttributes);
                //return App.SomeOtherModel...
            });
        });

    },

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

});

Works just perfectly :-) The application remains in the LoadingRoute until all records are loaded. Note that I am using Ember Model, but this should make no difference, it just have to return a Promise.

Upvotes: 3

Meori Oransky
Meori Oransky

Reputation: 688

The trick is to return a promise from the route's model method.
This will cause the router to transition into App.LoadingRoute route, until the promise resolves (which can be used for loading indication bars/wheels etc.)
When the promise resolves, the App.LoadingRoute will be deactivated, and the original route's setupController method will be called.
This works for ember-data promises, JQuery's $.ajax promises and ember-model's fetch promises.
Just make sure you return the actual model after resolving the promise.
This can also be a good place to handle errors if the promise is rejected - but I'll leave that to some other question.

As for where you should load your models - that is dependent on your app's usage.
Usually you would load a model where the URL indicates you need that model - a rule of thumb would be the indication of a model ID in the URL.
This of course changes if you need to prefetch some data.

And now for some code:

App.SomeRoute = Ember.Route.extend({
   model: function(params){
       return App.SomeModel.fetch(params.model_id).then(function(modelData){
           // it is better to return the actual model here, and not the promise itself
           return App.SomeModel.find(params.model_id);
       });

   },
   setupController: function(controller, model){
       controller.set("model", model);
       // do some controller setup here - can be omitted if no setup is needed
       // this will run only after the promise has been resolved.
   }
});

App.LoadingRoute = Ember.Route.extend({
        activate: function(){
            this._super();
            // add some loading indication here
        },
        deactivate: function(){
            this._super();
            // remove loading indication
        }
}

Hope this helps.

Upvotes: 6

Related Questions