Obversing route change to apply equivalent to onload - ember.js

I'm trying to observe the route change to apply some common action once rendered. The idea is to have a feature similar to the onload but as we use a single-page app this needs to be triggered on each route changes. (could be scoped to the new view)
I found how to observe the currentPath changes:
App.ApplicationController = Ember.Controller.extend({
currentPathDidChange: function() {
prettyPrint()
}.observes('currentPath');
});
While this works good in some cases, it gets triggered when the route changes, but still to early to apply content changes as it seem to append before the content gets rendered.
Any idea on the best practice to achieve such goal?

Have you tried deferring the code with Ember.run.schedule? For instance,
App.ApplicationController = Ember.Controller.extend({
currentPathDidChange: function() {
Ember.run.schedule('afterRender', this, function() {
prettyPrint();
});
}.observes('currentPath')
});

Due to the deprecation of Controllers in Ember 1.x finding the url in the router would be a good way to future proof your apps. You can do this in ember-cli like so:
// similar to onLoad event behavior
export default Ember.Route.extend({
afterModel: function (model){
Ember.run.next(() => {
console.log(this.get('router.url'));
});
}
});
// hacky way to get current url on each transition
export default Ember.Route.extend({
actions: {
didTransition: function() {
Ember.run.next(() => {
console.log(this.get('router.url'));
});
}
}
});
This will log: /posts and /posts/3/comments ect.

Related

Can Ember on and observer be chained and applied to one method using the new Ember.computed syntax?

I have recently started switching from syntax like this:
Ember.Object.extend({
someMethod: function(){
// do something
}.on('init')
});
to this:
Ember.Object.extend({
someMethod: Ember.on('init', function() {
// do something
})
});
Because it seems to be the convention nowadays and I hear it works better in add-ons.
However, occasionally I want to chain behaviors of the method like this:
Ember.Object.extend({
someMethod: function(){
// do something
}.on('init').observes('someProperty')
});
Is there a way to achieve this with the syntax that doesn't count on having function prototypes modified to include on and observes methods?
function() {}.on('init') does not work in addons because Function prototype extensions are disabled by default in addons. This was done so they work with applications that also have them turned off.
In this case, Ember.on('init', function() {}).observes('someProperty') is not considered a prototype extension because you are calling the .observes() function on whatever is returned from the .on() function.
You should avoid using .on() with lifecycle hooks, specifically .on('init'). The .on() event listener is asynchronous, leading to unexpected event execution ordering. The better alternative is to override the init method:
Ember.Object.extend({
init() {
this._super(...arguments);
}
});
Another suggested improvement is that starting with Ember 1.13 you have new lifecycle hooks available. This allows you to replace the .on() + .observes() with for example didReceiveAttrs:
Ember.Object.extend({
didReceiveAttrs() {
// check if `someProperty` changed
// do something
}
});
I just don't think it's possible with chaining, but if you need to do that with non-deprecated syntax this may work:
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Observe and Chain Demo',
clicks: 0,
someProp: 'undefined',
bindOnInitAndPropChange: Ember.on('init', function() {
this.set('someProp', `Initialized. I see ${ this.get('clicks') } clicks.`);
Ember.addObserver(this, 'clicks', null, function() {
this.set('someProp', `Observed, I now see ${ this.get('clicks') } clicks.`);
});
}),
actions: {
incrementClicks() {
this.incrementProperty('clicks');
}
}
});
Working Twiddle

Can I force 'active' class on a {{link-to}} helper?

Here is possibly an edge case for how ember adds the 'active' class on a link to helper.
I have my current router set up like so:
import Ember from 'ember';
var Router = Ember.Router.extend({
location: PortalDevENV.locationType
});
Router.map(function() {
this.resource('portal', function() {
this.route('admin');
this.resource('placements', function() {
this.route('import-debtors');
this.resource('add-debtor', function() {
this.route('debtor-form');
});
this.route('view-debtors');
});
this.resource('debtor', {path: 'placements/view-debtors/debtor/:debtor_id'}, function() {
this.route('agent-notes');
this.route('transactions');
});
});
});
export default Router;
notice how I have a resource called "debtor" that- while it is being rendering into the portal template- i still need it to appear (in terms of the URL) to be a child of the "view-debtors" route... which, in reality, is nested deeper within a separate set of templates.
This structure seems to be working fine, but it is breaking my breadcrumb-style navigation.
When moving into the "debtor" page.. i still want "view-debtors" {{link-to}} helper to get the 'active' class from ember... along with the {{link-to}}'s that lead up to the "view-debtors".
Is this possible to do by calling some functions in my routes... or some other way?
It doesn't seem to be a common ember convention... but then again perhaps Ember actually does work in this way and I did something else that broke it? Take a look and see if my set up is correct.
You should be able to bind the active class to a computed property. Assuming the {{link-to}} you are referring to is in your application.hbs template, you could do something like this:
// templates/applictaion.hbs
{{#link-to "view-debtors" class="isDebtorsRoute:active"}}View Debtors{{/link-to}}
// controllers/application.js
export default Ember.Controller.extend({
isDebtorsRoute: function() {
// use this.get('currentRouteName') or this.get('currentPath')
}.property('currentPath')
})
EDIT: Here is a jsbin example http://emberjs.jsbin.com/wuhor/1/edit?html,css,js,output

Ember one init for ALL routes

If I want to do something on route init, I use
MyRoute = Ember.Route.extend({
init: function(){
// do stuff
}
})
What about if I want to run the same function for initialization of all routes. Is there a way to do it globally without going through each route individually ?
Indeed there is. Just use a mixin.
var InitializeMixin = Ember.Mixin.create({
__init: function() {
// do stuff
}.on('init')
});
App.MyRoute = Ember.Route.extend(InitializeMixin, {
});
Just mix it into any route you want to do the setup in. Also note that I used on('init') instead of overriding the init function. This is a little cleaner (I think) because you don't have to call this._super().
Extend your base route:
MyRoute = Ember.Route.extend({
init: function(){
this._super();
// do stuff
}
});
OtherRoute = MyRoute.extend({
init: function(){
this._super();
}
});
Both answers are good, but require changing application code. In my case I want to use it for switching stylesheets so every route has it's own stylesheet. I need it only for development, and in production I would compile all stylesheets into one and remove the code which switches stylesheets.
In this case I found it's best to put your code in Ember core ( search for var Route = EmberObject.extend )
I also realized that for switching stylesheets it's better to have individual stylesheets not for routes, but for templates.
When I find out, I will post how to do it here: https://stackoverflow.com/questions/24068433/ember-change-stylesheet-for-every-template

Ember.js RC1 - controller 'needs' another that does not yet exist

My routing structure:
App.ready = function() {
App.Router.map(function() {
this.resource('contacts', function() {
this.resource('contact', function() {
});
});
});
}
Now in my contactsController I respond to and add action that transitions to the contact route. I would then like to call the add method on my contactController.
I have placed the needs: ['contact'] on my ContactController but then I get this message:
<App.ContactsController:ember197> needs controller:contact but it does not exist
When I use controllerFor (which is deprecated) I also get an error:
this.controllerFor('contact').add();
So Ember.js RC1 appears to only create the controllers (and other related instances) once one actually transitions to the appropriate route.
Is there a way around this.
So Ember.js RC1 appears to only create the controllers (and other related instances) once one actually transitions to the appropriate route.
Interesting - I had thought ember generated controllers earlier but guess not.
Is there a way around this?
Workaround is to define App.ContactController manually. Something like this will work:
App = Ember.Application.create({});
App.Router.map(function() {
this.resource('contacts', function() {
this.resource('contact', function() {
});
});
});
App.ContactController = Ember.Controller.extend({
add: function() {
alert('App.ContactController.add() was called!');
}
});
App.ContactsController = Ember.Controller.extend({
needs: ['contact'],
add: function() {
this.get('controllers.contact').add();
}
});
http://jsbin.com/osapal/1/edit

Connecting a View in connectOutlet with Ember RC1

From this [EDIT] [ToDo's sample]1, [/EDIT] I can connect a View via the connectOutlet. Is there an updated example for this using RC1?
index: Ember.Route.extend({
route: '/',
connectOutlets: function( router ) {
var controller = router.get( 'applicationController' );
var context = controller.namespace.entriesController;
context.set( 'filterBy', '' );
// This require was left here exclusively for design purposes
// Loads decoupled controller/view based on current route
require([ 'app/controllers/todos', 'app/views/items' ],
function( TodosController, ItemsView ) {
controller.connectOutlet({
viewClass: ItemsView,
controller: TodosController.create(),
context: context
});
}
);
}
}),
Actually the example you are linking should work. As you might know the Router API has changed and the code based on pre4 should still work. I am not aware of the requirements for the Todos App, so i cannot 100% tell, if it still works:
Todos.Router.map(function() {
this.resource('todos', { path: '/' }, function() {
this.route('active');
this.route('completed');
});
});
Todos.TodosRoute = Ember.Route.extend({
model: function() {
return Todos.Todo.find();
}
});
Todos.TodosIndexRoute = Ember.Route.extend({
setupController: function() {
var todos = Todos.Todo.find();
this.controllerFor('todos').set('filteredTodos', todos);
}
});
Here a little summary of the changes to the old router API:
You don't extend the Ember.Router Class anymore.
The URL Mappings don't reside in the Routes anymore. This is done via Todos.Router.map.
There is no connectOutlets event anymore in your routes. Instead there are 3 events you can implement: model(), setupController() & renderTemplate().
A little explanation on the hooks:
model(): Is called once when your route is entered via URL. This should return your model, which should become the content of your controller.
setupController(): Here you can get your controller and set its content how you may like. The default implementation sets the controller, that is name matching your route to the result of model().
renderTemplate(): Inside this hook you should use the new render method of routes to do the rendering. The render method is somehow the method that matches the old connectOutlets the most. There is also default implementation. Therefore it is also not implemented in the pre4 version of todomvc.
As Milkyway stated, you realy have to read the guides, but i hope this gets you started a little bit better.