ember.js, ember-cli: Outlets not nesting properly - ember.js

I'm having an issue where I'm unable to get nested outlets to appear properly in my Ember CLI app. The view tree I want is as follows:
application (list of all resources, of which client_availability is one)
- client_availabilities.index (list of client_availabilities)
-- client_availability (individual client_availability)
This is very similar to the "application > posts.index > post" hierarchy in the Ember Starter Kit. My desired behavior is for a list of client_availabilities to appear in "mainoutlet" when I navigate to client_availabilities.index, then persist when I bring up an individual client_availability in "suboutlet".
Easy, right? This is the default behavior & why we all love Ember. However, I can't seem to get it working. When I explicitly target my named suboutlet in client_availabilities.index and click on an individual client_availability, nothing shows up in either outlet:
Scenario 1: Render suboutlet inside client_availabilities
/app/template/application.hbs:
{{link-to 'Client Availabilities' 'client_availabilities'}}
{{outlet 'mainoutlet'}}
/app/template/client-availabilities/index.hbs:
{{outlet 'suboutlet'}}
/app/routes/client-availabilities/index.js:
import Ember from 'ember';
export default Ember.Route.extend({
renderTemplate: function(){
this.render({
into: "application",
outlet: "mainoutlet"
});
},
model: function() {
return this.store.find('client_availability');
}
});
/app/routes/client-availability.js:
import Ember from 'ember';
export default Ember.Route.extend({
renderTemplate: function(){
this.render('client_availability', {
into: "client_availabilities",
outlet: "suboutlet"
});
},
model: function(params) {
return this.store.find('client_availability', params.client_availability_id);
}
});
Alternately, when I target my mainoutlet in application, client_availability appears in "suboutlet" client_availabilities.index disappears from "mainoutlet":
Scenario 2: Render suboutlet inside application
/app/template/application.hbs:
{{link-to 'Client Availabilities' 'client_availabilities'}}
{{outlet 'mainoutlet'}}
{{outlet 'suboutlet'}}
/app/template/client-availabilities/index.hbs: (empty)
/app/routes/client-availabilities/index.js:
import Ember from 'ember';
export default Ember.Route.extend({
renderTemplate: function(){
this.render({
into: "application",
outlet: "mainoutlet"
});
},
model: function() {
return this.store.find('client_availability');
}
});
/app/routes/client-availability.js:
import Ember from 'ember';
export default Ember.Route.extend({
renderTemplate: function(){
this.render('client_availability', {
into: "application",
outlet: "suboutlet"
});
},
model: function(params) {
return this.store.find('client_availability', params.client_availability_id);
}
});
And here's my router, the same in both cases:
/app/router.js:
import Ember from 'ember';
var Router = Ember.Router.extend({
location: 'auto'
});
Router.map(function() {
this.resource('client_availabilities', function() {
this.resource('client_availability', { path: ':client_availability_id' });
});
});
export default Router;
I'm happy to share more code, but the application is split into several files and unfortunately not something I can post in its entirety. Can anyone see what I'm doing wrong? The rest of the app is working fine, I just can't seem to get this basic behavior to work.

Do you have an /app/templates/client-availibilities.hbs template with only {{outlet}} inside of it? Without this, the app is going to lose its place in the outlet tree. Ember-CLI and the Ember Starter Kit are very, very different from each other in structure, so I can see where the confusion comes from.
How I like to think of Ember's rendering style is that each handlebars file inside the templates folder (i.e. /templates/users.hbs) represents a change the overall state of the application from one subject to another (example: from newsfeed to users).
The corresponding subfolders inside the templates folder change the state of the subject itself.
For example:
Required Templates
Users container OR the only users page you need app-wide is at /templates/users.hbs
Optional Templates
Users Index would be at /templates/users/index.hbs
Users Show would be at /templates/users/show.hbs
Users New would be at /templates/users/new.hbs
You can have [ /templates/users.hbs ] without having [ /templates/users/*.hbs ] and still keep track of your data; however, you cannot have [ templates/users/index.hbs ] without [ /templates/users.hbs ] and still keep track of your data. Why? Imagine if you navigate to somesite.com/users. There is currently no top-level template with an outlet into which Ember can render the [ users/index.hbs ] template. The [ /templates/users.hbs ] template bridges that gap and also serves as a container for all other pages inside the /templates/users folder as well.
For example, in the terms of your app, in order to render [ /app/templates/client-availibilities/index.hbs ] when a user visits http://www.yourwebsite.com/client-availibilities, your app will need these templates defined so that ember can drill down into them.
application.hbs // and in its outlet, it will render...
--client-availibilities.hbs // and in its outlet, it will render by default...
----client-availibilities/index.hbs // then, for the client-availability (singular), you can have ember render it in
----client-availibilities/show.hbs // will render also in the client-availabilites as it is a separate state of the subject. Can also be nested inside the index route within the router so that it renders inside the index template.
As it is, I would structure your app as such...
/app/router.js
... // previous code
Router.map(function() {
this.resource('client_availabilities', function() {
this.route('show', { path: '/:client_availability_id' });
// this.route('new'); ! if needed !
// this.route('edit', { path: '/:client_availability_id/edit' ); ! if needed !
});
});
... // code
/app/templates/application.hbs
{{link-to 'Client Availabilities' 'client_availabilities'}}
{{outlet}}
/app/templates/client-availabilities.hbs
{{outlet}}
/app/templates/client-availabilities/index.hbs
<ul>
{{#each}}
{{#if available}}
<li>
{{#link-to #link-to 'client-availabilities.show' this}}
{{firstName}} {{lastName}}
{{/link-to}}
</li>
{{/if}}
{{else}} <!-- we want this to only render if the each loop returns nothing, which is why it's outside the if statement -->
<li>Nobody is available</li>
{{/each}}
</ul>
<!-- Note: you don't need to put an outlet here because you're at the end of the tree -->
/app/templates/client-availabilities/show.hbs
<!-- Everything you want to show about each availability -->>
<!-- Note: you don't need to put an outlet here because you're at the end of the tree -->
/app/routes/client-availabilities/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('client_availability');
}
});
/app/routes/client-availabilities/show.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return this.store.find('client-availability', params.client_availability_id);
}
});
/app/models/client-availability.js
import DS from 'ember-data';
var client-availability = DS.Model.extend({
firstName: DS.attr('string'),
lastname: DS.attr('string'),
available: DS.attr('boolean'),
available_on: DS.attr('date')
});
export default client-availability;
However, are you sure you want to structure your app by the availability of each client? Wouldn't it make more sense to structure it by each client and then just filter each client to show if they were available or not? Resources are supposed to be nouns, and routes are supposed to be adjectives. Therefore, it would be best to use a client as your model instead of their availability and have a either an isAvailable property on the model (as used in the example above) or a one-to-many association with an additional availability model if you want to show clients who have several availabilities (as shown below).
For example,
/app/models/client.js
import DS from 'ember-data';
var Client = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
availabilities: DS.hasMany('availability')
});
export default Client;
/app/models/availability.js
import DS from 'ember-data';
var Availability = DS.Model.extend({
date: DS.attr('date'),
client: DS.belongsTo('client')
});
export default Availability;
In the long run, this latter approach would set up your app to show all availabilities at once and allow the user to filter by the client, plus it would allow the user to view a client and see all their availabilities. With the original approach (the isAvailable property on the client model), the user can only get the availabilities from the client model itself, but what if the user wants to see all clients who are available on, say, March 3rd at noon? Well, without an availability model associated with the client model, you are going to have to put a lot of code into your client controller that ember would give you by default if you go down the one-to-many path.
If you need more advice on where to go from here, let me know. I'm more than happy to add more examples of the templates, controllers, and routes that you'll need in order to pull this off.

Related

How can I avoid "Assertion Failed: `id` passed to `findRecord()` has to be non-empty string or number" when refreshing an ember page?

There's this really annoying feature about Ember that I'm not sure how to get around. I may have a url that looks like the following
http://{my-blog-name}/posts/view/{some-blogpost-ID}
The way I get to this page is by clicking on a link inside of my {my-blog-name}/posts page. This works and will display the page as expected. However, if I try to refresh the page, or if I just literally type my http://{my-blog-name}/posts/view/{some-blogpost-ID} into my url search box, I will get
Assertion Failed: `id` passed to `findRecord()` has to be non-empty string or number
Here is how I navigate to the posts/view/{some-blog-id} page.
post.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('post');
}
});
posts.hbs
<li class="title-list-item">{{#link-to "posts.view" posts}}{{posts.title}}{{/link-to}}</li>
view.js
import Ember from 'ember';
var siteId;
export default Ember.Route.extend({
model(params) {
siteId = params.site_id;
return this.store.findRecord('post', params.site_id);
}
});
view.hbs
<div id="Links">
<h1 id="blog-header-title">My Blog</h1>
<!--<p>{{!#link-to 'welcome'}} See about me{{!/link-to}}</p>-->
{{outlet}}
</div>
{{outlet}}
router.js
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('index', { path: '/' }); // This is usually automatic if path undeclared, but declared here to support /index below
this.route('posts', function() {
this.route('view', {path: '/view/:post_id'});
});
this.route('welcome');
}
This is really frustrating because it means I can't make a blog post and share the link with a friend. Why does this happen and is there a good way to get around it?
posts.js route is returning all the posts available, that's the RecordArray.
<li class="title-list-item">{{#link-to "posts.view" posts}}{{posts.title}}{{/link-to}}</li>
so in the above posts - refers to single post model or RecordArray of post model ?. if the above is single model then you will receive params.post_id in model hook of view.js, currently you are taking params.site_id instead of params.post_id.
Reason for not executing the model hook.
https://guides.emberjs.com/v2.13.0/routing/specifying-a-routes-model/#toc_dynamic-models
Note: A route with a dynamic segment will always have its model hook
called when it is entered via the URL. If the route is entered through
a transition (e.g. when using the link-to Handlebars helper), and a
model context is provided (second argument to link-to), then the hook
is not executed. If an identifier (such as an id or slug) is provided
instead then the model hook will be executed.

Ember loading template works only on page refresh

I have in my Ember app this routes:
Router.map(function() {
// landing page (login/register)
this.route('home', {path: '/'});
// authenticated pages
this.route('webappFrame', {path: '/app'}, function() {
this.route('feed', {path: 'feed'});
this.route('photoDetail', {path: 'photo/detail/:id'});
});
});
where both "feed" and "photoDetail" have a model hook that return a promise (records coming from the server);
I have a loading.hbs template in the /template folder;
After a page refresh on both "feed" and "photoDetail" routes, the loading template is correctly displayed;
but when navigating between the routes, it is not shown again (the outlet remains white untile the promise is resolved);
I use ember-cli 2.3.0-beta.1 (but also tried 1.13.14 stable) with ember 2.3.0;
In the official ember docs is written that it should always traverse the templates tree up until it finds a loading template;
can someone show me what's wrong here?
UPDATE ----------------------------------------------------
// feed model hook
model: function(params, transition) {
return this.store.query('photo', {type: 'feed'});
}
// photoDetail model hook
model: function(params, transition) {
var self = this;
return Ember.RSVP.hash({
photo: self.store.find('photo', params.id),
comments: self.store.query('comment', {id: params.id})
});
},
and in feed template:
{{#each photo as |item|}}
{{photo-item item=item}}
{{/each}}
and then the photo-item component:
{{#link-to "webappFrame.photoDetail" item.id}}
<img class="photoImage" src="{{imageurl item.hash type='photo'}}">
{{/link-to}}
where {{imageurl}} is just an helper that creates the path for the image;
here i pass "item.id" to the link-to (instad of passing "item" itself) to force the reload of the photo when entering detail (in order to get also comments)
right now, I've added templates/webapp-frame/loading.hbs and it works;
the thing is that the loading template is always the same in the whole app; so I expected to have only one instead of having a copy of it in every /templates subfolder...
Ember handles the loading templates per route, its always "routename-loading.hbs", it seems you have not created the matching templates that´s why you only see a white page (default). If you´re not sure which templates are necessary or how they have to be named, install ember inspector and check the routes tab, there you can see what ember expects and how it should be named.

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.

Unable to use value from an Ember Data store in template

I'm building a simple application using Ember v1.8.1, Ember CLI 0.1.15 and Ember Data 1.0.0-beta.15 in an attempt to learn the basics of Ember.js. Data is coming from a simple REST API, so I'm using the DS.RESTAdapter and DS.RESTSerializer. The payload is normalized in the serializer, because the API doesn't properly wrap its data in the required hashes.
I'm trying to implement an edit route, but am unable to populate the form fields of that template or even display a simple value. My index route, displaying a list of posts, works as expected, but edit doesn't for some reason.
Using {{title}} in my template doesn't render the value, but outputs a string of unexpected data, something along the lines of: <my-app#model:post::ember413:21>. I'm probably passing something improperly from the route or elsewhere, but I don't know what and where exactly.
Below is some of my relevant code:
app/router.js
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('login');
this.resource('posts', function() {
this.route('add');
this.route('view', { path: ':post_id'});
this.route('edit', { path: ':post_id/edit'});
});
});
export default Router;
app/routes/posts/edit.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params){
return this.store.find('post', params.post_id);
}
});
app/routes/posts/index.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(){
return this.store.find('post');
}
});
app/controllers/posts/edit.js
import Ember from 'ember';
export default Ember.ObjectController.extend();
app/serializers/application.js
import Ember from "ember";
import DS from "ember-data";
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
primaryKey: 'ID',
attrs: {
author: { embedded: 'always' }
},
extractSingle: function(store, type, payload, id) {
payload = { post : payload };
return this._super(store, type, payload, id);
},
extractArray: function(store, type, payload) {
payload = { posts : payload };
return this._super(store, type, payload);
},
serializeIntoHash: function(hash, type, record, options) {
Ember.merge(hash, this.serialize(record, options));
}
});
app/templates/edit.hbs
<div class="page">
<div class="page-content">
<h2>{{title}}</h2>
{{{content}}}
</div>
</div>
app/templates/index.hbs
<div class="page">
<div class="page-content">
<ul>
{{#each}}
<li>
<h2>{{title}}</h2>
{{{content}}}
</li>
{{/each}}
</ul>
</div>
</div>
Having looked at some examples the above should work, so probably there's an error in the serializer. In the route I can access values from the store however. The following logs the expected data when used in the model method of my edit.js route:
this.store.find('post', params.post_id).then(function(post) {
console.log(post.get('title'));
});
UPDATE: Here is a JS Bin recreating most of the application without Ember CLI. The behaviour is exactly the same as on my local machine; the edit route doesn't properly show the content value (title works, for some reason). When I swap to fixtures instead of the API output the problem remains the same. Also the model in the bin doesn't map to all the values returned by the API, but that doesn't seem to make a difference.
UPDATE 2: Seems the content entry of the returned payload is causing the problems, when I rename the key it's displayed correctly in the edit template. Didn't realise this before, because I was so focussed on getting the content value to display and forgot to test other values. Not sure why it happens only happens on a nested route though and with that particular keyword.
UPDATE 3: If I understand correctly, this might be related to an outstanding issue for Ember Data, there's a proposal for reporting such collisions as well.

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'));
}