I'm experimenting with routing in ember at the moment, and have a working example. The problem is, I'm a bit confused WHY it works. Currently this route just has 2 simple views. Here is the code:
App = Em.Application.create();
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
redirectsTo: 'home' //when hitting the base URL, redirect to home
}),
home: Ember.Route.extend({
route: '/home',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('home');
}
}),
about: Ember.Route.extend({
route: '/about',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('about');
}
})
})
});
//Main controller + view
App.ApplicationController = Ember.Controller.extend({});
App.ApplicationView = Ember.View.extend({
templateName: 'application',
goHome: function(){
App.router.transitionTo('home');
},
goAbout: function(){
App.router.transitionTo('about');
}
});
// Home page
App.HomeView = Ember.View.extend({
templateName: 'home'
})
// About page
App.AboutController = Ember.Controller.extend({
numWidgets: 45
})
App.AboutView = Ember.View.extend({
numWidgetsBinding: 'App.aboutController.numWidgets',
templateName: 'about'
})
App.initialize();
In my HTML I just have a couple of really simple templates with the names "application", "home" and "about".
So, it all works, and looks very similar to all the examples floating about on the net. Great! But I'm confused about how it seems I have several things instantiated for me, without me asking to do it. Is this correct?
For example:
How is it creating an instance of ApplicationController?
In the connectOutlets functions, it's looking for a controller called "applicationController". I never created anything called "applicationController" (with lower-case "a"), I just extended a controller and called it "ApplicationController" (with a capital "A"). Why does this work?
How is it creating an instance of AboutController?
I did a simple test binding between the "about" page view and controller. In the view, I am binding with the variable 'App.aboutController.numWidgets'. I never called App.AboutController.create(). So how is there an instance of this ready for me to talk to? Again, it has a lower case letter ("aboutController"). All I ever did was extend a controller (and named it with a capital letter - "AboutController")
A little explanation would be great, as like any normal developer, I feel that using code where you dont know why it's working is crazy!
App.initialize(); does all the instantiation and injection stuff :), based on strong naming conventions: Ember naming / capitalization convention. When you call xxxController.connectOutlet(options), the option has is also conventional, see Confusion about naming conventions in emberjs
Hope that helps.
EDIT: With the latest master, you don't have to call App.initialize() manually. The application is auto-initialized when all is ready :)
Related
I have two controllers: postsController and postController. the post route is NOT nested under posts ( I do not want to do so because I want the posts view to be replaced by the post view, not added to it.)
This is what my router looks like.
this.resource('posts', {path: '/'}, function(){
// this.route('index', {path: '/'});
});
this.resource('post', { path: 'posts/:id' }, function(){
this.route('show');
}
Now, the postsController:
App.PostsRoute = Ember.Route.extend({
model: function(){
return this.store.find('post');
},
})
App.PostsController = Ember.ArrayController.extend({
testProperty: "This is a test"
})
And the PostController:
App.PostRoute = Ember.Route.extend({
model: function(params){
return this.store.find('post', params.id);
},
})
App.PostController = Ember.ObjectController.extend({
needs: ['posts'],
percentValue: 100,
progressBarWidth: null,
advancePost: function(delta) {
var that = this;
var posts = that.get('controllers.posts');
// debugger <-- This is where all my questions are concerned with.
...
},
actions: {
nextPost: function() {
this.advancePost(1);
},
previousPost: function() {
this.advancePost(-1);
},
}
})
So. PostController 'needs' PostsController, and on action advancePost, needs to access PostsController's model (which is supposed to contain an array of Post Objects.) At the point where my debugger is, the variable posts accessed postsController. At this point, if I run
posts.get('testProperty') // => gives "This is a test", which is correct.
on the chrome console, I get "This is a test". So I know that I have gained access to the Posts Controller itself. However, any of the following attempts:
posts.get('model') // gives []??
posts.get('content') // gives []??
returns an empty array ( [] ).
I fail to understand why. If I have access to the Posts arrayController, why do I not have access to its data? Consequently, how would I gain access to its data?
I would greatly appreciate clarity on this.
First, if I understand your context, I think you should rather nest post under posts by doing this :
this.resource('posts', {path: '/'}, function(){
this.route('post', {path: '/:id'});
});
And then, since posts is your "parent" route, you no longer need to declare the needs dependency in your controller.
To access get your posts, apply the following in your Route :
setupController: function(controller, model) {
this._super(controller, model);
controller.set('posts', this.modelFor("posts").get("content"));
}
You should think about the interaction patterns you want to support. Is it typical for a user to go back to the list of items they were just looking at? Reloading posts whilst keeping the scroll position in the list they were at will be problematic. Also re-fetching from the server and tearing down and setting up the DOM has a lot more overhead than just leaving it there.
Are you intending to use animation on your transitions? Using something like liquid-fire will need both outlets rendered to perform an animation so it makes no sense to tear down the list in that case.
CSS can be used to have the nested outlet fill whatever container it is put in allowing the approach provided by Pascal Boutin to be used.
This is a follow-up from Understanding Ember routes.
Master/detail views are great but I'm trying to have a a hierarchical URL route without nesting their templates. However, I still need access to the parent model for things like breadcrumb links and other references.
So /users/1/posts should display a list of posts for user 1. And /users/1/posts/1 should display post 1 for user 1, but it shouldn't render inside the user template's {{outlet}}. Instead, it should completely replace the user template. However, I still need access to the current user in the post template so I can link back to the user, show the user's name, etc.
First I tried something like this (Method #1):
App.Router.map(function() {
this.resource('user', { path: '/users/:user_id' }, function() {
this.resource('posts', function() {
this.resource('post', { path: '/:post_id' });
});
});
});
...
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
renderTemplate: function() {
this.render('post', {
into: 'application'
});
}
});
This replaced the the user template with the post one, as expected. But when I click the browser's back button the user template doesn't render again. Apparently the post view is destroyed but the parent view is not re-inserted. There are a few questions on here that mention this.
I then got it to work with something like this (Method #2):
App.Router.map(function() {
this.resource('user', { path: '/users/:user_id' }, function() {
this.resource('posts');
this.resource('post', { path: '/users/:user_id/posts' }, function() {
this.resource('post.index', { path: '/:post_id' });
});
});
...
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.User.find(params.user_id);
},
setupController: function(controller, model) {
controller.set('user', model);
}
});
App.VideoIndexRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
}
});
App.PostIndexController = Ember.ObjectController.extend({
needs: 'post'
});
But this seems a bit hacky to me and not very DRY.
First, I need to retrieve the User again in the PostRoute and add it as an ad-hoc variable to the PostController (this wouldn't be necessary if the routes were properly nested and I could just set a needs: 'user' property in the PostController). In addition, this may or may not have an impact on the back-end depending on the adapter implementation of ember-data or whatever technology is used to retrieve the data from the server (i.e. it may cause an unnecessary second call to load User).
I also need an additional `PostIndexController' declaration just to add that new dependency, which is not a big deal.
Another thing that doesn't feel right is that /users/:user_id/posts appears twice in the router configuration (one nested, one not).
I can deal with these issues and it does work but i guess that, overall, it seems forced and not as graceful. I'm wondering if I'm missing some obvious configuration that will let me do this with regular nested routes or if someone has a recommendation for a more "Ember.js way" of doing this.
I should mention that regardless of the technical merits of Method #2, it took me quite a while to figure out how to make it work. It took a lot of searching, reading, experimenting, debugging, etc. to find just the right combination of route definitions. I would imagine that this is not a very unique use-case and it should be very straightforward for a user to set up something like this without spending hours of trial and error. I'll be happy to write up some tips for this in the Ember.js documentation if it ends up being the right approach.
Update:
Thanks to #spullen for clarifying this. My case was not as straightforward as the example because some sub-routes need nested templates and some don't, but the answer helped me figure it out. My final implementation looks something like this:
App.Router.map(function() {
this.resource('users', { path: '/users/:user_id' }, function() {
this.resource('users.index', { path: '' }, function() {
this.resource('posts')
});
this.resource('post', { path: '/posts/:post_id' }, function() {
this.resource('comments', function() {
this.resource('comment', { path: '/:comment_id' });
});
});
});
});
So now posts renders under the users template but post replaces everything. comments then renders under post and comment, in turn, renders under comments.
All of them are sub-routes of users so the user model is accessible to all of them without acrobatics, by doing this.modelFor('users') in each Route where needed.
So the templates look like this:
users
|- posts
post
|- comments
|-comment
I don't know why the { path: '' } is needed for the users.index resource definition but if I take it out Ember doesn't find the users route. I would love to get rid of that last vestige.
You could define the parent template to just display the outlet and have an index route which will get displayed inside that. Then for the nested resource you can do the same thing.
<script type="text/x-handlebars" data-template-name="user">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="user/index">
<h2>user/index</h2>
</script>
<script type="text/x-handlebars" data-template-name="posts">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="posts/index">
<h2>posts/index</h2>
</script>
That way it won't be a master/detail.
The router would be:
App.Router.map(function() {
this.resource('user', function() {
this.resource('posts', function() { });
});
});
Then if you need to get information about the parent you can use modelFor. So if you were in posts, you could do this.modelFor('user');
Here's a jsbin that demonstrates this.
Hope this is helpful.
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.
I'm trying to access an instance of a controller that has been wired automatically using App.initialize();
I've tried the below but it returns a Class not an instance.
Ember.get('App.router.invitesController')
I have a quick post about this exact subject on my Blog. It's a little big of a different method, but seems to work well for Ember.js RC1.
Check it out at: http://emersonlackey.com/article/emberjs-instance-of-controller-and-views
The basic idea is to do something like:
var myController = window.App.__container__.lookup('controller:Posts');
This answer works with RC1/RC2.
Now you can use the needs declaration in order to make the desired controller accessible. Here's an example:
Suppose I want to get something from my SettingsController from within my ApplicationController. I can do the following:
App.SettingsController = Ember.Controller.extend({
isPublic: true
});
App.ApplicationController = Ember.Controller.extend({
needs: 'settings',
isPublicBinding: 'controllers.settings.isPublic'
});
Now in the context of my ApplicationController, I can just do this.get('isPublic')
You can access a controller instance inside an action in the router via router.get('invitesController'), see http://jsfiddle.net/pangratz666/Pk4k2/:
App.InvitesController = Ember.ArrayController.extend();
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
route: '/',
index: Ember.Route.extend({
route: '/',
connectOutlets: function(router, context) {
var invitesController = router.get('invitesController');
},
anAction: function(router) {
var invitesController = router.get('invitesController');
}
})
})
});
You can access any controller instance by name using lookup method of Application instance.
To get Application instance you can use getOwner from any route or controller.
const controllerName = 'invites';
Ember.getOwner(this).lookup(`controller:${controllerName}`));
Works for me in Ember 2.4 - 3.4.
I implemented the following using latest ember.js (0.9.8.1) by referring to Ember Router not updating url in Chrome and Safari. When I try switch b/w routes via router.transitionTo('route path') method (last 3 lines of the attached snippet), browser url is not updated correctly, but I do see the view markup being updated confirming that state change do happen. Could some help in identifying whether am I missing something here?
BTW: I tested this in Chrome 20.0.1132.27 beta-m
App = Ember.Application.create({});
App.IndexView = Ember.View.extend({
template: Ember.Handlebars.compile(
'hello world from index'
)
});
App.ShowView = Ember.View.extend({
template: Ember.Handlebars.compile(
'hello world from show'
)
});
App.Router = Ember.Router.extend({
location: 'hash',
enableLogging: true,
root: Ember.State.extend({
index: Ember.State.extend({
route: '/',
redirectsTo: 'tasks'
}),
tasks: Ember.State.extend({
route: '/tasks',
index: Ember.ViewState.extend({
route: '/',
view: App.IndexView
}),
show: Ember.ViewState.extend({
route: '/show',
view: App.ShowView
})
})
})
});
var router = App.Router.create({});
App.initialize(router);
router.transitionTo('root');
router.transitionTo('root.tasks');
router.transitionTo('root.tasks.show');
I ran your code, and in the console, I have the following error "Object hash has no method 'setURL'". By debugging a bit, I found that you have to define the location of the Router with:
location = Ember.Location.create({ style: 'hash' })
or
location = Ember.Location.create({implementation: 'hash'})
I don't know why exactly, but it seems to work. Perhaps it's only due to ember version.
You are mixing up 2 initialization methods.
When you define App.Router all you need to do is call App.initialize(). It automatically creates an instance of Ember.Router and assigns it to the variable App.stateManager. You can then use App.stateManager to call transitionTo.
You can define the variable router to extend Ember.Router then call App.initialize(router). This method also creates an instance of Ember.router and assigns it to App.stateManager. You can then use App.stateManager to call transitionTo.
Either of the methods will work but I prefer method 1. To manipulate the route we always use App.stateManager.