Right way to do navigation with Ember
What's the "right way" (or at least the options, if there is no single "Ember way" of doing this) for a navigation sidebar? Should I be looking at ContainerViews somehow, or should I just use the new outlet feature and stick the navigation inside my application view?
In addition, what's the "right way" to set an .active class on an li depending on the URL (I'm using routing)? Is there some kind of a helper for this?
Solution 1:
<Update date="2013-01-16">
The previous examples are no longer valid since the recent changes in the Router API, so I'm not going to fix those. All applications using the old router should migrate as soon as possible to the latest.
Here's an updated fiddle: Source | Live Demo
I will be maintaining/fixing/adding features every so often.
Edit on Apr 3rd 2013:
Alternative Sample - WIP: source | demo
ALL CONTENT BELOW IS DEPRECATED
<Update>
2012-Nov-09
Just adding a link to a better sample that also addresses the part which the OP wants to set a class in the current navbar item when a given route is selected
Source in JSFiddle (Click here to see it running)
If you look in the NavigationController you'll see the 'selected' property, which I use to check in the isActive method of the child view NavigationItemView. The isActive will return true or false based on the value in the menu
property of the same view (defined in the template) and the value in the property selected
(of the controller). Then check the classNameBinding expressions which either sets 'active' or nothing to the class of that particular child.
Also check the ConnectOutlets which is where I'm marking that nav item as selected.
This is running on ember-latest. I'm also using a little of Ember.Bootstrap as well as some of the original Twitter Bootstrap features/classes/etc (but I've replaced the styles with metro-bootstrap).
Not pasting the whole thing here because of space and all. I will leave the original code and link to the same for reference to the original question/answer.
I'll keep updating this new sample just because ember is fun =)
</Update>
This fiddle shows a static navbar from a view, and the outlet is used only for content display, or you can go directly to the display to see it running
As you can see, you can use a simple view containing your action links and have this view rendered in you application main view. The sub-routes under "starting" also have a mini navbar, coming from a similar template.
Handlebars:
<script type="text/x-handlebars" data-template-name="application">
<h1>My Ember Application</h1>
{{view App.NavbarView controllerBinding="controller.controllers.navbarController"}}
<br /><hr />
<div class="content">
{{outlet}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="navbar">
<ul>
<li><a href="#" {{action gotoHome}}>Home</a></li>
<li><a href="#" {{action gotoStarting}}>Getting Started</a></li>
<li><a href="#" {{action gotoCommunity}}>Community</a></li>
</ul>
</script>
<script type="text/x-handlebars" data-template-name="getting-started-menu">
<ul>
<li><a href="#" {{action gotoIndex}}>Overview</a></li>
<li><a href="#" {{action gotoMVC}}>About MVC</a></li>
<li><a href="#" {{action gotoEmber}}>About Ember</a></li>
</ul>
</script>
<script type="text/x-handlebars" data-template-name="home">
<h2>Welcome</h2>
<br />
<img src="http://emberjs.com/images/about/ember-productivity-sm.png" alt="ember logo" />
<br />
<br />
<p>Bacon ipsum dolor sit amet qui ullamco exercitation, shankle beef sed bacon ground round kielbasa in. Prosciutto pig bresaola, qui meatloaf ea tongue non dolore et pork belly andouille ribeye spare ribs enim. Enim exercitation elit, brisket nisi ex swine in jerky consequat pastrami dolore sed ad. In drumstick cow, salami swine fatback short ribs ham ut in shankle consequat corned beef id. Deserunt prosciutto beef speck. Sirloin incididunt kielbasa excepteur irure.</p>
<p>Do beef ribs dolore swine chicken shankle, venison officia qui magna ea anim. Jerky shank shankle, tongue in pork loin commodo boudin elit cupidatat turducken id capicola meatball. Strip steak ham hock tenderloin, id chicken drumstick sint jerky. Dolore veniam cillum minim, pariatur est beef. Sunt fatback tri-tip ex chuck.</p>
<br />
<br />
<strong>Note</strong>: This is a basic template with no <i>bindings</i>
</script>
<script type="text/x-handlebars" data-template-name="starting">
<h2>Getting Started with Ember</h2>
{{view App.StartingMenuView}}
<br />
<br />
<br />
<p>Bacon ipsum dolor sit amet qui ullamco exercitation, shankle beef sed bacon ground round kielbasa in. Prosciutto pig bresaola, qui meatloaf ea tongue non dolore et pork belly andouille ribeye spare ribs enim. Enim exercitation elit, brisket nisi ex swine in jerky consequat pastrami dolore sed ad. In drumstick cow, salami swine fatback short ribs ham ut in shankle consequat corned beef id. Deserunt prosciutto beef speck. Sirloin incididunt kielbasa excepteur irure.</p>
<p>Do beef ribs dolore swine chicken shankle, venison officia qui magna ea anim. Jerky shank shankle, tongue in pork loin commodo boudin elit cupidatat turducken id capicola meatball. Strip steak ham hock tenderloin, id chicken drumstick sint jerky. Dolore veniam cillum minim, pariatur est beef. Sunt fatback tri-tip ex chuck.</p>
<br />
<br />
<strong>Note</strong>: This is a basic template has a menu view embedded
</script>
<script type="text/x-handlebars" data-template-name="about-mvc">
<h2>About MVC</h2>
{{view App.StartingMenuView}}
<br /><br />
<br /><p>
Model–View–Controller (MVC) is a software design for interactive computer user interfaces that separates the representation of information from the user's interaction with it.[1][8] The model consists of application data and business rules, and the controller mediates input, converting it to commands for the model or view.[3] A view can be any output representation of data, such as a chart or a diagram. Multiple views of the same data are possible, such as a pie chart for management and a tabular view for accountants.
</p>
Read more at <a href="http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller" target="_blank">Wikipedia</a>
<br />
<br />
<strong>Note</strong>: This is a basic template has a menu view embedded
</script>
<script type="text/x-handlebars" data-template-name="about-ember">
<h2>About Ember</h2>
{{view App.StartingMenuView}}
<br /><br />
<br />
<p>A framework for creating <strong>ambitious</strong> web applications</p>
Read more at <a href="http://emberjs.com/about/" target="_blank">emberjs.com</a>
<br />
<br />
<p>Bacon ipsum dolor sit amet qui ullamco exercitation, shankle beef sed bacon ground round kielbasa in. Prosciutto pig bresaola, qui meatloaf ea tongue non dolore et pork belly andouille ribeye spare ribs enim. Enim exercitation elit, brisket nisi ex swine in jerky consequat pastrami dolore sed ad. In drumstick cow, salami swine fatback short ribs ham ut in shankle consequat corned beef id. Deserunt prosciutto beef speck. Sirloin incididunt kielbasa excepteur irure.</p>
<p>Do beef ribs dolore swine chicken shankle, venison officia qui magna ea anim. Jerky shank shankle, tongue in pork loin commodo boudin elit cupidatat turducken id capicola meatball. Strip steak ham hock tenderloin, id chicken drumstick sint jerky. Dolore veniam cillum minim, pariatur est beef. Sunt fatback tri-tip ex chuck.</p><br />
<br />
<strong>Note</strong>: This is a basic template has a menu view embedded
</script>
<script type="text/x-handlebars" data-template-name="community">
<h1>Ember Community</h1>
<p>
Get in touch with the community
</p>
<p>Featured contact info:</p>
{{#each item in content}}
<a {{bindAttr href="item.linkUrl" }} target="_blank">
<img height="32" width="32" {{bindAttr src="item.imageUrl" title="item.displayName" alt="item.displayName"}} /><br />
{{item.displayName}}
</a><br />
{{/each}}
<br />
Check more information about ember community at <a href="http://emberjs.com/community/" target="_blank">emberjs.com</a>
<br />
<br />
<strong>Note</strong>: This is a template with a <i>foreach</i> type of loop
</script>
JavaScript:
App = Em.Application.create();
App.ApplicationController = Em.Controller.extend();
App.ApplicationView = Em.View.extend({
templateName: 'application'
});
App.HomeController = Em.Controller.extend();
App.HomeView = Em.View.extend({
templateName: 'home'
});
App.NavbarController = Em.Controller.extend();
App.NavbarView = Em.View.extend({
templateName: 'navbar'
});
App.StartingController = Em.Controller.extend();
App.StartingView = Em.View.extend({
templateName: 'starting'
});
App.StartingMenuController = Em.Controller.extend();
App.StartingMenuView = Em.View.extend({
templateName: 'getting-started-menu'
});
App.AboutMVCController = Em.Controller.extend();
App.AboutMVCView = Em.View.extend({
templateName: 'about-mvc'
});
App.AboutEmberController = Em.Controller.extend();
App.AboutEmberView = Em.View.extend({
templateName: 'about-ember'
});
App.CommunityModel = Em.Object.extend({
displayName: null,
linkUrl: null,
imageUrl: null
});
App.CommunityController = Em.ArrayController.extend({
content: [],
init: function() {
this._super();
this.pushObject(
App.CommunityModel.create({
displayName: 'Twitter',
linkUrl: 'https://twitter.com/#!/emberjs',
imageUrl: 'http://icons.iconarchive.com/icons/iconshots/social-media-network/32/twitter-icon.png'
}));
this.pushObject(
App.CommunityModel.create({
displayName: 'GitHub',
linkUrl: 'https://github.com/emberjs/ember.js',
imageUrl: 'http://www.workinprogress.ca/wp-content/uploads/github.png'
}));
}
});
App.CommunityView = Em.View.extend({
templateName: 'community',
contentBinding: 'App.CommunityController.content'
});
App.Router = Em.Router.extend({
enableLogging: true,
location: 'hash',
root: Em.Route.extend({
// EVENTS
gotoHome: Ember.Route.transitionTo('home'),
gotoStarting: Ember.Route.transitionTo('starting.index'),
gotoCommunity: Ember.Route.transitionTo('community.index'),
// STATES
home: Em.Route.extend({
route: '/',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('home');
}
}),
starting: Em.Route.extend({
// SETUP
route: '/starting',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('starting');
},
// EVENTS
gotoMVC: Ember.Route.transitionTo('mvc'),
gotoEmber: Ember.Route.transitionTo('ember'),
gotoIndex: Ember.Route.transitionTo('index'),
// STATES
index: Em.Route.extend({
route: '/',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('starting');
}
}),
mvc: Em.Route.extend({
route: '/mvc',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('aboutMVC');
}
}),
ember: Em.Route.extend({
route: '/ember',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('aboutEmber');
}
})
}),
community: Em.Route.extend({
// SETUP
route: '/community',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('community');
},
// EVENTS
// STATES
index: Em.Route.extend({
route: '/',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet('community');
}
})
})
})
});
App.initialize();
Solution 2:
I didn't like MilkyWayJoe's answer because if you sooner or later want to change the naming of your states or whatever you have to go through the code AND the view as well, also adding a function to transitionTo every route seems not desirable. My approach is a bit more programmatic and modularized:
# Parent View-Tamplate, holding the navbar DOM elements
App.NavView = Ember.View.extend(
controller: App.NavArrayController
templateName: "ember-nav"
)
# We push NavItems into this array
App.NavArrayController = Ember.ArrayController.create(
content: Ember.A([])
)
# NavItem has two settable properties and
# an programmatic active state depending on the router
App.NavItem = Ember.Object.extend(
title: ''
goto: null # <=this is the name of the state we want to go to!
active: (->
if App.router.currentState.name == @.get "goto"
true
else
false
).property('App.router.currentState.name').cacheable()
)
# the actual NavElement which gets the class="active" if the
# property "active" is true, plus a on-click binding to
# make the Router transition to this state
App.NavItemView = Ember.View.extend(
tagName: "li"
classNameBindings: ["active"]
click: ->
App.router.transitionTo(@get('goto'))
false
)
nav-view.hbs (for twitter-bootstrap-style navs)
<div class="nav-collapse collapse">
<ul class="nav">
{{#each App.NavArrayController}}
{{#view App.NavItemView classBinding="active" gotoBinding="goto"}}
<a href="#" {{bindAttr data-goto="goto"}}> {{title}}</a>
{{/view}}
{{/each}}
</ul>
</div>
This way, I can just create and mess around with my routes in the Router, and keep the Nav-Definitions side-by-side:
# put this somewhere close to the Router
App.NavArrayController.pushObjects(
[
App.NavItem.create(
title: 'Home'
goto: 'home'
),
App.NavItem.create(
title: 'Chat'
goto: 'chat'
),
App.NavItem.create(
title: 'Test'
goto: 'test'
)
]
)