Best practice to get multiple data from route before the view rendering?


#1

I met a requirement which needs multiple data from different ajax requests in one route. And I want the data is prepared before view rendering. Now I can do that partially, but I want to hear your suggestions about what is the best practice.

For simplicity I use $.get in demo code. And in my project I don’t use ED or other model layer libs, just simple encapsulation for $.ajax.

My requirement is like this:

I have a “edit user” page. In this page I can edit a user and assign it with a role. So I need two data: user and role list. At beginning I get user data in model hook and role list in setupController.

1st solution

App.Router.map(function() {
  this.route('user', {path: '/user/:user_id'});
});

App.UserRoute = Em.Route.extend({
  model: function(params) {
    return $.get('/users/'+params.user_id+'.json');
  },

  setupController: function(controller, model) {
    controller.set('model', model);
    $.get('/role_list.json').then(function(roles) {
      controller.set('roleList', roles);
    });
  }
});

But in this way, I find that the role list is loaded after view rendering. That’s not what I want.

Because only beforeModel, model, afterModel hook support promise, and before the promise is resolved, the setupController is not called and view is not rendered. I start thinking about getting all data in model hook. And Em.RSVP seems a good choice.

2nd solution

App.UserRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      user: $.get('/users/'+params.user_id+'.json'),
      roleList: $.get('/role_list.json')
    });
  },

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

It accomplish the task for me: getting all data before view rendering. But since the route contains dynamic segments, one thing breaks this solution: link-to.

I have this link in my template:

{{#link-to "user" user}}Some user{{/link-to}}

The model hook is called only when the route is entered via URL, if the route is entered by click link-to link, It passes the model from link-to to that route.

So in my case, when the route is entered via URL, the model parameter for setupController is the result of Em.RSVP which is a hash (a plain JavaScript object). when the route is entered via link-to link, the model parameter for setupController is the model link-to give it to. It’s bad, and since model hook is not called, I can not get the role list.

But there is another way. even model hook is not called, afterModel hook is always called and it also support promise. So the 3rd solution is to get role list in afterModel hook.

3rd solution

App.UserRoute = Em.Route.extend({
  model: function(params) {
    return $.get('/users/'+params.user_id+'.json');
  },

  afterModel: function(model, transition) {
    return $.get('/role_list.json').then(function(roleList) {
      // Because I can not pass role list to setupController, I have to set it as a model property.
      model.get('roleList', roleList);
    });
  },

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

It works, but not a best practice. As you can see there is no way to pass role list to setupController, because setupController only get the model from model hook or link-to. I have to set it as model property. Another way is to set the role list to a route property and get it in setupController, and then clear the route property. But I think it’s also not a good solution.

What I’m thinking about is, setupController is used for getting multiple data and set multiple controllers. Currently we can set multiple controllers, but we don’t have a good way to get all data before view rendering.

We can only get main data for a route (user in this case) before view rendering, and for the rest data, we have to get them after view is rendered, and before that, we need to display “Loading” everywhere.

What I think a good solution, is to make setupController to also support promise in future.

Do you have any idea about this situation?


#2

I think you have the right idea in your second solution.

Have a look at the link-to documentation here: https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/helpers/link_to.js#L648

Instead of passing the model to the link-to helper, you can pass a string or integer argument:

{{#link-to "user" user.id}}Some user{{/link-to}}

This will trigger the model hook of your route with params.user_id set to the user id, giving you the behavior you expect in the second solution.


#3

Yeah that works with the 2nd solution. Although it also give up the convenience Ember give us and not what Ember expects (use existing data to avoid getting it again), it can work perfectly with Em.RSVP.


#4

If you were using something like ember data, the user would be cached if it was fetched previously so passing the id to User.find would get you the cached data instantly. Of course this isn’t the case with your manual AJAX request, but you can return a promise in afterModel:

App.UserRoute = Em.Route.extend({
  model: function(params) {
    return $.get('/users/'+params.user_id+'.json');
  },

  afterModel: function(model) {
    return Em.RSVP.hash({
      user: model,
      roleList: $.get('/role_list.json')
    });
  }
});

Best practice for multiple models per route with pagination
#5

That’s the 3rd solution which uses afterModel. However in the code the roleList can not be passed to setupController. Even you return Em.RSVP.hash in afterModel, the model parameter for setupController is still user.

To solve this problem I have to pass roleList to user model or pass it as a route property. That’s why I think it’s not a good solution.

So currently link-to with id is the best way for this question.


#6

This looks like the best solution. But there is no way to access roleList in setupController hook, because setupController gets what returned in model hook, not afterModel.


#7

We faced the same issue, so we had to options:

  1. use Route instance property so in afterModel we would set this.roleList = roleList; and access it in setupController:

  2. return model and roleList from afterModel as one object and manage this in setupController like this

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

But as been mentioned, option 2 won’t work. So we fell down to option 1.


#8

I think link-to with id is not the best answer, since it requires you to remember about this implementation detail and whenever somebody is doing link-to they will run into issues if they won’t remember that. Therefore route property sounds like the best solution to me, since polluting model feels dirty and hacky.


#9

In a similar case, the following solution has worked for me:

App.UserRoute = Ember.Route.extend({
	model: function () {
		return Em.RSVP.hash({
                     content: $.get('/users/'+params.user_id+'.json'),
                     roleList: $.get('/role_list.json')
	 	});
	},
	setupController: function(controller, model) {
		controller.setProperties(model);
	},
});

Credits:

Stackoverflow: Can Ember.Select render a promise for content