Working hard on my Ember app here, and it's going along fine. However, I've run into an issue of unexpected behaviour and I'm not sure regarding the best approach to this problem.
The problem is that in a specific route, I want to render another route into another outlet. However, the other route that I render into the other outlet doesn't retain it's own model.
If I do this:
App.TestRoute = Ember.Route.extend({
model: function() {
return {
heading: "Test",
testContent: "This is test."
}
}
});
App.IndexRoute = Ember.Route.extend({
renderTemplate: function() {
this.render("test", {
outlet: "left"
});
this.render({
outlet: "right"
});
},
model: function() {
return {
heading: "Index",
indexContent: "This is index."
}
}
});
... and access the IndexRoute, I would expect the TestRoute's model to be rendered into the TestRoute's template, but only the IndexRoute's model is relayed to both templates.
Fiddle here:
http://jsfiddle.net/3TtGD/1/
How do I allow Ember to use the default model for a route without having to expressively merge them? It seems tedious.
Also, having the same name of some model properties, like {{heading}} is desirable, but not necessary.
What's the best approach for solving this issue?
Thank you for your time.
Best regards,
dimhoLt
In the renderTemplate method you're telling Ember to render a template inside an outlet but it will just default the controller to the one managing the route. Given it's the controller handling the route it makes sense that it manages all the templates within that route.
Of course you can specify a different controller using:
this.render("test", {
outlet: "left",
controller: 'test'
});
it can in turn be a controller you already instantiated (and maybe set its content):
var testController = this.controllerFor('test');
testController.set(....)
this.render("test", {
outlet: "left",
controller: testController
});
About using the model: You can call this.modelFor('test') inside the route and it will return the model of the test route (it even knows if it has already been resolved). I usually do this when I need to access the model of one of the parent routes.
I believe it makes sense to access the model of a parent route, but not so much if you're accessing the model of an unrelated route. Why don't you want to merge both models?
Related
I have an Emberjs app that has a search action that needs to be available from all routes. To accomplish this, I've defined the 'search' action in the application route like this:
App.ApplicationRoute = Ember.Route.extend({
actions: {
search: function (query) {
this.transitionTo('search', { queryParams: { q: query } });
}
}
});
The 'q' querystring parameter is defined in the SearchController:
App.SearchController = Ember.ArrayController.extend({
queryParams: ['q'],
q: ''
});
The search route calls a service that queries my database with the query parameter like this:
App.SearchRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('activity', { query: params.q }),
}
});
I know that the model hook is not called on transtionTo, but when the user is already on the search page and wants to search again with a different query, I need to reload the search route with a new model.
Is using transitionTo in the application route the wrong approach in this case?
Any ideas are greatly appreciated!
I would add a named {{outlet}} in your Application template, wherever you want the search results to appear. Then, in your Application route, inside the renderTemplate hook, I would render the search results template into the new outlet, also specifying what controller it should use.
On the controller, you can create a computed property, which would detect changes in the query string (or however you want to supply the search results). This property (or properties) would then feed the data in your search results template.
More on rendering a template inside a route:
http://emberjs.com/guides/routing/rendering-a-template/
If you decide to go with putting the renderTemplate hook in Application route, you can set the Search controller's model (or whatever you want to call it) property from any route which needs to update the model on the search controller for it to display proper results:
this.controllerFor('search').set('model', model);
You can also create a Mixin, which would contain the renderTemplate hook, which you can include in any route you want to do your searches from. In the hook, you could send your route's model into the controller:
renderTemplate: function(controller, model) {
this.render('search', {
into: 'search',
outlet: 'application',
controller: 'search',
model: model
});
}
Play around with some of these techniques. I'm sure I'm missing some details, but I think you can get them to work.
I have been looking to a solution to this for about a week now with no luck. We have an ember application which has a sidebar that is present on all routes which displays a list of user posts. It is important that the posts update in real-time as they are submitted as well as sort with the newest post at the top of the list, which from what I've read will require an array controller. The problem is, I cant find any way (or rather dont understand) to use an array controller and specific model that is not directly referenced to the current route. I have tried rendering the sidebar with the following code in the application route:
Destination.ApplicationRoute = Ember.Route.extend({
model: function(model) {
var self = this;
return new Em.RSVP.Promise(function (resolve, reject) {
new Em.RSVP.hash({
post : self.store.find('post')
}).then(function (results) {
resolve({
post: results.post
});
});
});
},
renderTemplate: function(controller, model) {
this.render();
this.render('sidebars/postBar', {
outlet: 'postbar',
into: 'application',
controller: 'posts',
model: 'post'
});
}
Then I have the following code for my array controller
Destination.PostsController = Ember.ArrayController.extend({
itemController: 'post',
sortProperties: ['id'],
sortAscending: false
});
However this doesnt work at all and I'm having trouble finding any examples of how to accomplish this.
The approach you can use is to load whatever models you need for the entire application in the ApplicationRoute. You don't have to create the RSVP.Promise as you have done, simply return an RSVP.all or RSVP.hash as follows:
Destination.ApplicationRoute = Ember.Route.extend({
model: function(model) {
return Em.RSVP.Hash({
post : self.store.find('post')
// fetch other models as required
});
}
});
Now there are two options for the controller setup and rendering.
Option 1: Outlets and route based controller setup.
The next thing is to setup the appropriate controller and render the view. Assuming you have defined an {{outlet 'sidebar'}} in your application template, the ApplicationRoute can render the sidebar as follows:
Destination.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model, transition) {
// perform default application controller setup
this._super(controller, model, transition);
// setup sidebar controller model
this.controllerFor('side-bar').set('model', model.posts);
// setup other controllers as required...
},
renderTemplate: function(controller, model) {
// render `posts` template into `side-bar` outlet with `side-bar` controller.
var c = this.controllerFor('side-bar');
this.render('side-bar', { outlet: 'side-bar, controller: c });
// other top level outlet rendering as required...
}
});
Option 2: View helper based controller setup and rendering.
Instead of using additional outlets, we can avoid the need to override setupController or renderTemplate in the route entirely. We can use the handlebars render helper to specify both the model and controller to use directly from our template.
So given your application controller will be setup with the result of the RSVP hash by default, it will contain a 'posts' property on its model/content. Just add the following to your application template:
{{render 'side-bar', posts}}
The above will render the sidebar template and setup the singleton SideBar controller using the posts model for you. I think this is cleaner than messing about with outlets given it doesn't sound like you going to be rendering different views into the sidebar based on your question.
API documentation on the render helper is here, with an overview of the rendering helpers here.
Note I have used Ember-cli resolver naming conventions which use a dasherized naming convention. If you're not using Ember CLI (which I highly recommend) then you may have to use the PascalCased string names ie 'SideBar' instead of 'side-bar'.
My router in it's entirety:
Books.Router.map(function () {
this.resource('books', { path: '/' }, function () {
this.route('search', { path: 'search/:keyword' });
});
});
Books.BooksRoute = Ember.Route.extend({
model: function(){
return this.store.find('book');
},
actions: {
postAlert: function (msg, classes) {
var postAlert = $('#alert');
postAlert.html(msg).toggleClass(classes).toggle(1000);
setTimeout(function () {
postAlert.toggle(1000, function () {
postAlert.toggleClass(classes)
});
}, 3000);
}
}
});
Books.BooksIndexRoute = Ember.Route.extend({
model: function () {
return this.modelFor('books');
},
renderTemplate: function () {
this.render({ controller: 'books' });
}
});
Books.BooksSearchRoute = Ember.Route.extend({
model: function (params) {
return this.store.filter('book', function (book) {
return book.get('titleSlug').indexOf(params.keyword) > -1;
})
},
renderTemplate: function (controller) {
this.render('books/index', { controller: controller });
}
});
Now let's focus on the last bit of the router, the BooksSearchRoute. When I leave my router as it is right now and go to the route localhost/#/search/the_adventures_of_huckleberry_finn then I will see the books/index template populated with the model where the titleSlug contained the dynamic segment which is great, exactly what I expect.
Now when I try to use an action defined in my books controller from that URL I get an error that nothing handled the action. In response to that I switched the renderTemplate line so that it uses 'books' as the controller instead of the default controller.
renderTemplate: function () {
this.render('books/index', { controller: 'books' });
}
That change allows me to access the actions in the books controller that I need. However after making the change the filter does not appear to work anymore as all of the books in the libray are displayed rather than just thouse matching the search term. Can someone please explain to me what is happening here?
That is actually the expected behaviour.
Explanation
When a Route is hit, it obtains the model and passes that to the Controller associated with that route. It determines which one using the Ember naming conventions.
Override template
What you have done here is override renderTemplate here to specify that a different template should be used, than the one that the naming conventions tell it to use.
That works fine in this case because the model for the BooksIndexRoute and the model for the BooksSearchRoute are compatible - they are both arrays of Books.
Override template AND controller
The next thing that you did was to override renderTemplate here to specify that a different template should be used, and it should use a different controller too, BooksController, according to the naming convention.
Of course, BooksController doesn't know that you have done this, and will use the model that it is aware of, the model returned by its own Route, which is this case was this.store.find('book').
... and since that model is not filtered, the template renders the full set of Book models.
Suggested solution
You can probably continue along this path, where you override the template and controller, and refactor the required actions such that they are available on both controllers. However, I would not suggest this, as it goes against the grain of how Ember was designed, plus it will involve quite a lot of spaghetti code.
The canonical solution would involve using the routes and controllers that you already have, but do not override renderTemplate in BooksIndexRoute.
Instead, extract the code that renders your list of books into a separate template and put it into a folder called partials, then invoke that partial from both the templates:
from books (or books\index as the case may be), as well as
from books\search
The syntax looks like this
I do not know what your templates look like, but if you post them, I can show you how to do so.
I have a route that loads all my models, and a nested route that allows the user to add a new model.
App.Router.map(function() {
this.resource("foo", {path: "/foo"}, function() {
this.route("add", {path: "/add"});
});
});
My add template looks like this (very basic)
{{input value=wat}}
Here is the linkTo from my index template
{{#linkTo 'foo.add'}}Add A New Model{{/linkTo}}
When I click the add button I simply create the model using $.ajax and transition back to the list route. All works great, until I click the "add" link again.
When the add route loads up the template from above the 2nd time it still shows the "wat" value I entered previously. I was hoping it would not persist any state as each time I "add" a new model it should be unaware of any previous model data.
How can I achieve this with ember 1.1.2+
Update
The approach I took was to reset each element in the setupController method of the route (as this is invoked each time you load the controller).
App.FooAddRoute = Ember.Route.extend({
model: function(params) {
var parentId = 1;
return Ember.Object.create({'bar': parentId});
},
setupController: function(controller, model) {
this._super(controller, model);
controller.set('bazz', '');
}
});
The quick and dirty answer is you want to use a model on the route. If you didn't, you'd have to manually blank out the values on the controller. Ember builds up singleton controllers. This generally is super convenient and very performant.
Singleton controllers keep state. The best way to keep them stateless is to have them backed by a model (return an empty object from the model hook, and don't have the values defined on the controller). By returning something from the model hook it will use an ObjectController (or you'll need to update your code to use an ObjectController on your controller). Then all values will be proxied to the model instead of being stored on the controller.
http://emberjs.jsbin.com/OPaguRU/1/edit
I have a very basic route setup that allows me to first show "all" records for some object. Then if the user selects a dropdown they can filter this down using a date.
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like
this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded)))
from within the setupController method of the RecordsDateRoute
App.Router.map(function(match) {
return this.resource("records", { path: "/" }, function() {
return this.route("date", { path: "/:date_loaded" });
});
});
App.RecordsController = Ember.ArrayController.extend({
selected: 0,
dates: Ember.computed(function() {
return App.Date.find();
}).property()
});
App.RecordsIndexRoute = Ember.Route.extend({
model: function() {
this.controllerFor("records").set("selected", 0);
return App.Record.find();
}
});
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
//the controllerFor below seems to be working great ... but what about needs?
this.controllerFor("records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
With rc2, instances of other controllers can be retrieved via "controllers.controllerName", in you case it would be this.get('controllers.records').
The "needs" declaration makes the referencing controller sort of import the reference to the other controller; in your case, the date controller would be:
App.RecordsDateRoute = Ember.Route.extend({
needs: ['records'],
model: function(params) {
this.get("controllers.records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
Regarding App.Record.find(new Date(model.loaded)), find() expects an id or an object whose keys and values will be used to filter the collection of models, but you're giving it a Javascript date.
Did you mean App.Record.find(new App.Date(model.loaded)), or maybe something like App.Record.find({ loaded: model.loaded }) /* assuming it's already a Date */?
There is also an initController(controller, model) method in the route called , maybe you could use that instead of "overloading" the model() method with too many responsibilities. http://emberjs.com/api/classes/Ember.Route.html#method_setupController
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
To access another controller from route hooks you should continue to use controllerFor. Controller.needs is for communication between controllers, it replaces the now deprecated use of controllerFor method on controllers. AFAIK there is no plan to deprecate controllerFor on ember Routes.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
For this use case it would be best to stick with controllerFor. It is possible to use needs this way, by specifying that App.RecordsDateController needs = ['records'] you could access the records controller via controller.get('controllers.records') from within your route's setupController hook.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded))) from within the setupController method of the RecordsDateRoute
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
return App.Date.create({ loaded: params.date_loaded });
},
setupController: function(controller, model) {
var recordsController = this.controllerFor("records");
// Moved this from model hook, since here you are 'setting up a controller'
recordsController.set("selected", model.date_loaded);
// Set query based on current route's model
var query = { loaded: model.loaded };
recordsController.set("content", App.Record.find(query));
}
});