Emberjs: Resetting controller based on dynamic segment - ember.js

I have this route structure in my app:
Profile.Router.map(function() {
this.resource('profile', { path: '/:user' }, function(){
this.route('followers');
});
});
My /:user/followers page is supposed to show the list of followers of :user. The controller for profile.followers is setup for infinite scroll - so it has properties like curPage, resultsPerPage.
Profile.ProfileFollowersController = Ember.ArrayController.extend(InfiniteScroll.ControllerMixin,{
curPage: 1,
resultsPerPage: 20,
//other code for infinite scroll which increments curPage as user scrolls down
})
Now, since controllers are singleton in emberjs, when I move from /tom/followers to /jin/followers, the properties for infinite scroll that were set for /tom/followers get preserved and applied for /jin/followers as well.
For eg. say I am on /tom/followers and scroll down 4 pages, curPage property of 'profile.followers' controller gets set as 4. Now when I move to /jin/followers, though the model hook of the route would return list of followers for jin, but would pick curPage as 4 since ember's controllers are singleton and I had scrolled down to 4th page on /tom/followers.
How is this supposed to be handled?
Here is my profile.followers route as well:
Profile.ProfileFollowersRoute = Ember.Route.extend({
model: function(params) {
console.log("fetching followers model");
return Ember.$.getJSON('/'+this.modelFor('profile').user+'/followers?new=1');
},
});

You can use the route's setupController hook for this. This hook is fired every time the route is entered.
App.ProfileFollowersRoute = Ember.Route.extend({
setupController: function(controller, model) {
// Call _super for default behavior
this._super(controller, model);
// Reset the desired controller properties
controller.setProperties({
curPage: null,
someOtherProp: null
});
}
});
I've made a jsbin demonstrating it:
http://emberjs.jsbin.com/fiyetefeno/6/
You can read the API documentation here:
http://emberjs.com/api/classes/Ember.Route.html#method_setupController

Related

Ember - How to send data down to a component?

I'm building an Ember app that needs to fade out a background DIV when a form input becomes focused.
I have defined actions on my Application route, and set a property in my model (because I'm trying to do this without a controller, like the Ember 2.0 way). I'm trying to do Action Up, Data Down. I have the actions going up to the Application route, but the data just isn't making it back down to the component.
I have the actions bubbling up to the application route just fine, but when I update the property this.controllerFor('application').set('showBackground', true); it never makes it back down to the component.
I have this fading out background image on every route of my site, so moving all the actions to each route seems like a lot of code duplication.
What am I doing wrong?
// Application route.js
var ApplicationRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
showBackground: false
});
},
setupController: function(controller, models) {
controller.setProperties(models);
},
action: {
showBackground: function(){
// This runs fine
this.controllerFor('application').set('showBackground', true);
},
hideBackground: function(){
// This runs fine
this.controllerFor('application').set('showBackground', false);
}
}
});
// Background component.js
var BackgroundImage = Ember.Component.extend({
// This never runs for some reason!?
controlImage: function(){
if( this.get('showBackground') ) {
// Open menu!
console.log('show image');
} else {
// Close menu!
console.log('hide image');
}
}.observes('showBackground')
});
// Login template.hbs
{{background-image showBackground=showBackground}}
Is this the correct way to replace "properties" and controllers with routes? All the "move to Ember 2.0" advice I can find doesn't mention how to replace high level properties.
EDIT
I created a JSbin, but I'm not sure if it's setup correctly for the 2.0 style (no controllers), as the import/export (ES6?) stuff doesn't work on JSbin.
http://emberjs.jsbin.com/wunalohona/1/edit?html,js,console,output
I couldn't actually get any of the actions to bubble correctly.
Here is the working demo.
There were multiple issues in the jsbin you provided. Here are some of the issue I fixed.
You need to specify the routes, components on the App namespace or Ember will not be able to find it. The resolver used in ember-cli is custom.
var ApplicationRoute = Ember.Route.extend({ should be
App.ApplicationRoute = Ember.Route.extend({
var BackgroundImage = Ember.Component.extend({ should be
App.BackgroundImageComponent = Em.Component.extend({
More about it here.
You don't need to specify the setupController method in the route. By default the model returned from the model hook is set to the model property of the controller.
https://github.com/emberjs/ember.js/blob/v1.11.1/packages/ember-routing/lib/system/route.js#L1543
The proxying behavior of ObjectController along with ObjectController has been deprecated.
Now refer model property by adding model.+modelPropertyName
You can read more about this in the deprecation page for v1.11
action in the ApplicationRoute should be actions

Implementation of Site-wide arrayController in Ember

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'.

EmberJS redirect when no subroute specified

I have a set of nested routes and templates that I'd like to auto-select the first model if no sub-routes are specified. The route structure is:
App.Router.map(function() {
this.route('sales', function () {
this.route('orders', function () {
this.route('order', { path: ':order_id' });
});
});
});
If the user hits sales.orders then they should be redirected to the first order on the sales.orders model. Making this work is no problem. The issue comes when the user hits sales/orders/:order_id No matter what :order_id is the user is always redirected to the first order in the orders array.
I'm currently performing the redirect in the setupControllerhook of the SalesOrders route as I have some sorting on the controller that needs to be in place prior to redirecting.
App.SalesOrdersRoute = Ember.Route.extend({
model: function () {
return this.store.find('order');
},
setupController: function (controller, model) {
controller.set('model', model);
var firstObject = controller.get('sortedContent').get('firstObject');
this.transitionTo('sales.orders.order', firstObject);
}
});
App.SalesOrdersController = Ember.ArrayController.extend({
sortProperties: ['orderNumber:desc'],
sortedContent: Ember.computed.sort('model', 'sortProperties')
});
I have a jsbin that shows my issue.
Hitting any specific order will always redirect to the first order in the array (4 in this case).
I need it to keep the deep linked url and only redirect when no order is specified.
I feel like this question and this question are both similar to what I'm trying to do except neither addresses auto-selecting the first item if no sub-routes are specified.
You should do the redirect in your SalesOrdersIndex route. The additional index route of each route will only be created when it matches the complete URL mapping. So for any url that isn't exactly "sales/orders" it will not be created. Just what you want.
App.SalesOrdersIndexRoute = Ember.Route.extend({
setupController: function (controller, model) {
controller.set('model', model);
this.controllerFor('salesOrders').set('model',model);
var firstObject = this.controllerFor('salesOrders').get('sortedContent').get('firstObject');
this.transitionTo('sales.orders.order', firstObject);
}
});
jsbin: 4 3 2 1 redirect to 4
One option could be to check the transition on the afterModel hook and redirect if the user is trying to access the sales.orders.index route.
Something like this:
afterModel: function(model, transition){
if (transition.targetName === "sales.orders.index"){
var first = model.objectAt(0);
this.transitionTo('sales.orders.order', first);
}
}
Here's an example.
That won't work with setupController as the setupController hook does not have access to the transition. In your case, since you just want to sort, you could do something like:
var first = model.sortBy('orderNumber').reverse().objectAt(0);
As far as I know, setupController is called after both redirect and afterModel so I'm not sure it's possible to get the sorted content from the controller through the afterModel and redirect hooks.

`needs` not waiting for data to be returned before rendering template

I am trying to implement a controller needing another (CampaignsNew needing AppsIndex), which looks like
App.CampaignsNewController = Ember.Controller.extend({
needs: ['appsIndex']
});
And in my CampaignsNew template I am showing it via
{{#if controllers.appsIndex.content.isUpdating}}
{{view App.SpinnerView}}
{{else}}
{{#each controllers.appsIndex.content}}
{{name}}
{{/each}}
{{/if}}
However controllers.appsIndex.content.isUpdating is never true. I.e. it attempts to show the data before it has been loaded.
My AppsIndex route has the model overridden:
App.AppsIndexRoute = Ember.Route.extend({
model: function(controller) {
var store = this.get('store').findAll('app');
}
...
});
I can get it to work if I put the same code within my CampaignsNew route and modify the template to each through controller.content. Which says to me that needs is not using the route? It also works if I go to the /apps page and it loads the data, and then navigate to the /campaigns/new page.
How do I get this to work? Thanks!
Edit:
As requested, the relevant parts of my router:
App.Router.map(function() {
this.resource('apps', function() {
...
});
this.resource('campaigns', function() {
this.route('new');
});
});
And the AppsIndex is accessed at /apps and CampaignsNew is at /campaigns/new
Edit2:
After implementing the suggestion by #kingpin2k, I've found that Ember is throwing an error. Below are the updated files and the error received.
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign'
});
App.CampaignsNewRoute = Ember.Route.extend({
model: function(controller) {
return Ember.RSVP.hash({
campaign: this.store.createRecord('campaign'),
apps: this.store.find('app')
});
// return this.store.createRecord('campaign');
},
setupController: function(controller, model){
controller.set('apps', model.apps);
this._super(controller, model.campaign);
}
});
Ember throws this error:
Error while loading route: Error: Assertion Failed: Cannot delegate set('apps', <DS.RecordArray:ember689>) to the 'content' property of object proxy <App.CampaignsNewController:ember756>: its 'content' is undefined.
I read online that this is because the content object doesn't exist. If I set it like so:
App.CampaignsNewController = Ember.ObjectController.extend({
content: Ember.Object.create(),
...
});
Then the page loads without error, and when inspecting the Ember Chrome extension, I can see the data has loaded. But it doesn't show on the page. Which I suppose happened because the content object existed and so Ember didn't wait for the model's promise to fulfill before rendering the template. Seems odd that you should have to define content in such a way though. Any insight on how to handle this?
Edit3: Question answered for me in another thread
Based on your router, apps isn't a parent of campaigns/new.
This means someone could hit #/campaigns/new and Ember would hit ApplicationRoute, CampaignsRoute, and CampaignsNewRoute to populate the necessary information for the url requested. Using needs as a way of communicating between controllers really only makes sense in an ancestral pattern (aka communicating with your parents, grandparents etc).
Just as another quick note, AppsIndex is a route of Apps, it won't be hit when your url includes a child. e.g.
Router
this.resource('apps', function() {
this.resource('chocolate', function(){
.....
});
});
Url being hit
#/apps/chocolate
Routes that will be hit
ApplicationRoute
AppsRoute
ChocolateRoute
ChocolateIndexRoute
The index route is only hit when you don't specify a route of a resource, and you are hitting that exact resource (aka nothing past that resource).
Update
You can return multiple models from a particular hook:
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
}
});
If you want the main model to still be cows, you could switch this up at the setupController level.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
},
setupController: function(controller, model){
controller.set('dogs', model.dogs); // there is a property on the controller called dogs with the dogs
this._super(controller, model.cows); // the model backing the controller is cows
}
});
Check out the second answer here, EmberJS: How to load multiple models on the same route? (the first is correct as well, just doesn't mention the gotchas of returning multiple models from the model hook).
You can also just set the property during the setupController, though this means it won't be available when the page has loaded, but asynchronously later.
Which controller?
Use Controller if you aren't going to back your controller with a model.
App.FooRoute = Em.Route.extend({
model: function(){
return undefined;
}
});
Use ObjectController, if you are going to set the model of the controller as something, that isn't a collection.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
}
});
Use ArrayController if that something is going to be a collection of some sort.
App.FooRoute = Em.Route.extend({
model: function(){
return ['asdf','fdsasfd'];
}
});
Note
If you override the setupController, it won't set the model of the controller unless you explicitly tell it to, or use this._super.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
},
setupController: function(controller, model){
controller.set('cows', model.cows);
controller.set('dogs', model.dogs);
// uh oh, model isn't set on the controller, it should just be Controller
// or you should define one of them as the model
// controller.set('model', model.cows); or
// this._super(controller, model.cows); this does the default setupController method
// in this particular case, ArrayController
}
});

Ember js pre 4 - router not resolving id on link click

I have a list of accounts and then i have a view link to view an account in detail and this is the account route. When i click the view link the (guid) doesnt update when going through the router, it only updates in the URL but it doesnt seem to be carrying through to the code.
When i do a browser refresh then the (guid) gets carried through to the router... Its not resolving for some reason.
Im not using ember-data but will use it in the future.
Here is my "Accounts" template code with the "View" link:
{{#each accountdata in controller}}
<tr>
<td>{{accountdata.accountnumber}}</td>
<td>{{accountdata.accountname}}</td>
<td>{{accountdata.accounttypestatus}}</td>
<td>{{accountdata.accountuser}}</td>
<td>{{#linkTo account accountdata}}View{{/linkTo}}</td>
</tr>
{{/each}}
accountdata is the context with the "accountguid" which is my id.
Here is my router:
App.Router.map(function() {
this.resource("accounts", { path: '/accounts' });
this.resource("account", { path: "/accounts/:accountguid" });
});
App.AccountsRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.set('searchfilter','ALL');
controller.search();
}
});
App.AccountRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.show(controller);
},
model: function(params) {
this.controllerFor('account').set('accountguid',params.accountguid);
},
serialize: function(model) {
return {accountguid: Em.get(model, 'accountguid')}
}
});
My controller.show is where i send the context to call a script to display the account details.
So i just need view to carry through the correct accountguid each time which it isnt and then to call the show(context) method.
Thanks
This is my old router code which worked 100%. When i clicked a link it resolved the :accountguid and when i did a browser refresh it did the same thing. i had no problems, everything just worked.
// //Accounts
// accounts: Ember.Route.extend({
// route: '/accounts',
// index: Ember.Route.extend({
// route: '/',
// connectOutlets: function (router) {
// router.get('applicationController').connectOutlet('accounts');
// router.get('accountsController').set('searchfilter','ALL');
// router.get('accountsController').search();
// }
// }),
// view: Ember.Route.extend({
// route: '/:accountguid',
// connectOutlets: function (router, account) {
// router.get('applicationController').connectOutlet('account', account);
// router.get('accountController').show(account);
// //router.get('accountController').connectOutlet('eventloghistory','eventloghistory');
// }
// })
// }),
I managed to solve my problem with the following code. I am now able to refresh the browser and i am able to click the link and it will carry through the current :accountguid in use to the show() method.
App.AccountRoute = Ember.Route.extend({
model: function(params) {
return {accountguid: params.accountguid};
},
setupController: function(controller, model) {
controller.show(model);
},
serialize: function(model) {
return {accountguid: Em.get(model, 'accountguid')}
}
});
Change your router map to the following and it should work
App.Router.map(function() {
this.resource("accounts", function(){
this.resource('account',{path:':account_id'});
});
});
Please, show us the code behind your AccountsController and AccountController. It would be most useful if you provide a jsfiddle with the whole construction.
In general, you may be unaware of the new flow of things. Here is what happens in the two scenarios:
1. You navigate to the AccountRoute by setting the URL (/account/5 for example).
1.1. the 'model' hook of the AccountRoute is called
model: function(params) {
return your model here...
}
with params = { accountguid: 5 }. Because you are not using ember-data, you should implement this hook and initialise and return the model there.
1.2. the setupController hook is called with the AccountController and the model returned by the model hook. Without the code behind
controller.show(controller);
It is not quite clear what its purpose is, but you should probably do something like
setupController: function(controller, model) {
this._super(controller, model);
controller.set('content', model);
controller.show(model);
}
As you can see, by not implementing the model hook, you URL stays correct, but the route does not know how to build the needed model resource.
You transition to the route via a linkTo call
{{#linkTo account accountdata}}View{{/linkTo}}
Here, linkTo expects accountdata to be the full account model for the route. Meaning it may not carry only partial data like id for example (read this for clearance: In latest Ember, how do you link to a route with just the id/name of a model, rather than providing all of its attributes in the linking page?).
2.1. The model hook of the AccountRoute is NOT called. The AccountRoute model property is set to the object that is passed to linkTo instead (in our case 'accountdata').
2.2. setupController is called with AccountController and the accountdata object.
If you accountdata object is not complete, it would be wise to create a complete instance here and set it to the controller.
As you can imagine, if you accountdata is like { id: 5, accountname: "John", accounttypestatus: "A", ...}, then after a click on linkTo, the URL will update correctly to /account/5, but the account template will receive accountdata, rather than an actual account.
P.P. If none of the above is any help, this might be your issue: https://github.com/emberjs/ember.js/issues/1709