Nested routes in Ember JS and Ember Rails - ember.js

Can someone explain in layman's terms the way nested outlets work in ember templates?
In particular trying understand this from the docs:
http://emberjs.com/guides/routing/rendering-a-template/
"The immediate parent route did not render into the main outlet ..."
This means that the current route tried to render into the parent
route's template, but the parent route didn't render a template, or,
if it did, that the template which the parent route provided did not
render into the main template (i.e., a default {{outlet}}).
More specifically I am trying understand how to create a nested view hierarchy in my app.
It is three layers deep of collections. I want to create a series of collapsible nested views, based on the contents of the collections. The data structure could be tree like.
Libraries -> each Library has many Books -> each Book has many Pages
Looking for an illustrative jsbin or code sample that demonstrates the nested template structure in practice.
Imagine a router that looks like this:
App.Router.map(function() {
this.resource('libraries', function() {
this.route('new');
this.resource('library', {path: ':library_id'}, function() {
this.resource('books', function() {
this.route('new');
this.resource('book', {path: ':book_id'}, function() {
this.resource('pages', function() {
this.route('new');
this.resource('page', {path: ':page_id'}, function() {
}); // Page
}); // Pages
}); // Book
}); // Books
}); // Library
}); // Libraries
}); // map

In general most templating languages provide some way to wrap the target content of a page into a main layout. This allows separation of the common page layout into another file and the smaller target template in a different file.
There have been a few iterations of this in Ember, currently this functionality is provided by the {{outlet}} helper. Outlets are Ember's way to yield into a layout.
The area where outlet diverges significantly from yield is nesting. Yielding on server-side is much simpler. You only need to mark areas of a template to yield into and then call to yield a block of content into that designated target.
However when the rendering of content is switched to client-side javascript, only parts of a page are updated on demand. You can no longer simply yield directly into markers. You need a smarter yield ie:- outlet.
There are 2 sides to an {{outlet}}.
A marker that indicates where you want to yield. This is the {{outlet}} helper.
Code that renders a template into this outlet. This is the render method used inside the renderTemplate hook.
By default an {{outlet}} does not need a name. This makes it the default outlet for that template. There can be many such outlets in a template and they can be specified by giving it a name. For eg:-
{{outlet 'sidebar'}}
{{outlet 'nav'}}
This declares 2 outlets named 'sidebar' and 'nav'. You can now render other templates into these outlets.
Default outlets are used when rendering without an explicit outlet name. For named outlets the rendering is done by calling render in a renderTemplate hook of a Route.
You do this by specifying an outlet option in a hash passed to the render method as options.
renderTemplate() {
this.render('recentPosts', { outlet: 'sidebar' });
}
Here, the template recentPosts will be rendered into an outlet named 'sidebar' inside its parent template.
When routes are nested inside other nested routes they will render into the nearest parent outlet. If the parent resource doesn't have a default outlet then it's parent is used, and so on until the application template is reached.
When you declare a resource with this.resource('posts'); in the Router, you are indicating a few things based on convention.
Render the posts route with the layout template posts.
Optionally, render the implicit posts.index route with the template posts/index.
The posts template contains layout common to all posts and it's sub resources. At the bare minimum it must contain at least a default outlet like, {{outlet}}.
Without this {{outlet}} child routes will not have an immediate parent outlet to render into. They will then render in that parent's parent or ultimately the application template's outlet. When this happens you will see the "The immediate parent route did not render into the main outlet ..." warning. Check the location of your outlets when this happens.
The posts.index is an implicit route given to all resources that have nested routes. In other words if your resource has nested routes, you don't need to explicitly declare a nested, this.route('index)`.
This index route can display the content of that resource. For instance, for posts.index, You can display a list of all posts. One secondary caveat with this implicit route is that the model is on the parent posts route. You have to use the needs api to get at this model in the PostsIndexController.
needs: ['posts'],
contentBinding: 'controller.posts'
Further, this posts.index route is optional. You can place the UI from posts/index used to display a list of posts, directly into the posts template itself. However this means any child resource will also render with the list of posts, along side the outlet in posts. The decision whether to use an explicit index route or not depends on the UI that needs to be displayed.
Sitting above all other templates is the application template. It must have an outlet for nested resources to render in, and will typically house the layout common to the page. If you don't specify an application template a default template will be used. This generated template is equivalent to {{outlet}}, ie:- a template with just a default outlet.
Consider the following routes.
App.Router.map(function() {
this.resource('posts', function() {
this.route('new')
this.resource('post', {path: ':post_id'}, function() {
this.resource('comments', function() {
this.route('new');
});
});
});
});
Here, posts.new will be rendered into posts which will be rendered inside posts, which will be rendered into the application template's default outlet. The rest of the templates used are listed below.
+---------------------------+--------------------------------------------------------+
| Route | Templates used (default outlets) |
+---------------------------+--------------------------------------------------------+
| posts.index | posts.index > posts > application |
+---------------------------+--------------------------------------------------------+
| posts.new | posts.new > posts > application |
+---------------------------+--------------------------------------------------------+
| posts.post.index | post.index > post > posts > application |
+---------------------------+--------------------------------------------------------+
| posts.post.new | post.new > post > posts > application |
+---------------------------+--------------------------------------------------------+
| posts.post.comments.index | comments.index > comments > post > posts > application |
+---------------------------+--------------------------------------------------------+
| posts.post.comments.new | comments.new > comments > post > posts > application |
+---------------------------+--------------------------------------------------------+
This default template hierarchy can be changed by specifying an into option to the render method.
renderTemplate: function() {
this.render('posts', { into: 'sidebar' })
}
Here the posts template will render into the default outlet of the sidebar template.
That's about it. Outlet is another ember concept that uses a good deal of convention over configuration. The defaults are quite good, at the same time easy to customize.

Related

Override application.hbs template in Emberjs

In emberjs, I am in a situation that my application already has routes template that uses the application.hbs template, but now I want to create a new route templates that doesn't use application.hbs.
Is there any easy solution for that?
I have seen many answers but that doesn't match my specification and also my version of ember is 2.11.
Thank you.
Keep application.hbs as minimal and common across all routes as you can. What you shoud do is generate top level routes for your application.
Say you have a common setup where you have an authenticated section, post login, and a login section. It is common for pre and post login to have differing top level templates. Try something like this:
ember g route login
ember g route login/index
ember g route login/forgot-password
ember g route authenticated
ember g route authenticated/index
ember g route authenticated/profile
etc...
Your login.hbs would have its own style, and potentially child routes, which will assume that style and place subsequent nested templates in the {{outlet}} that others have mentioned.
File Structure:
routes/
-login/
----index.hbs
----forgot-password.hbs
-authenticated/
----index.hbs
----profile.hbs
login.hbs
authenticated.hbs
application.hbs
In the example above, login.hbs might look like this:
{{yellow-navbar}}
{{outlet}}
and authenticated.hbs like this:
{{pink-navbar}}
{{user.name}}
{{outlet}}
Now, the login/index.hbs and login/forgot-password.hbs templates will render in the login.hbs outlet. both of these pages will render a yellow navbar, and then their own content.
Because authenticated.hbs is another top level parent route, both authenticated/index.hbs and authenticated/profile.hbs will render their content beneath a pink navbar and a display of the current user's name.
if your application.hbs looked like this:
{{#general-site-container}}
<h2>Website Name</h2>
{{outlet}}
{{/general-site-container}}
Then all of the routes, both authenticated and login, will be in the general-site-container, and will all show the h2 with the website name.
An important note in this, and something that I see a lot of people get confused with, is that this folder structure does not dictate the actual path of the route.
The router might be configured like this, to avoid showing "authenticated" in the url:
Router.js
// login.index route assumed at root url /login
this.route('login', { path: '/login' }, function() {
// avail at /login/forgot-password
this.route('forgot-password', { path: '/forgot-password' }
});
//authenticated.index.hbs assumed at root avail at /
//notice that the authenticated parent route has a route of "/"
this.route('authenticated', { path: '/' }, function() {
// authenticated.profile route avail at /profile
this.route('profile', { path: '/profile' });
// as an exmaple you can further nest content to your desire
// if for instance your profile personal info section has a parent
// template/route
this.route('', { path: '/personal-info' }, function() {
this.route('address', { path: '/address' }); //profile/personal-info/address
});
});
i think you need to use {{outlet}} for achieve this.
create diffrent outlets where you need to show diffrent templates by overriding application template
{{outlet}} //application hbs default one
{{outlet "view1"}} // another template
{{outlet "view2"}} //another template
there should be view1.hbs and view2.hbs in order to render those templates

Ember can't find my template when using the renderTemplate hook

I can’t figure out why Ember can’t find and render the template specified in my renderTemplate hook override. I have the following route configuration as per router.js:
this.route('posts', function() {
this.route('history', function() {
this.route('new', { path: '/new/:id'});
});
});
I’m also making use of my history index route. Therefore, my history.hbs is just an outlet, and all my templating for this route is actually housed in index.hbs. This way, I can render my new.hbs template on its own page.
In new.hbs, I’d like to render an outlet, in addition to some other content. To handle this, I attempted to override the renderTemplate hook in routes/new.js, like this:
renderTemplate() {
this.render('posts/history/test', {
into: 'new',
outlet: 'test',
});
}
I created the new template under templates/posts/history:test.hbs`. File structure for my templates folder looks like this:
templates > posts > history > new.hbs, test.hbs
Finally, in new.hbs, I added my outlet for this new template:
{{outlet 'test'}}
When I navigate to my new route, I get the following error:
Assertion Failed: You attempted to render into ’new' but it was not found. Anyone know how I can get past this?
I believe you can only render into an ancestor route.

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.

Rendering a controller/template/route into another template

This is probably a very common task, but I am not sure which is the right approach to tackle this.
I have a Route / Controller / Model setup that loads groups. In the header area before the main content I want to show notifications for the user. I have created a notificationcontroller that is an arraycontroller and as well as a route that loads all notifications.
I don't want this route to be used for anything else than within this area.
I have already tried in my groups.hbs to add an additional {{ outlet notification }} and try to render in it:
App.GroupsRoute = GambifyApp.BaseRoute.extend({
model: function() {
return this.get('store').find('group');
},
renderTemplate: function() {
this.render('notifications', { // the template to render
// into: 'notifications', // the route to render into
outlet: 'notification', // the name of the outlet in the route's template
controller: 'notifications', // the controller to use for the template
});
this.render('groups');
}
});
But somehow my notifications template is not used. For now there is not much in my NotificationsController (It's an ArrayController) , NotificationsRoute is only reading all Notifications as Model and the template is currently only text, which is not rendered in my groups.hbs
The template containing the outlet you want to render into must be rendered first in order to render into the outlet. Here is an example of rendering into a named outlet that is in the application template from the index route:
http://emberjs.jsbin.com/qoyi/1/edit?html,js,output
Leave a comment if this example doesn't clear it up for you.

{{outlet}}, {{view}}, {{render}}, and {{control}} helpers

I am trying to put together a simple master-details Ember app. Directory tree on one side and file list on another.
Ember offers few helpers to render context into a view. Which of them I can use for:
Subtrees of the directory tree.
Details list.
In fact, would be very helpful if someone can point me to any docs I can read about the difference between {{render view}}, {{view view}} and {{control view}} helpers and how to use them properly.
Thanks a lot!
{{view "directory"}} renders the view within the context of the current controller.
{{render "directory"}} renders the view App.DirectoryView with template directory within the context of the singleton App.DirectoryController
{{control directory}} behaves the same way as render only it creates a new instance of App.DirectoryController every time it renders (unlike render which uses the same controller instance every time).
Update 18 Feb 2014: {{control}} has been removed.
The last two helpers are relatively new, so there isn't much documentation about them. You can find {{view}} documentation here.
Now looking at your use case, I don't think you need any of these helpers. Just use nested routes and the {{outlet}} helper and it should just work.
App.Router.map(function(){
this.resource('directories', function() {
this.resource('directory', { path: '/:directory_id'}, function() {
this.route('files');
});
});
});
You can build on that following this guide.
UPDATE: {{render}} now creates a new instance every time if you pass a model.
For a very good explanation of the helpers render, partial, outlet and template have a look at this question.
Just as a rough a summary, how one might use those helpers:
{{render "navigation"}} -> Renders the NavigationController and NavigationView at this place. This is helper is good for places, where the Controller and View do not change, e.g. a navigation.
{{outlet "detailsOutlet"}} -> This will provide a stub/hook/point into which you can render Components(Controller + View). One would use this with the render method of routes. In your case you will likely have a details route which could look like this. This would render the DetailsController with DetailsView into the outlet 'detailsOutlet' of the index template.
App.DetailsRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('details', { // the template/view to render -> results in App.DetailsView
into: 'index', // the template to render into -> where the outlet is defined
outlet: 'detailsOutlet', // the name of the outlet in that template -> see above
});
}
});
{{view App.DetailsView}} -> This will render the given view, while preserving the current context/controller. One might change the context, e.g. using your master entity and pass its details to a view like this:
{{view App.DetailsView contextBinding="masterEntity.details"}}
This is helper is useful, when you want to encapsulate certain parts of a component in subviews, that have own custom logic like handling of actions/events.
{{control}} I know that control instantiates a new controller every time it is used, but I cannot see a good fit for your, nor have i a good example for using it.
To Understand the difference between ember {{render}},{{template}},{{view}},{{control}}
you can refer this article
http://darthdeus.github.io/blog/2013/02/10/render-control-partial-view/