How to wire related models/controllers - ember.js

I'm writing an Ember application for storing "recommendations" about locations.
The user must select a location from a list (retrieved by a custom API), write a description, take a photo and make a POST with that data.
So I have a LocationModel and LocationController which retrieves the places from my API, display it on a list and allows the user to select one of them.
Then a DescriptionController and PhotoController which allow the user to write the description and take the photo.
I need to have each of these "parts" on individual controllers/models since I want them to reuse on others parts of my final app.
My Recommendation's model looks like this:
App.RecommendationModel = DS.Model.extend({
location: DS.BelongsTo('location'),
description: DS.BelongsTo('description'),
photo: DS.BelongsTo('photo')
});
The corresponding recommendation template:
<section id="select-location">
{{render "location" location}}
</section>
<section id="write-description">
{{render "description" description}}
</section>
<section id="take-photo">
{{render "photo" photo}}
</section>
<section id="send-recommendation">
{{render "share"}}
</section>
Each 'section' has a button with a 'complete' action:
Eg:
App.DescriptionController = Ember.Controller.extend({
actions: {
complete: function() {
_saveDescription(this.get('value'));
}
}
});
My problem is how to "assemble" this parts in order to fulfil the RecommendationModel.
I think that the complete action should be something like:
App.DescriptionController = Ember.Controller.extend({
actions: {
complete: function() {
recommendation.set('description', this);
}
}
});
Should the "complete" action be in the RecommendationController instead? If so, how should I wire each component?
Any other approach would be appreciated.
Thank you!

I believe that the 'global' complete action should be handled by the RecommendationController and each complete action should be handled by each controller (description, Photo and whatever).
What you can do is use the needs property in each controller that handles a part of the Recommendation:
needs: ['recommendation'],
And make the RecommendationRoute setup and use each controller and template of every part of the commendation like this:
App.RecommendationRoute = Ember.Route.extend({
setupController: function(controller, model) {
this._super();
var descController = this.controllerFor('description').set('content', model.get('description'));
var photoController = this.controllerFor('photo').set('content', model.get('photo'));
this.set('descController', descController).set('photoController', photoController);
...
},
renderTemplate: function() {
this._super();
this.render('description', {
into: 'application',
outlet: 'description',
controller: this.get('descController')
});
this.render('photo', {
into: 'application',
outlet: 'photo',
controller: this.get('photoController')
});
}
});
This way, each controller of a part of the recommendation can handle its complete action. All use the same recommendation model and they can call the RecommendationController whenever necessary like this:
this.get('controllers.recommendation')
For instance, you'd do this to enable the complete action once all the parts of the recommendation have been completed.
Finally, you can bind the action of the 'global' complete action in an element inside the recommendation template, or just add another render call inside renderTemplate in RecommendationRoute.
Hope it helps!

Related

How does one access model data in a router/controller?

Bear with me please, I'm new.
Been breaking my head over this problem and sort of here as last resort. It's about how to access a model's data when that route loads. For instance, when /meals/2 loads, I want a function to run that sets the background of the document using that model's background-image string property. Or when /meals loads, the a function that uses a property of the collection's first item.
Any help on 'the ember way' to do this would be much appreciated.
Menu.hbs
{{#each meal in model}}
<span {{action 'mealSelected' meal.image_large}}>
{{#link-to 'menu.meal' meal tagName="li" class="meal-block" href="view.href"}}
[...]
{{/link-to}}
</span>
{{/each}}
<div id="meal-info-wrapper">
{{outlet}}
</div>
Model:
export default DS.Model.extend({
name: DS.attr('string'),
image: DS.attr('string')
});
Router.js
export default Router.map(function() {
this.route('about');
this.route('menu', { path: '/' }, function() {
this.route('meal', { path: '/meal/:id/:slug' });
});
});
routes/menu.js
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function() {
Ember.$(document).anystretch('temp-images/bg-1.png');
}
});
What I want to do in routes/menu.js for instance would be to have that image url be supplied by the model.
afterModel will run only once the model has been resolved, and the model is passed as an argument. So, based on my understanding of your app, you can adjust your routes/menu example to:
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function(model) {
Ember.$(document).anystretch(model.get('firstObject.image'));
}
});
Correct me if I misunderstood something, what you want to do is:
Change the background image of a DOM element based on a property found
in each Model's record.
Model loading is an async operation, you want to do the image swaping once you are sure the data is loaded. You used the afterModel hook to guarantee that, but that is not enough.
You want to modify the DOM inside your template, but you need to make sure that the template has been rendered. So, the DOM manipulation logic, instead of placing it in afterModel, it belongs to the didInsertElement event that Views have.
I suggest you use a component (its a view too), something like:
// your template
{{#each meal in model}}
{{meal-component content=meal}}
{{/each}}
// the meal-component
didInsertElement: function() {
var imgURLProperty = this.get('content.imgURLProperty');
Ember.$(document).anystretch(imgURLProperty);
}
Of course, you can't copy paste any of that. It just shows you the main mechanic of how you can modify a template based on the properties of a model.

Accordion wizard with multiple steps in ember

I am using ember to built a "wizard accordion".
Basically what I want is:
an accordion which is always shown
the accordion contains all the steps
one step is active but it is also possibly to change the header of previous steps
each step has its own model (e.g. Selecting from countries in first step, selecting from products in second)
it should be possible to jump back and force between steps
out of all the selections a central model is built which is sent to the server after the final step
The number of steps is dynamic and sometimes certain steps are skipped
The architecture i thought about is have 3 outlets in the application template:
{{previousSteps}}
{{outlet}}
{{nextSteps}}
and always rendering 3 templates in each Route. Each step would have it's own route and controller and the create action of each step would save the shared model to application controller and transition to the next step.
But I guess this solution is by far not the best. Does somebody have a better architecture for this?
Especially how to structure the routes, controller and templates
Thank you
Update:
I am doing it the following way now. I appreciate any comments on my solution.
App.Router.map(function() {
this.resource('steps', { path: '/' });
});
App.StepsRoute = Ember.Route.extend({
model: function() {
return [
{ controllerName: 'countries', templateName: 'countries', items: this.store.find('country') },
{ controllerName: 'products', templateName: 'products', items: this.store.find('product') }
]
}
});
App.StepsController = Ember.ArrayController.extend({
currentStep: "countries",
transitionToStep: function(step) {
this.set('currentStep', step);
},
lookupItemController: function(modelObject) {
return modelObject.controllerName;
}
});
App.CountriesController = Ember.ObjectController.extend({
needs: ['steps'],
currentStep: Ember.computed.oneWay('controllers.steps.currentStep'),
isExpanded: function() {
return "countries" === this.get('currentStep');
}.property('currentStep')
});
Steps.handlebars:
{{#each step in controller}}
<div {{bind-attr class=":step controller.isExpanded:expanded"}}>
{{view Ember.View templateName=step.templateName}}
</div>
{{/each}}
I had to use ObjectController for Countries controller since using an ArrayController as itemController did not work
Based on the way you laid out the UI, I think I'd do something like this in the router:
App.Router.map(function() {
this.resource('wizard', function() {
this.route('step1');
this.route('step2');
this.route('step3');
});
});
Then, have a wizard template that looks like this:
{{#link-to 'wizard.step1'}}Step 1{{/link-to}}
<div {{bind-attr class='step1Expanded}}>{{outlet 'step1'}}</div>
{{#link-to 'wizard.step2'}}Step 2{{/link-to}}
<div {{bind-attr class='step2Expanded}}>{{outlet 'step2'}}</div>
{{#link-to 'wizard.step3'}}Step 3{{/link-to}}
<div {{bind-attr class='step3Expanded}}>{{outlet 'step3'}}</div>
Then, inside each of the step routes, you would need to override renderTemplate so that it will render in the appropriate outlet, like this:
App.WizardStep1Route = Ember.Route.extend({
renderTemplate: function() {
this.render({outlet: 'step1'});
}
});
Finally, inside the WizardController, you would need to add computed properties to handle the step1Expanded logic being applied to the classes so you can know which one is displaying at any given time.
All this will allow you to load different models per step and will also allow you to handle model validation logic inside your routes. For example, if you cannot proceed to step3 until step1 and step2 are completed, you can add logic to handle that inside the step1 and step2 routes.

Ember: How to set controller model for use in select view

See: http://jsfiddle.net/cyclomarc/VXT53/8/
I have a select view in which I simply want to list all the authors available in the fixtures data. I therefore try to use a separate controller in which I want to set the content = App.Author.find(), but that doesn't work ...
App.AuthorsController = Ember.ArrayController.extend({
//content: App.Store.findAll(App.Author)
//content: App.Author.find()
});
I then want to use the AuthorController for contentBinding in the selectView, but also this is not succsesful ...
{{view Ember.Select
contentBinding="App.AuthorsController"
optionValuePath="content.id"
optionLabelPath="content.name"
valueBinding="App.PublicationsEditController.author"
prompt="Select an author"
}}
The use case is that I have a publication in which an author is set (e.g. Marc) and I want to allow the user to change this to one of the available authors and then bind the new selected author to the publication model so that after a save the new author is saved.
I guess this what you are after: http://jsfiddle.net/VXT53/10/
I had to do a couple of changes, first your router map where slightly wrong, the edit segment had to go after the id to match your template name pulications/edit:
App.Router.map(function () {
this.resource('publications', { path: '/' }, function () {
this.route("edit", { path: "/edit/:publication_id" });
});
});
Your Ember.Select where binding to a class instead to some content, to set it up correctly I've defined a PublicationsEditControllet to requiere trough the needs API access to the AuthorsController's content:
App.PublicationsEditController = Ember.ObjectController.extend({
needs: ['authors'],
...
});
this is how you then use it for your select:
{{view Ember.Select
contentBinding="controllers.authors.content"
optionValuePath="content.id"
optionLabelPath="content.name"
valueBinding="content.name"
selectionBinding="selectedAuthor"
prompt="Select an author"
}}
Furthermore I've created a setupController hook which is used to set the AuthorsController's content to the list of authors:
App.PublicationsRoute = Ember.Route.extend({
model: function () {
return App.Publication.find();
},
setupController: function(controller, model) {
this._super(controller, model);
this.controllerFor('authors').set('content', App.Author.find());
}
});
And lastly on you PublicationsEditController is a new property attached selectedAuthor to hold the currently selected author for binding etc.
App.PublicationsEditController = Ember.ArrayController.extend({
needs: ['authors'],
selectedAuthor: null
});
I guess this should work now and brings you one step further.
Hope it helps.

Ember, Ember-data, and jquery-ui.dialog, "Oh my!"

The task:
Open a form in a lightbox to create a new "event"; the opened form should be bookmarkable.
The road blocks:
There are examples of opening a lightbox using {{action}} tags, but could not find one that opened in its own route.
There are many examples using older versions of ember.js.
There is not a lot of documentation related to ember-data and REST (I know, I know...it isn't "production ready").
The problem:
The fields in the form were not being tied to a backing model so "null" was being posted to my servlet (a Spring controller).
My very first iteration was not too far off from the final outcome (jsfiddle). The thing that finally made it works swapping this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
controller.set("model", model);
},
...
});
...for this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
this.controllerFor("events-new").set("model", model);
},
...
});
The question:
Why does the setupController function need to call controllerFor in order to properly set up the model?
And finally, since I struggled to find a fully-functional example, I wanted to make this accessible (and hopefully discover improvements).
Here's the fiddle: http://jsfiddle.net/6thJ4/1/
Here are a few snippets.
HTML:
<script type="text/x-handlebars">
<div>
<ul>
{{#linkTo "events.new" tagName="li"}}
Add Event
{{/linkTo}}
</ul>
</div>
{{outlet events-new}}
</script>
<script type="text/x-handlebars" data-template-name="events-new">
<form>
<div>
<label>Event Name:</label>
{{view Ember.TextField valueBinding="name"}}
</div>
<div>
<label>Host Name:</label>
{{view Ember.TextField valueBinding="hostName"}}
</div>
</form>
</script>
JavaScript:
...
EP.Router.map(function() {
this.resource("events", function() {
this.route("new");
});
});
EP.EventsNewRoute = Ember.Route.extend({
model : function() {
return EP.Event.createRecord();
},
setupController : function(controller, model) {
//controller.set("model", model); // Doesn't work? Why not?
this.controllerFor("events-new").set("model", model); // What does this do differently?
},
...
});
EP.EventsNewController = Ember.ObjectController.extend({
save : function() {
this.get("content.transaction").commit(); // "content.store" would commit _everything modified_, we only have one element changed, so only "content.transaction" is necessary.
}
});
EP.EventsNewView = Ember.View.extend({
...
});
EP.Event = DS.Model.extend({
name : DS.attr("string"),
hostName : DS.attr("string")
});
Resources:
http://emberjs.com/guides/routing/setting-up-a-controller/
http://emberjs.com/guides/getting-started/toggle-all-todos/ (trying to mimic what I learned, but morph the add-new to a new route)
Writing a LightboxView causes problems / Integrating DOM Manipulating jQuery-Plugins makes actions unusable (lightbox "example")
Dependable views in Ember (another lightbox "example" but doesn't have routes for the lightbox opening)
Why does the setupController function need to call controllerFor in order to properly set up the model?
Ember makes URLs a very integral part of its conventions. This means that the state of your application is represented by the route it is on. You've grokked most of this correctly. But there are couple of subtle nuances, that I will clarify below.
First consider an app with the following URLs,
/posts - shows a list of blog posts.
/posts/1 - shows a single blog post.
And say clicking on a post in the list at /posts takes you to /posts/1.
Given this scenario, there 2 ways a user will get to see the post at /posts/1.
By going to /posts and clicking on the 1st post.
By typing in /posts/1, via bookmarks etc.
In both these cases, the PostRoute for /posts/1 will need the model corresponding to Post id 1.
Consider the direct typing scenario first. To provide a way to lookup the id=1 post model, you would use,
model: function(params) {
return App.Post.find(params.post_id);
}
Your template for post will get the model and it can render using it's properties.
Now consider the second scenario. Clicking on post with id=1 takes you to /posts/1. To do this your template would use linkTo like this.
{{#linkTo 'post' post}} {{post.title}} {{/linkTo}}
Here you are passing in the post model to the linkTo helper. It then serializes the data for the post into a URL, ie:- '/posts/1'. When you click on this link Ember realizes that it needs to render the PostRoute but it already has the post model. So it skips the model hook and directly calls setupController.
The default setupController is setup to simply assign the model on the controller. It's implemented to do something like,
setupController: function(controller, model) {
controller.set('model', model);
}
If you do not need to set custom properties on your controller, you don't need to override it. Note: if you are augmenting it with additional properties you still need to call _super to ensure that the default setupController behaviour executes.
setupController: function(controller, model) {
this._super.apply(this, arguments);
controller.set('customProp', 'foo');
}
One final caveat, If you are using linkTo and the route does not have dynamic segments, then the model hook is still called. This exception makes sense if you consider that you were linking to the /posts route. Then the model hook has to fire else Ember has no data to display the route.
Which brings us to the crux of your question. Nearly there, I promise!
In your example you are using linkTo to get to the EventsNewRoute. Further your EventsNewRoute does not have dynamic segments so Ember does call the model hook. And controller.set("model", model); does work in so much as setting the model on the controller.
The issue is to do with your use of renderTemplate. When you use render or {{render}} helper inside a template, you are effectively getting a different controller to the one you are using. This controller is different from the one you set the model on, hence the bug.
A workaround is to pass the controller in the options, which is why renderTemplate gets this controller as an argument.
renderTemplate: function(controller) {
this.render("events-new", {
outlet : "events-new", controller: controller
});
}
Here's an updated jsfiddle.
Final Note: Unrelated to this question, you are getting the warning,
WARNING: The immediate parent route ('application') did not render into the main outlet and the default 'into' option ('events') may not be expected
For that you need to read this answer. Warning, it's another wall of text! :)

How to use multiple models with a single route in EmberJS / Ember Data?

From reading the docs, it looks like you have to (or should) assign a model to a route like so:
App.PostRoute = Ember.Route.extend({
model: function() {
return App.Post.find();
}
});
What if I need to use several objects in a certain route? i.e. Posts, Comments and Users? How do I tell the route to load those?
Last update forever: I can't keep updating this. So this is deprecated and will likely be this way. here's a better, and more up-to-date thread EmberJS: How to load multiple models on the same route?
Update: In my original answer I said to use embedded: true in the model definition. That's incorrect. In revision 12, Ember-Data expects foreign keys to be defined with a suffix (link) _id for single record or _ids for collection. Something similar to the following:
{
id: 1,
title: 'string',
body: 'string string string string...',
author_id: 1,
comment_ids: [1, 2, 3, 6],
tag_ids: [3,4]
}
I have updated the fiddle and will do so again if anything changes or if I find more issues with the code provided in this answer.
Answer with related models:
For the scenario you are describing, I would rely on associations between models (setting embedded: true) and only load the Post model in that route, considering I can define a DS.hasMany association for the Comment model and DS.belongsTo association for the User in both the Comment and Post models. Something like this:
App.User = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
email: DS.attr('string'),
posts: DS.hasMany('App.Post'),
comments: DS.hasMany('App.Comment')
});
App.Post = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
author: DS.belongsTo('App.User'),
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.extend({
body: DS.attr('string'),
post: DS.belongsTo('App.Post'),
author: DS.belongsTo('App.User')
});
This definition would produce something like the following:
With this definition, whenever I find a Post, I will have access to a collection of comments associated with that post, and the comment's author as well, and the user which is the author of the post, since they are all embedded. The route stays simple:
App.PostsPostRoute = Em.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
}
});
So in the PostRoute (or PostsPostRoute if you're using resource), my templates will have access to the controller's content, which is the Post model, so I can refer to the author, simply as author
<script type="text/x-handlebars" data-template-name="posts/post">
<h3>{{title}}</h3>
<div>by {{author.fullName}}</div><hr />
<div>
{{body}}
</div>
{{partial comments}}
</script>
<script type="text/x-handlebars" data-template-name="_comments">
<h5>Comments</h5>
{{#each content.comments}}
<hr />
<span>
{{this.body}}<br />
<small>by {{this.author.fullName}}</small>
</span>
{{/each}}
</script>
(see fiddle)
Answer with non-related models:
However, if your scenario is a little more complex than what you described, and/or have to use (or query) different models for a particular route, I would recommend to do it in Route#setupController. For example:
App.PostsPostRoute = Em.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
// in this sample, "model" is an instance of "Post"
// coming from the model hook above
setupController: function(controller, model) {
controller.set('content', model);
// the "user_id" parameter can come from a global variable for example
// or you can implement in another way. This is generally where you
// setup your controller properties and models, or even other models
// that can be used in your route's template
controller.set('user', App.User.find(window.user_id));
}
});
And now when I'm in the Post route, my templates will have access to the user property in the controller as it was set up in setupController hook:
<script type="text/x-handlebars" data-template-name="posts/post">
<h3>{{title}}</h3>
<div>by {{controller.user.fullName}}</div><hr />
<div>
{{body}}
</div>
{{partial comments}}
</script>
<script type="text/x-handlebars" data-template-name="_comments">
<h5>Comments</h5>
{{#each content.comments}}
<hr />
<span>
{{this.body}}<br />
<small>by {{this.author.fullName}}</small>
</span>
{{/each}}
</script>
(see fiddle)
Using Em.Object to encapsulate multiple models is a good way to get all data in model hook. But it can't ensure all data is prepared after view rendering.
Another choice is to use Em.RSVP.hash. It combines several promises together and return a new promise. The new promise if resolved after all the promises are resolved. And setupController is not called until the promise is resolved or rejected.
App.PostRoute = Em.Route.extend({
model: function(params) {
return Em.RSVP.hash({
post: // promise to get post
comments: // promise to get comments,
user: // promise to get user
});
},
setupController: function(controller, model) {
// You can use model.post to get post, etc
// Since the model is a plain object you can just use setProperties
controller.setProperties(model);
}
});
In this way you get all models before view rendering. And using Em.Object doesn't have this advantage.
Another advantage is you can combine promise and non-promise. Like this:
Em.RSVP.hash({
post: // non-promise object
user: // promise object
});
Check this to learn more about Em.RSVP: https://github.com/tildeio/rsvp.js
But don't use Em.Object or Em.RSVP solution if your route has dynamic segments
The main problem is link-to. If you change url by click link generated by link-to with models, the model is passed directly to that route.
In this case the model hook is not called and in setupController you get the model link-to give you.
An example is like this:
The route code:
App.Router.map(function() {
this.route('/post/:post_id');
});
App.PostRoute = Em.Route.extend({
model: function(params) {
return Em.RSVP.hash({
post: App.Post.find(params.post_id),
user: // use whatever to get user object
});
},
setupController: function(controller, model) {
// Guess what the model is in this case?
console.log(model);
}
});
And link-to code, the post is a model:
{{#link-to "post" post}}Some post{{/link-to}}
Things become interesting here. When you use url /post/1 to visit the page, the model hook is called, and setupController gets the plain object when promise resolved.
But if you visit the page by click link-to link, it passes post model to PostRoute and the route will ignore model hook. In this case setupController will get the post model, of course you can not get user.
So make sure you don't use them in routes with dynamic segments.
For a while I was using Em.RSVP.hash, however the problem I ran into was that I didn't want my view to wait until all models were loaded before rendering. However, I found a great (but relatively unknown) solution thanks to the folks at Novelys that involves making use of the Ember.PromiseProxyMixin:
Let's say you have a view that has three distinct visual sections. Each of these sections should be backed by its own model. The model backing the "splash" content at the top of the view is small and will load quickly, so you can load that one normally:
Create a route main-page.js:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('main-stuff');
}
});
Then you can create a corresponding Handlebars template main-page.hbs:
<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
<li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
<h1>Reasons I Love Cheese</h1>
</section>
<section>
<h1>Reasons I Hate Cheese</h1>
</section>
So let's say in your template you want to have separate sections about your love/hate relationship with cheese, each (for some reason) backed by its own model. You have many records in each model with extensive details relating to each reason, however you'd like the content on top to render quickly. This is where the {{render}} helper comes in. You can update your template as so:
<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
<li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
<h1>Reasons I Love Cheese</h1>
{{render 'love-cheese'}}
</section>
<section>
<h1>Reasons I Hate Cheese</h1>
{{render 'hate-cheese'}}
</section>
Now you'll need to create controllers and templates for each. Since they're effectively identical for this example, I'll just use one.
Create a controller called love-cheese.js:
import Ember from 'ember';
export default Ember.ObjectController.extend(Ember.PromiseProxyMixin, {
init: function() {
this._super();
var promise = this.store.find('love-cheese');
if (promise) {
return this.set('promise', promise);
}
}
});
You'll notice that we are using the PromiseProxyMixin here, which makes the controller promise-aware. When the controller is initialized, we indicate that the promise should be loading the love-cheese model via Ember Data. You'll need to set this property on the controller's promise property.
Now, create a template called love-cheese.hbs:
{{#if isPending}}
<p>Loading...</p>
{{else}}
{{#each item in promise._result }}
<p>{{item.reason}}</p>
{{/each}}
{{/if}}
In your template, you'll be able to render different content depending on the state of promise. When your page initially loads, your "Reasons I Love Cheese" section will display Loading.... When the promise is loaded, it will render all the reasons associated for each record of your model.
Each section will load independently and not block the main content from rendering immediately.
This is a simplistic example, but I hope everyone else finds it as useful as I did.
If you're looking to do something similar for many rows of content, you may find the Novelys example above even more relevant. If not, the above should work fine for you.
This might not be best practice and a naïve approach, but it shows conceptually how you would go about having on multiple models available on one central route:
App.PostRoute = Ember.Route.extend({
model: function() {
var multimodel = Ember.Object.create(
{
posts: App.Post.find(),
comments: App.Comments.find(),
whatever: App.WhatEver.find()
});
return multiModel;
},
setupController: function(controller, model) {
// now you have here model.posts, model.comments, etc.
// as promises, so you can do stuff like
controller.set('contentA', model.posts);
controller.set('contentB', model.comments);
// or ...
this.controllerFor('whatEver').set('content', model.whatever);
}
});
hope it helps
Thanks to all the other excellent answers, I created a mixin that combines the best solutions here into a simple and reusable interface. It executes an Ember.RSVP.hash in afterModel for the models you specify, then injects the properties into the controller in setupController. It does not interfere with the standard model hook, so you would still define that as normal.
Example use:
App.PostRoute = Ember.Route.extend(App.AdditionalRouteModelsMixin, {
// define your model hook normally
model: function(params) {
return this.store.find('post', params.post_id);
},
// now define your other models as a hash of property names to inject onto the controller
additionalModels: function() {
return {
users: this.store.find('user'),
comments: this.store.find('comment')
}
}
});
Here is the mixin:
App.AdditionalRouteModelsMixin = Ember.Mixin.create({
// the main hook: override to return a hash of models to set on the controller
additionalModels: function(model, transition, queryParams) {},
// returns a promise that will resolve once all additional models have resolved
initializeAdditionalModels: function(model, transition, queryParams) {
var models, promise;
models = this.additionalModels(model, transition, queryParams);
if (models) {
promise = Ember.RSVP.hash(models);
this.set('_additionalModelsPromise', promise);
return promise;
}
},
// copies the resolved properties onto the controller
setupControllerAdditionalModels: function(controller) {
var modelsPromise;
modelsPromise = this.get('_additionalModelsPromise');
if (modelsPromise) {
modelsPromise.then(function(hash) {
controller.setProperties(hash);
});
}
},
// hook to resolve the additional models -- blocks until resolved
afterModel: function(model, transition, queryParams) {
return this.initializeAdditionalModels(model, transition, queryParams);
},
// hook to copy the models onto the controller
setupController: function(controller, model) {
this._super(controller, model);
this.setupControllerAdditionalModels(controller);
}
});
https://stackoverflow.com/a/16466427/2637573 is fine for related models. However, with recent version of Ember CLI and Ember Data, there is a simpler approach for unrelated models:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller,model);
var model2 = DS.PromiseArray.create({
promise: this.store.find('model2')
});
model2.then(function() {
controller.set('model2', model2)
});
}
});
If you only want to retrieve an object's property for model2, use DS.PromiseObject instead of DS.PromiseArray:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller,model);
var model2 = DS.PromiseObject.create({
promise: this.store.find('model2')
});
model2.then(function() {
controller.set('model2', model2.get('value'))
});
}
});
Adding to MilkyWayJoe's answer, thanks btw:
this.store.find('post',1)
returns
{
id: 1,
title: 'string',
body: 'string string string string...',
author_id: 1,
comment_ids: [1, 2, 3, 6],
tag_ids: [3,4]
};
author would be
{
id: 1,
firstName: 'Joe',
lastName: 'Way',
email: 'MilkyWayJoe#example.com',
points: 6181,
post_ids: [1,2,3,...,n],
comment_ids: [1,2,3,...,n],
}
comments
{
id:1,
author_id:1,
body:'some words and stuff...',
post_id:1,
}
...
I believe the link backs are important so that the full relationship is established. Hope that helps someone.
You could use the beforeModel or afterModel hooks as these are always called, even if model is not called because you're using dynamic segments.
As per the asynchronous routing docs:
The model hook covers many use cases for pause-on-promise transitions, but sometimes you'll need the help of the related hooks beforeModel and afterModel. The most common reason for this is that if you're transitioning into a route with a dynamic URL segment via {{link-to}} or transitionTo (as opposed to a transition caused by a URL change), the model for the route you're transitioning into will have already been specified (e.g. {{#link-to 'article' article}} or this.transitionTo('article', article)), in which case the model hook won't get called. In these cases, you'll need to make use of either the beforeModel or afterModel hook to house any logic while the router is still gathering all of the route's models to perform a transition.
So say you have a themes property on your SiteController, you could have something like this:
themes: null,
afterModel: function(site, transition) {
return this.store.find('themes').then(function(result) {
this.set('themes', result.content);
}.bind(this));
},
setupController: function(controller, model) {
controller.set('model', model);
controller.set('themes', this.get('themes'));
}