How to use afterModel results in setupController


#1

In several routes in throughout our application, we rely on model hooks to fetch some additional, related data, usually via an Ember.RSVP.hash:

model: function() {
  return Ember.RSVP.hash({
    model: this.store.find('post', params.post_id),
    trendingPosts: this.store.find('post', { trending: true })
  });
},
setupController: function(controller, hash) {
  controller.set('model', hash.model)
  controller.set('trendingPosts', hash.trendingPosts);
}

However, this get’s problematic when we try to use a {{#link-to 'posts.index' postRecord}}, since the model hook never fires, so the additional data is never fetched.

We tried limiting the model hook to only fetch the resource for that route (i.e. fetch the model you’d pass in to link-to) and instead use afterModel to fetch additional data:

model: function() {
  return this.store.find('post', params.post_id);
},
afterModel: function() {
  return this.store.find('post', { trending: true })
}

This feels much cleaner, but now there’s no way to get that additional data (trendingPosts) attached to the controller. setupController gets the results of the model hook, not the afterModel hook.

The only way around this I can see would be to set a property on the route as a temporary storage until setupController is called, but that’s a pretty ugly hack:

model: function() {
  return this.store.find('post', params.post_id);
},
afterModel: function() {
  var _this = this;
  return this.store.find('post', { trending: true }).then(function (trendingPosts) {
    _this.set('trendingPosts', trendingPosts);
  });
},
setupController: function(controller, model) {
  controller.set('model', model)
  controller.set('trendingPosts', this.get('trendingPosts'))
}

Any suggestions on the best way to fetch additional data?


Ember Wishlist!
#2

I’m speaking off the cuff here as I haven’t tested this…but I believe you could leave the hash in the model hook as you had it, and then pass in just the ID to the link-to helper…like: {{#link-to 'posts.index' postRecord.id}} …then, that will force your model hook to run again.


#3

@Spencer_Price yea, that’s another approach we tried.

But then why support the object reference syntax at all? That means now, wherever I link to a route, I need to have knowledge of its internals - will it fetch additional data in the model hook? If so, I have to use the id value. If not, then I can link with a direct object reference. That kind of coupling and uncertainty feels very un-Ember-ish, so I thought I would check here if I’m missing something.

It honestly feels like the results of afterModel should be made available in setupController. The combination of wait-on-promise but ignoring the results seems odd.


#4

@davewasmer I see your point.

I’m going to take another stab…I think this is a good design question…and leverage a separate named controller for the trending posts using the Promise Proxy mixin. Again, haven’t tested…no idea how this would work in practice:

App.TrendingPostsController = Ember.ArrayController.extend(Ember.PromiseProxyMixin);

App.PostController = Ember.ObjectController.extend({
  needs: ['trendingPosts'],
  trendingPosts: Ember.computed.alias('controllers.trendingPosts')
});

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('post', params.post_id);
  },
  afterModel: function() {
    var trendingPromise = this.store.find('post', {trending: true});
    return this.controllerFor('trendingPosts').set('model', trendingPromise);
  }
});

#5

As @Spencer_Price I didn’t tried this myself, but have you tried to modify the hash result (from the model hook) inside the after model hook? (you have it as first parameter in aftermodel) As far as I understand, all the model hooks (before, model and after) should pause the router until the promise returned is fullfilled, so that if you add a new promise to your RSVP.hash it should just work…


#6

Hey @nandosan …I’m not sure I follow. I think the point of what the the OP is trying to do is keep the model hook as plain as can be…in fact, in my example above, we should be able to safely remove the model hook and Ember’s magic will make it do just that. In my example, the afterModel does pause the router, since the TrendingPosts controller has been made promise aware (by using in the Ember.PromiseProxyMixin).


#7

@Spencer_Price that feels a bit better. Unfortunately, it means controllers for every bit of additional data, but I suppose thats not a terrible thing - gets you closer to controllers as presenters, where each controller decorates one bit of data.

I still think having the results of afterModel in setupController makes sense, so I’m curious - are there any drawbacks to that?


#8

@davewasmer Yea, I think more controllers is not a bad thing. It was something I had resisted for a while (See this ugly implementation and Alex Speller’s elegant, and now obvious, alternative). Once I embraced the idea of “controllers for everything!”, my code really started to become much much more simple to read and reusable. (And it finally made the {{render}} helper useful for me. Perhaps in your situation, you might be able to just do {{render 'trendingPosts'}} and have a separate named template and everything for that other controller)

But, I do think that you have a valid need here for which there is not an obvious solution. I think that the intent behind the afterModel hook is not for loading data per se, but allowing you to validate asynchronously wether this route should in fact be loaded. (This replaced the redirect hook back before the router was completely asynchronous).

Maybe your idea should be worked into a new hook…a metaModel hook or something that pauses the transition as you would expect, but also allows you to load other models for a particular route? (Ignoring the fact that it’d be a breaking change to do it the way I outline…but perhaps here’s what it could look like). I think this could help many of us who have run into this problem in the past and had different ways of solving it…

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('post', params.post_id);
  },
  metaModel: function() {
    return this.store.find('post', {trending: true});
  },
  // This is the breaking change as the transition object would become the 4th arg
  setupController: function(controller, model, metaModel/*, transition*/) {
    controller.set('model', model);
    controller.set('trendingPosts', metaModel);
  }
});

Thoughts?


#9

I’m in the multiple controllers hooked with needs camp. Controllers are long-lived, and the model hook is just setting the data there anyway.


#10

Hi @Spencer_Price, I didn’t noticed you used the PromiseProxyMixin. Yes that should work, but why use that approach when everything can be done in normal router lifecycle? (Just asking, looks like a good and sometimes the sole solution…)

I like the @alexspeller focus as well, basically he is reducing the whole window interface in little chunks of MVC, as blocks (components are also useful for that) . I tried that approach myself sometime ago, but I have a lot of trouble because controllers are singletons in the Ember app, so that you have to be careful to force the creation of new ones when needed and, at least in my case, causing the all reusable-singleton-instance being sort of broken. But maybe it was just my first or second Ember app :smile:

Personally I don’t see any trouble in charging the route hooks of code. The model hook is only called when your application is accesed directly by URL, so that you have to initialize it all (among all model hooks called in the stack…)


#11

This is a bit of a gotcha, but the way I would approach this (and do in most of my apps) is to pass an id instead of a record to the link-to helper, e.g. {{#link-to 'posts.index' postRecord.id}}. The model hook will always be called in this case, and everything will work as expected.

Another alternative would be to make use of nested routes, and make the post model the model of post route and trendingPosts the model of postIndex route, so that you have two model hooks.


#12

That would work, but then I’d repeat my response to @Spencer_Price:


@Spencer_Price you mentioned a metaModel hook as a solution:

I don’t see how this is any different than afterModel. I agree that most of the docs and examples I’ve seen for afterModel so far use it for validation and redirection, but functionally, it does exactly what we need: it pauses the transition, it’s separate from the model hook, and it doesn’t introduce any additional complexity to the router.

The only change needed would be to pass the results to the setupController hook, and it completely solves this problem. Thoughts?


#14

I very much concur that this is a big issue for non-master-detail pages, which need some element of control over loading important things first. I’ve been diving into composing a page from multiple data sources (with some endpoints being more important than others to the UX), and I believe it’s harder than it needs to be at the moment.

The methodology at the moment is to

  1. try to use afterModel or setupController, which don’t play that
    seamlessly well together
  2. try to use controllers to fetch
    additional data on .init or some other hook, which seems leaky
  3. utilize PromiseProxyArray, which I firmly believe is too complex for novices
  4. utilize RSVP.hash, but this is all or nothing and blocks the UI. I’d rather see my template data appear as separate promises resolve.

I don’t want to be that guy, but this all seems way too difficult for a fairly generic and common use case. Especially for novices like myself.

I would love to see either a simple, documented way of using the existing route hooks OR a new hook (like the mentioned metaModel or additionalData or auxillaryData) where you can feed an array of promises that resolve individually and fill in their sections of the template.

Can someone more familiar with the internals chime in on feasibility or why this would be a bad idea?


#15

The PromiseProxyMixin methodology is definitely the way to go if you want data to be filled in as the promises resolve. If more than one of the promises is required to be fulfilled for the UI to function correctly, using an RSVP.hash on the model hook and setting up the rest of the promises with PromiseProxies in setupController is a powerful combo and allows all the loading spinners/error states to be handled in the appropriate places.

Once you see an example of this method in action, I definitely don’t think it’s too complex for novices to understand (at least not any more than a non route-driven controller is), but it definitely could use better documentation since it’ll pop up in just about every single application…


#16

Do you know of any examples showing this method?


#17

Sorry for the late reply –

Here’s an article about loading states that has some notes about PromiseProxyMixin towards the end. It definitely helped me figure things out:

http://cball.me/understanding-loading-substates-in-ember/

It’s not a full example, but it might elucidate some things for you. If I come up with a minimal example of the whole thing, I’ll be sure to post it here.


#18

One gotcha with this approach of using afterModel to fetch a record within a route, then getting it within the setupController hook is this: when using queryParams + refreshModel, is that refreshModel: true triggers the three model hooks, but not the setupController hook. So if you’re trying to update your models upon query param changes, your setupController won’t fire… and thus your updated page won’t actually be fully updated with any data you were expecting to be set from setupController.

I’m beginning to think the solution is to just create another route… or something with render, although I’m not too familiar with render yet.