Ember - Transition to same route with same model - ember.js

I have a left tree list, and each item in the list opens same route with different model. What i want is, if i click on the same item again and again, i want the route to load again. I know ember doesn't normally function this way. Are there any tweaks to achieve this?
Update:
Left tree is parent route.On clicking items in left tree, child route is loaded in its outlet.
My left tree will be structured like this,
item1(link to bodyRoute1 with model1)
item2(link to bodyRoute1 with model2)
item3(link to bodyRoute1 with model3)
item4(link to bodyRoute2 with model1)
...etc

You could use refresh() route method api link, for example:
// route
actions: {
refreshRoute: function() {
this.refresh();
}
}
//template
<ul>
{{#each items as |item|}}
<li {{action 'refreshRoute'}}>{{item}}</li>
{{/each}}
</ul>
Update
One of the controller property need to be updated from server
So you could use afterModel model hook.
From guides:
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.

I got it working by doing like this,
I maintain the currentRouteName and currentModelId(An id to uniquely identify the model) in an object and it is injected in all routes. It gets updated on any transition.
All the transitions from left tree go through a common function, And in that function i check if the forwarding route,modelId is the same as current route,modelId. If so, i change the value of another globally injected property refreshSameRoute. This value is observed in the child routes and if it changes this.refresh() is called.
Still searching for a better method.

The methods for transitions, like transitionToRoute and replaceRoute, returns some params, including the targetName. When the query params are the same, the targetName returns undefined. You can check that and do a route refreshing.
const queryParams = {<your params>};
const routeName = 'my.route';
const transition = this.transitionToRoute(routeName, { queryParams });
if (transition.targetName !== routeName) {
const route = getOwner(this).lookup(`route:${routeName}`);
route.refresh();
}

Related

How do I set a class on a parent element when a route is the first one loaded?

I have an Ember demo app that works fine if the first route loaded is 'index', 'list' or 'list/index', but not if the first route loaded is 'list/show'. Code is at https://github.com/DougReeder/beta-list , demo is running at https://ember-demo.surge.sh To see the problem, set your window narrower than 640px and surf to https://ember-demo.surge.sh/list/5 You'll see the list panel, rather than the detail panel.
The underlying problem is that, when the route is 'list/show', the divs with class 'panelList' and 'panelDetail' should also have the class 'right'.
I can't set this in the template, because panelList and panelDetail are created by the parent 'list' template. If I move panelList and panelDetail to the child templates 'list/index' and 'list/show', then the list gets re-rendered when going from 'list/index' to 'list/show' which would be a terrible performance hit.
Currently, I use the 'didTransition' action to toggle the class 'right'. This is called both then transitioning from 'list/index' to 'list/show', and when 'list/show' is the initial route. Unfortunately, if 'list/show' is the first route, none of the DOM elements exist when 'didTransition' is called.
I can envision two routes to a solution, but don't know how to implement either:
Toggle the class 'right' on some action which happens after DOM elements exist.
Insert conditional code in the 'list' template, which sets the class 'right' on 'panelList' and 'panelDetail' if the actual route is 'list/show'.
Suggestions?
Answer current as of Ember v2.12.0
You can use the link-to helper to render elements other than links, with styles that change based on the route. Utilizing the activeClass, current-when, and tagName properties, you can basically have that element be styled however you want depending on which route you are on. For example, to render your panelList div:
{{#link-to tagName='div' classNames='panelList' activeClass='right' current-when='list/show'}}
More markup
{{/link-to}}
I love a trick with empty component. In didInsertElement and willDestroyElement hooks you can add and remove a css class from parent element or (I like it better) body. Here is a code:
import Ember from 'ember';
export default Ember.Component.extend({
bodyClass: '',
didInsertElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').addClass(bodyClass);
}
},
willDestroyElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').removeClass(bodyClass);
}
}
});
I use it in template (in my example it's a template of player route) like this
{{body-class bodyClass='player-page-active'}}
To apply classes to parent element, you can use this.$().parent(), but using body is more reliable. Note that this component will create an empty div, but it shouldn't be a problem (can be in rare cases, fix it with classNames and css if needed).
Sukima suggested looking at currentRouteName, and I thus found hschillig's solution, which I simplified for my case. In the controller, I created an isShow function:
export default Ember.Controller.extend({
routing: Ember.inject.service('-routing'),
isShow: function() {
var currentRouteName = this.get('routing').get('currentRouteName');
return currentRouteName === 'list.show';
}.property('routing.currentRouteName'),
In the template, I now use the if helper:
<div class="panelList {{if isShow 'right'}}">
RustyToms's answer eliminates the need for adding a function to the Controller, at the expense of being less semantic.

how to inject a store into a component (when using localstorage adapter)

Ember docs say to define a store like this
MyApp.Store = DS.Store.extend();
If you are looking up records in components, this doc says you can inject the store into the component like this
// inject the store into all components
App.inject('component', 'store', 'store:main');
However, I am using the local storage adapter which I define like this
App.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'my-namespace'
});
Therefore, I don't know how to inject this into the component (where I need to look up a record) following the above instructions.
Following the instructions of this SO answer, I tried to inject the store into a component by passing it in like store=store and/or store=controller.store
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}} </li>
or
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=controller.store}} </li>
The goal was then to be able to do this in an action in the componeent
var todo = this.get('store');
console.log(todo, "the new store");
todo.set('notes', bufferedTitle);
console.log(todo, "todo with notes set");
todo.save();
However, todo.save(); always triggers
Uncaught TypeError: undefined is not a function
Notice that I logged the store? this is what it shows
Class {_backburner: Backburner, typeMaps: Object, recordArrayManager: Class, _pendingSave: Array[0], _pendingFetch: ember$data$lib$system$map$$Map…}
If i inspect it(by opening the tree, which isn't shown here), it does indeed show that notes were set via todo.set('notes', bufferedTitle); however, it doesn't have any of the other attributes of my model that I defined for the index route, and this object doesn't have a 'save' method. Therefore, it doesn't seem to be the actual store, but rather just some backburner object.
I got the same results trying this SO answer where it says to get the store of the targetObject
var todo = this.get('targetObject.store');
Note, I also tried this, i.e. setting the store to be the store of the item.
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=item.store}} </li>
It should be noted that if I set the store in the component, I can print the store on the page by doing {{store}} which gives me
<DS.Store:ember480>
but I can't do var todo = this.get('store'); in the action that handles the click even in the application code.
Question, using the localStorage adapter, how am I able to look up a record in a component (with the aim of then being able to alter the record and then save it again)
Note, if it's important, I define a model for the (index) route like this
App.Index = DS.Model.extend({
title: DS.attr('string'),
version (unfortunately I don't know what version of Ember data or the adapter I'm using)
Ember Inspector
1.7.0
Ember
1.9.1
Ember Data
<%= versionStamp %>
Handlebars
2.0.0
jQuery
1.10.2
Update in response to request for more info
The code that sets up the problem is very simple.
here's the router (with a bad name for the resource :)
App.Router.map(function(){
this.resource('index', { path: '/'});
}
Here's the route that gets the record to use in the Index route
App.IndexRoute = Ember.Route.extend({
model: function{
var resource = this.store.find('index');
return resource;
}
});
I have an Index Controller which does nothing in particular for the component (unless I should be defining methods on the Controller that get triggered by component events)
In the html, I do this with handlebars to pass data to the component
{{#each item in items}}
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}}
{{/each}}
Then, in components/my-component, I have a label that when clicked is supposed to trigger an action that will let me edit one of the attributes on the model
<label> {{action "editTodo" on="doubleClick">{{notes}}</label>
that click triggers this code in App.MyComponent, which triggers the error that prompted this question
var todo = this.get('store')
todo.set('notes', bufferedTitle);
todo.save()
IMHO injecting store into components is not the best idea... By design, components should be isolated and shouldn't have any knowledge about the store.
In the doc you've given, it's written: In general, looking up models directly in a component is an anti-pattern, and you should prefer to pass in any model you need in the template that included the component.
However, if you really need it for some reason, then why not just to pass the variable store to the component?
{{my-component store=store}}
Then, you can pass the store from your controller only in the components where you really need that.
Injecting the store in all your components will most likely lead you to the bad design (although it seems tempting at first).
Here's an updated answer for Ember 2:
Ember Data's store is now a Service, and we can easily inject it into all Components via an Initializer, e.g. app/initializers/inject-store-into-components:
export function initialize(application) {
application.inject('component', 'store', 'service:store');
}
export default {
name: 'inject-store-into-components',
initialize,
}
Then, in your Components, you can access the store with this.get('store'). The obviates the need to directly pass the store as an argument to Components, which requires a lot of boilerplate in your templates.
Whilst the accepted answer is sensible for simple applications it is perfectly acceptable to inject a store into a component if that component doesn't have a relationship with the url, like side bar content or a configurable widget on a dashboard.
In this situation you can use an initializer to inject the store into your component.
However, initializers can be a pain to mimic in testing. I have high hopes that the excellent Ember.inject API that is testing friendly will extend beyond services and accommodate stores. (Or that stores will simply become services).
According to this docThe preferred way to inject a store into a component is by setting a store variable to the record, for example
{{#each item in arrangedContent}}
<li> {{my-component store=item}} </li>
{{/each}}
Then in application code, you can do
var store = this.get('store');
store.set('todo', bufferedTitle);

How to use itemControllerClass to achieve a singleton like experience with ember.js

I've got a fairly simple form but it should never carry any state with it. I started reading an older discussion about how you can use itemControllerClass to get a "singleton" class created each time you enter the route.
If I want to use this how would I plug this into the template and wire it up from the parent controller?
Here is what I'm guessing you would do from the javascript side
App.FooController = Ember.Controller.extend({
itemControllerClass: 'someclass_name'
});
The only "must have" is that I need to have access to the parent route params from the child singleton controller.
Any guidance would be excellent -thank you in advance!
Update
Just to be clear about my use case -this is not an ArrayController. I actually just have a Controller (as shown above). I don't need to proxy a model or Array of models. I'm looking for a way to point at a url (with a few params) and generate a new instance when the route is loaded (or the parent controller is shown).
I'm doing this because the template is a simple "blank form" that doesn't and shouldn't carry state with it. when the user saves the form and I transition to the index route everything that happened in that form can die with it (as I've cached the data in ember-data or finished my $.ajax / etc)
Anyone know how to accomplish this stateless behavior with a controller?
I'm betting you're talking about this discussion. It's one of my personal favorite discoveries related to Ember. The outcome of it was the itemController property of an ArrayController; I use it all the time. The basic gist of it is, when you're iterating over an array controller, you can change the backing controller within the loop. So, each iterating of the loop, it will provide a new controller of the type you specify. You can specify the itemController as either a property on the ArrayController, or as an option on the {{#each}} handlebars helper. So, you could do it like this:
App.FooController = Ember.ArrayController.extend({
itemController: 'someclass'
});
App.SomeclassController = Ember.ObjectController.extend({});
Or, like this:
{{#each something in controller itemController='someclass'}}
...
{{/each}}
Within the itemController, you can access the parent controller (FooController, in this case), like:
this.get('parentController');
Or, you can specify the dependency using needs, like you ordinarily would in a controller. So, as long as the params are available to the parentController, you should be able to access them on the child controller as well.
Update
After hearing more about the use case, where a controller's state needs to reset every time a transition happens to a particular route, It sounds like the right approach is to have a backing model for the controller. Then, you can create a new instance of the model on one of the route's hooks; likely either model or setupController.
From http://emberjs.com/api/classes/Ember.ArrayController.html
Sometimes you want to display computed properties within the body of an #each helper that depend on the underlying items in content, but are not present on those items. To do this, set itemController to the name of a controller (probably an ObjectController) that will wrap each individual item.
For example:
{{#each post in controller}}
<li>{{title}} ({{titleLength}} characters)</li>
{{/each}}
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
In some cases it is helpful to return a different itemController depending on the particular item. Subclasses can do this by overriding lookupItemController.
For example:
App.MyArrayController = Ember.ArrayController.extend({
lookupItemController: function( object ) {
if (object.get('isSpecial')) {
return "special"; // use App.SpecialController
} else {
return "regular"; // use App.RegularController
}
}
});
The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.

Lazy loading in Ember.js?

i am attempting to implement lazy loading pagination. i have a model that represents a 'page' in the paginated view. it contains an array of paginated objects plus a nested set of other 'pages'. (see https://gist.github.com/hooverlunch/6314207) i don't want to load in the array of objects for the other pages only after their links are clicked.
but it seems the route's model method fires only once, when the page is first loaded.
is there any way to make it fire each time a relevant link is clicked? or is there a better way to do this?
Model hook gets called when you visit a route directly via url but not when you use linkTo helper or pass context via transitionTo. If you need something to be called in those cases, use the route's setupController hook instead.
See In Ember.js does setupController and model hooks work only for dynamic segments?
Create an action
<button {{action 'goToPage' pageObj}}>{{pageObj.id}}</button>
in your route add
Myember.KbGlyprobsPageRoute = Ember.Route.extend({
events: {
goToPage: function(pageObj){
var newPage = Myember.KbGlyprobPage.find(pageObj.id);
this.transitionTo('Page', newPage);
}
}
});

How can I get My previous route?

How can I get my previous router in my current controller.
App.MyController = Em.ObjectController.extend({
next:function() { // This is my action helper in HBS
this.transitionTo('nextPage');
},
back:function() { // This is my action helper in HBS
// Here I need to dynamically identify my previous route.
// How can I get my previous route.
}
});
After having inspected the router object again, I don't see any property in there that will allow you to grab the last route. In pre 4 there was a property for the last route, but it was a difficult property to work with.
My solution is therefore the same as it was in pre 4: I'd create my own mixin to handle the routes that you navigate into, and from that list of routes, you can get whatever route you're after: the current one, the last one, et cetera...
jsFiddle here: http://jsfiddle.net/sMtNG/
Mixin
The first thing to do is create the mixin that will allow us to push the routes into a HistoryController. We can do this by creating a setupController method which of course gets invoked every time you move into a route.
App.HistoryMixin = Ember.Mixin.create({
setupController: function() {
this.controllerFor('history').pushObject(this.get('routeName'));
}
});
We are pushing the route into the HistoryController.
History Controller
Since we're currently pushing the routeName into a non-existent HistoryController, we'll need to go ahead and create that, which is absolutely nothing special.
App.HistoryController = Ember.ArrayController.extend();
Index Controller
Since the HistoryController stores the list of routes we've navigated into, we'll need it accessible on other controllers, such as the IndexController, we'll therefore use needs to specify in which controller it should be accessible.
App.ApplicationController = Ember.Controller.extend({
needs: ['history']
});
Implement Mixin
We now have everything we need to keep a track of the routes, and so we'll specify that our routes need to implement this mixin.
App.CatRoute = Ember.Route.extend(App.HistoryMixin);
Template
Last but not least, now that we have a HistoryController which our IndexController can access, and the mixin pushes each accessed route into the HistoryController, we can use our application view to output a list of the routes, and specify the last route. Of course in your case you'll need the last route minus one, but there's no sense in me doing everything!
<h1>Routes History ({{controllers.history.length}})</h1>
<ul>
<li>Last Route: {{controllers.history.lastObject}}</li>
{{#each controllers.history}}
<li>{{this}}</li>
{{/each}}
</ul>
I hope this gets you onto the straight and narrow.
Current solutions feel like the tail wagging the dog.
Simple solution is to do window.history.back()
Ember doesn't keep track of the router history, since it would be redundant. All browser already handle this by default.