Dashboard type views

I’m trying to understand the best way to lay out an app where some of the pages will require multiple models.

For example, my home page needs to make 3 api calls to get projects, users, and recent activity.

How would I represent that in code. Do I create something like this:

export default Ember.Route.extend({
   model: function() {
     return Ember.RSVP.hash({
        projects: this.store.find('project'),
        users: this.store.find('user'),
        activities: this.store.find('activity')
     });
   }
});

or is there a better way to do this?

6 Likes

That is precisely how I handle it.

The only catch I’ve found is that typically with something like this, I would want the controller to be an ArrayController so that I can define an itemController…but, with this, you must use an ObjectController. So, you can do something like this to wrap the results into an ArrayController. It seems to work well (though, not sure if this is the best way to do it):

// controllers/index.js
export default Ember.ObjectController.extend({
  projectsProxy: Em.computed(function()
    return this.container.lookupFactory("controller:array").create({
      parentController: this,
      itemController: "projectItem",
      contentBinding: "parentController.projects"
    });
  )
});

There’s certainly some flaws…but it does the trick.

yep, RSVP is how you handle multiple model promises.

Though in order to access the models and instead of having to do model.users, I extend this little route to just be able to access users:

var RSVPRoute = Ember.Route.extend({
  setupController: function(controller, model) {
    for (var name in model) {
      controller.set(name, model[name]);
    }
  }
});
1 Like

Hey @jhsu …is that necessary to do? As long as you’re using an ObjectController , then users will be automagically available without having to do model.users. (ObjectController implements ObjectProxy).

@Spencer_Price ah you are right… i was using just Ember.Controller, thanks.

In case anyone else comes across it, I found a little gem in the source code that makes my solution a little more palatable…one of the items in the proxy array would have to refer to it’s parentController.parentController in order to get properties from the route’s main controller.

However, there’s a (private) property _isVirutal that removes it from the chain so it will work exactly as expected.

// controllers/index.js
export default Ember.ObjectController.extend({
  projectsProxy: Em.computed(function()
    return this.container.lookupFactory("controller:array").create({
      parentController: this,
      itemController: "projectItem",
      _isVirtual: true,
      contentBinding: "parentController.projects"
    });
  )
});

I’d recommend a different approach to any of these. It seems you are going through a lot of ugly code to do something that should be relatively simple.

Using RSVP in the model hook looks good, however IMO the best way to deal with the returned promise is to assign the data to the appropriate controllers in the setupController hook, like so:

export default Ember.Route.extend({
   model: function() {
     return Ember.RSVP.hash({
        projects: this.store.find('project'),
        users: this.store.find('user'),
        activities: this.store.find('activity')
     });
   },

  setupController: function (controller, context) {
    this.controllerFor('projects').set 'model', context.projects;
    this.controllerFor('users').set 'model', context.users;
    this.controllerFor('activities').set 'model', context.activities;
  }
});

You can do this slightly less verbosely using something like @jhsu’s technique:

var RSVPRoute = Ember.Route.extend({
  setupController: function(controller, model) {
    for (var name in model) {
      this.controllerFor(name).set('model', model[name]);
    }
  }
});

Controllers are supposed to wrap an object or a collection, which should be assigned to their model property. The techniques described in previous posts assign the objects as arbitrary properties of a single controller meaning they’re not wrapped in their respective controllers, which are designed for presenting data to templates.

Using the approach I suggest in this post means you can take advantage of all the features of controllers e.g. sortProperties in array controllers, and if desired can use the {{render}} helper to render the appropriate controller, view and model in your dashboard template.

20 Likes

Hey @alexspeller — thanks for this. This does seem like the more “Ember” way of handling this scenario.

That being said, it seems like this solution assumes that the different models are to be part of discreet areas/widgets in the UI. In my use case, I’m often combining the models in some way to be presented in a single part of the UI…say, for example, that I’m combining different data series in a graph.

If we go with the way you’d suggested in the route, I might end up with something like this in my controller. Are there any pitfalls I might be missing? Any thoughts on wether this is the right way to handle it? (This is essentially what the method above gets me; but, less clean and without named controllers).

export default Ember.Controller.extend({
  needs: ['projects', 'users', 'activities'],
  allModels: Em.computed.union(
    'controllers.projects',
    'controllers.users.',
    'controllers.activities'),
  values: Em.computed.mapBy('allModels','value'),
  min: Em.computed.min('values'),
  max: Em.computed.max('values')
});

Yes, that’s the right way to handle it, using the needs mechanism is definitely the way to go for linking different data together. If you find you’re using the properties a lot in the same controller, you can use Em.computed.alias to make it a bit less verbose, but it’s not really worth it if you only access the properties once.

export default Ember.Controller.extend({
  needs: ['projects', 'users', 'activities'],
  projects: Em.computed.alias('controllers.projects')
  //etc
})
1 Like

Thanks for all the helpful tips. This is a lot of good information.

This is a great solution. However this brings a question in my mind: when do you use these nested models vs using the render helper to bring the different pieces of data?

Great. Thanks for the awesome tip.

Hi Alex,

Thanks for your post.

TLDR; I would like to display all panels right away and have each display its own loading or error substate.

Following your solution, the dashboard index route displays its loading substate until ALL the promises resolve and then all panels in the dashboard display at once.

Since I wanted to give the user more immediate feedback, my approach is to render all the templates into their named outlets from the dashboard index route:

In my dashboard index route:

export default Em.Route.extend({

  renderTemplate: function() {
    this.render();

    this.render('dashboard/cars', {
      into: 'dashboard.index',
      outlet: 'cars',
      controller: 'dashboard.cars'
    });

    this.render('dashboard/boats', {
      into: 'dashboard.index',
      outlet: 'boats',
      controller: 'dashboard.boats'
    });
  }
});

The corresponding dashboard index template:

<h3>My Dashboard</h3>

<div class="panel">
  {{outlet "cars"}}
</div>

<div class="panel">
  {{outlet "boats"}}
</div>

And then make the calls from the respective controllers:

export default Ember.ArrayController.extend({
  content: function() {
    return this.store.find('car');
  }.property()
});

.....

export default Ember.ArrayController.extend({
  content: function() {
    return this.store.find('boat');
  }.property()
});

This does show the user all (empty) panels on the page right away and they each fill in as their promises resolve, which is nice.

However, it would be much nicer to display the loading or error substate inside each panel template. Am I correct in assuming that those substates are only available to routes? Is this approach not the prescribed way to compose independently backed panels?

Im relatively new, so I appreciate any feedback.

Thank you.

8 Likes

Having the exact same problem. How did you end up solving this? Would love to make use of loading/error substates but it seems it’s just not possible when rendering to named outlets.

@migbar @rytis I met the similar problem. And in this case I don’t think route loading/error substate can solve it. In current Ember design, multiple route can not be used at the same time in the same level, and URL also can not express this kind of status.

So my solution is simllar as yours (using controllers), but with some differences:

  1. Use dashboard route to load data, but in order to show UI as quickly as possible, I use setupController instead of model hook.
  2. Use PromiseProxyMixin with ArrayController to give controller isFulfilled, isRejected, isPending status. It will be convenient to display loading/error status or real data in template.

The dashboard route and controllers:

// routes/dashboard.js
export default Ember.Route.extend({
  setupController: function() {
    this.controllerFor('dashboard.cars').set('promise', this.store.find('car'))
    this.controllerFor('dashboard.boat').set('promise', this.store.find('boat'))
  }
});

// controllers/dashboard/cars.js
export default Ember.ArrayController.extend(Ember.PromiseProxyMixin, {
});

// controllers/dashboard/boat.js
export default Ember.ArrayController.extend(Ember.PromiseProxyMixin, {
});

The dashboard templates:

<h3>My Dashboard</h3>

<div class="panel">
  {{render 'dashboard/cars'}}
</div>
<div class="panel">
  {{render 'dashboard/boats'}}
</div>

And dashboard/cars template:

{{#if isFulfilled}}
  {{#each}}
    Display a single car
  {{/each}}
{{/if}}

{{#if isPending}}
  Loading...
{{/if}}

{{#if isRejected}}
  Display error
{{/if}}

You can also wrap the promise logic in template to a component like this:

{{#promise-content content=controller}}
  Display controller content
{{/promise-content}}

And in component:

{{#if content.isFulfilled}}
  {{yield}}
{{/if}}

{{#if isPending}}
  Loading...
{{/if}}

{{#if isRejected}}
  Display error
{{/if}}

I expect there is a better solution. Because sometimes I use route’s loading/error substate and the other time I use PromiseProxyMixin. I expect anyone can work out a unified solution for loading data.

11 Likes

@darkbaby123 your solution looks good.

I use setupController instead of model hook.

Why to use the setupController hook? You can use the model hook itself right?

The requirement is: On dashboard page there’re several sections. For user experience I want each section loads it’s own data and has it’s own loading state. But not the whole page shows “Loading” unless all section’s data is resolved. I don’t think model hook and Ember route’s loading/error substates can do this.

When the model hook doesn’t return a promise, loading/error substates will not be in action. I just thought of moving the code section in setupController to model hook. This way we can preserve the convention of fetching the model from the model hooks

Just a simple note :smile:

Hello, probably a super noob question here but why are you guys prefixing your model/route/controller definitions with “export default”? I don’t see that in many tutorials.

Thanks!

Hey @supairish,

If you’re using ember-cli, then you’ll be using the ES6 module syntax (which is later converted/transpiled to “regular” javascript). This syntax includes those import and export statements.

Even if you’re new-ish to Ember, I’d really recommend taking a look at ember-cli and consider using it to build your projects. It’s pretty awesome.