emberjs - how to mark active menu item using router infrastructure
Asked Answered
D

15

51

I'm trying to create navigation tabs (taken from Twitter Bootstrap):

<ul class="nav nav-tabs">
    <li class="active"><a href="#">Home</a></li>
    <li><a href="#">Profile</a></li>
    <li><a href="#">Messages</a></li>
</ul>

The active tab is marked with class="active".

There is great example of static navbar and Router/Outlet feature at http://jsfiddle.net/schawaska/pfbva/, but I can't understand how to create a dynamic navbar/menu/tab view.

As far as I understand, it is possible to use class bindings in each menu item:

 classNameBindings: ['isActive:active']

But where is the right place to switch isActive attributes ?

Draft answered 24/7, 2012 at 10:2 Comment(1)
FYI: In the new router, {{linkTo}} provides this behavior automatically -- emberjs.com/guides/routing/defining-your-routesSemifluid
R
26

If you're using Ember >= 1.11, then https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructure below is the correct answer.


I would create a NavigationView, see http://jsfiddle.net/pangratz666/z8ssG/:

Handlebars:

<script type="text/x-handlebars" data-template-name="navigation">
    <ul class="nav nav-tabs">
        {{#view view.NavItemView item="home" }}
            <a {{action gotoHome}} >Home</a>
        {{/view}}
        {{#view view.NavItemView item="profiles" }}
            <a {{action gotoProfiles}} >Profiles</a>
        {{/view}}
        {{#view view.NavItemView item="messages" }}
            <a {{action gotoMessages}} >Messages</a>
        {{/view}}        
    </ul>
</script>

JavaScript:

App.NavigationView = Em.View.extend({
    templateName: 'navigation',
    selectedBinding: 'controller.selected',
    NavItemView: Ember.View.extend({
        tagName: 'li',
        classNameBindings: 'isActive:active'.w(),
        isActive: function() {
            return this.get('item') === this.get('parentView.selected');
        }.property('item', 'parentView.selected').cacheable()
    })
});

And inside your route's connectOutlets you have to set the current navigation item via router.set('navigationController.selected', 'home'); ...


Also take a look at the ember-bootstrap repository, which wraps this and more features of Bootstrap inside Ember.js

Reeta answered 24/7, 2012 at 11:34 Comment(6)
can you update your answer to reflect changes in the new router? Check out #14328795.Semifluid
what if you want the tabs to change without the url changing? (while also still using the router to get to the tabs, which are at say /wizards/:wizard_id/edit and there are 3 steps to edit in the wizard which you don't want to change the url.)Hoitytoity
FWIW, I updated pangratz's example as illustrated with this jsfiddleGerber
@bazzel: Unfortunately this is not working any more.Hart
There should be better way that is not interresting, I have often to connect route logic to present logic. That is a bad wayEnterotomy
This should not be the accepted answer anymore, as this won`t work for any recent ember version. I tried the summarize the working solutions here: https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructureBilbao
D
153

Ember 1.11+:

{{#link-to "dashboard" tagName="li"}}
  <a href="{{view.href}}">Dashboard</a>
{{/link-to}}

Ember < 1.11 (bind-attr required):

{{#link-to "dashboard" tagName="li"}}
  <a {{bind-attr href="view.href"}}>Dashboard</a>
{{/link-to}}
Dingbat answered 24/1, 2013 at 12:8 Comment(12)
This needs to be the accepted answer. I just spent 10 minutes trying to get a bindAttr on an <li> tag to reference the view.href by name until I found this and realized I had it all backwards.Obelisk
@Obelisk Agree, I've posted a full solution below combining the two pieces.Sustain
This is definitely the easiest way to do itErdman
The problem is that this approach doesn't work with a nested menuErdman
I guess with a nested menu, you'll need some specific component to handle it for you. Anyway this is the cleanest and easiest way to do it in the context of the original question.Heterodyne
Definitely the best answer I found out of the group.Krisha
This is definately the most easiest way to do it! Should be the accepted answer.Mauritamauritania
Just adding my support. A much better answer.Donnelldonnelly
How exactly is the poor soul who comes across my code after I've inserted this oddity supposed to know that it will insert an 'active' class in the li? Very unclear.Intertidal
You'll want to explicitly style your cursor to be a pointer when over the link element: a {cursor: pointer;} See marceldegraaf.net/2013/05/17/…Hut
AFAIK this solution will not work anymore as href wont be generated for a non a tags: discuss.emberjs.com/t/tagname-and-html-best-practices/4260/3Jepum
This sort of works but the href for <a> tag does not work so command + click won't open in new tabsSaprophagous
R
26

If you're using Ember >= 1.11, then https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructure below is the correct answer.


I would create a NavigationView, see http://jsfiddle.net/pangratz666/z8ssG/:

Handlebars:

<script type="text/x-handlebars" data-template-name="navigation">
    <ul class="nav nav-tabs">
        {{#view view.NavItemView item="home" }}
            <a {{action gotoHome}} >Home</a>
        {{/view}}
        {{#view view.NavItemView item="profiles" }}
            <a {{action gotoProfiles}} >Profiles</a>
        {{/view}}
        {{#view view.NavItemView item="messages" }}
            <a {{action gotoMessages}} >Messages</a>
        {{/view}}        
    </ul>
</script>

JavaScript:

App.NavigationView = Em.View.extend({
    templateName: 'navigation',
    selectedBinding: 'controller.selected',
    NavItemView: Ember.View.extend({
        tagName: 'li',
        classNameBindings: 'isActive:active'.w(),
        isActive: function() {
            return this.get('item') === this.get('parentView.selected');
        }.property('item', 'parentView.selected').cacheable()
    })
});

And inside your route's connectOutlets you have to set the current navigation item via router.set('navigationController.selected', 'home'); ...


Also take a look at the ember-bootstrap repository, which wraps this and more features of Bootstrap inside Ember.js

Reeta answered 24/7, 2012 at 11:34 Comment(6)
can you update your answer to reflect changes in the new router? Check out #14328795.Semifluid
what if you want the tabs to change without the url changing? (while also still using the router to get to the tabs, which are at say /wizards/:wizard_id/edit and there are 3 steps to edit in the wizard which you don't want to change the url.)Hoitytoity
FWIW, I updated pangratz's example as illustrated with this jsfiddleGerber
@bazzel: Unfortunately this is not working any more.Hart
There should be better way that is not interresting, I have often to connect route logic to present logic. That is a bad wayEnterotomy
This should not be the accepted answer anymore, as this won`t work for any recent ember version. I tried the summarize the working solutions here: https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructureBilbao
T
16

Some of the above suggestions are still valid for twitter bootstrap case. You can also try something like this

{{#link-to 'dashboard' tagName='li'}} 
  {{#link-to 'dashboard'}}Link Title{{/link-to}}
{{/link-to}}
  1. The link-to with li tagName applies the active class to the li
  2. The inner link-to would be a anchor element which gives you Open in New Tab functionality when right-clicked
Transpose answered 31/5, 2014 at 5:17 Comment(1)
This is what we do, ideal for TWBS tabsHomecoming
E
10

Recently an Ember-cli addon came available to just do this. It is called ember-cli-active-link-wrapper.

Install: ember install ember-cli-active-link-wrapper

You can use it like this:

{{#active-link}}
  {{link-to "Index" "index"}}
{{/active-link}}

which results in:

<li class='active'>
    <a href="/" class='active'>Index</a>
</li>
Erdman answered 29/4, 2015 at 9:38 Comment(1)
This should really be the accepted answer. All the higher-upvoted ones are broken in modern Ember or otherwise unmaintainable.Bandage
B
7

I know this is old post, but here are updates for Ember 2.4.0

For creating links you can write

{{#link-to 'photoGallery'}}
  Great Hamster Photos
{{/link-to}}

or

{{link-to 'Great Hamster Photos' 'photoGallery'}}

Ember will automatically set class to active when current route matches link's route (in this example photoGallery).

If you want to control 'active' class on other routes as well, you can do it by setting current-when attribute.

{{#link-to 'photoGallery' current-when='photoGallery photoPreview'}}
  Great Hamster Photos
{{/link-to}}

This link will have active class on both photoGallery and photoPreview routes.

https://github.com/emberjs/ember.js/blob/v2.4.0/packages/ember-routing-views/lib/components/link-to.js#L140

Bandore answered 12/3, 2016 at 8:40 Comment(0)
D
4

Handlebars

<ul class="nav">
    <li>{{#linkTo "index"}}Index{{/linkTo}}</li>
    <li>{{#linkTo "about"}}About{{/linkTo}}</li>
</ul>

Javascript

App.Router.map(function() {
    this.route("about");
});

It will add active class automatically based on route. Note: It is tested using ember-1.0.0-pre.4.js

Drava answered 16/2, 2013 at 12:51 Comment(1)
This works, the only issue I'm having is that it doesn't set the default on my root link. I render my index template to my 'Intro' controller, but this doesn't set the class on the link. Any idea how to do that?Pleuro
M
3

You can also change the isActive method into something like this:

isActive: function() {
    return App.getPath('router.currentState.path') === "root.firms";
}.property("App.router.currentState"),

or

isActive: function() {
    return this.get('controller.target.currentState.path') === "root.firms";
}.property("controller.target.currentState"),
Marguerita answered 2/8, 2012 at 7:8 Comment(0)
Y
3

I see this question is quite old, but if you upgraded Ember.js to the RC3 you can use tagName property, like:

{{#link-to messages tagName="li"}}Messages{{/link-to}}

Here is the API - http://emberjs.com/api/classes/Ember.LinkView.html

Yachting answered 26/4, 2013 at 23:38 Comment(0)
M
1

Not sure if it's very dynamic but try to see solution at http://codebrief.com/2012/07/anatomy-of-an-ember-dot-js-app-part-i-redux-routing-and-outlets/ The main idea is to check state of your app

JavaScript:

function stateFlag(name) {
  return Ember.computed(function() {
    var state = App.router.currentState;
    while(state) {
      if(state.name === name) return true;
      state = state.get('parentState');
    }
    return false;
  }).property('App.router.currentState');
}

ApplicationController: Ember.Controller.extend({
    isHome: stateFlag('home'),
    isSections: stateFlag('sections'),
    isItems: stateFlag('items')
  })

Handlebars:

<li class="home" {{bindAttr class="isHome:active"}}>
</li>
<li class="sections" {{bindAttr class="isSections:active"}}>
</li>
<li class="items" {{bindAttr class="isItems:active"}}>
</li>

Update: pangratz's solution looks prettier

Monkfish answered 24/7, 2012 at 11:14 Comment(0)
S
1

Here's a full working solution:

View:

App.NavView = Ember.View.extend({
  tagName: 'li',
  classNameBindings: ['active'],

  active: function() {
    return this.get('childViews.firstObject.active');
  }.property()
});

Template:

<ul>
  {{#each item in controller}}
  {{#view App.NavView}}
  {{#linkTo "item" item tagName="li"}}
      <a {{bindAttr href="view.href"}}>
        {{ item.name }}
      </a>
  {{/linkTo}}
  {{/view}}
  {{/each}}
</ul>
Sustain answered 14/2, 2013 at 21:20 Comment(0)
B
1

Beginning with v0.8.0 ember-bootstrap supports navs, including handling the active state correctly. And that without any link-to/tagName kind of hacks:

{{#bs-nav type="pills"}}
   {{#bs-nav-item}}
      {{#link-to "foo"}}Foo{{/link-to}}
   {{/bs-nav-item}}
   {{#bs-nav-item}}
     {{#link-to "bar"}}Bar{{/link-to}}
   {{/bs-nav-item}}
 {{/bs-nav}}

See http://kaliber5.github.io/ember-bootstrap/api/classes/Components.Nav.html

Bilbao answered 9/7, 2016 at 8:37 Comment(0)
B
1

A lot of the proposed solutions here do not work for any recent Ember version (e.g. views being deprecated). Moreover just using the link-to helper will not solve the issue, as bootstrap expects the active class to be present on the <li> not the <a>!

So I will try to summarize the solutions that do in fact work as of now:

use ember-cli-active-link-wrapper

The addon provides a component for this special use case:

<ul class="nav nav-tabs">
  {{#active-link}}
    {{#link-to "foo"}}Foo{{/link-to}}
  {{/active-link}}
  {{#active-link}}
    {{#link-to "bar"}}Bar{{/link-to}}
  {{/active-link}}
</ul>

Taken from https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructure

use ember-bootstrap

ember-bootstrap provides many components that integrate bootstrap functionality into your ember app, amongst them the nav components:

{{#bs-nav type="tabs"}}
   {{#bs-nav-item}}
      {{#link-to "foo"}}Foo{{/link-to}}
   {{/bs-nav-item}}
   {{#bs-nav-item}}
      {{#link-to "bar"}}Bar{{/link-to}}
   {{/bs-nav-item}}
 {{/bs-nav}}

Taken from https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructure

link-to Hack

Somewhat hacky, but should work without any additional addon:

<ul class="nav nav-tabs">
  {{#link-to "foo" tagName="li"}} 
    {{#link-to "foo"}}Foo{{/link-to}}
  {{/link-to}}
  {{#link-to "bar" tagName="li"}} 
    {{#link-to "bar"}}Bar{{/link-to}}
  {{/link-to}}
</ul>

Taken from https://mcmap.net/q/344310/-emberjs-how-to-mark-active-menu-item-using-router-infrastructure

Bilbao answered 28/7, 2016 at 10:54 Comment(0)
G
0

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'
    )
  ]
)
Groceryman answered 27/12, 2012 at 19:18 Comment(0)
F
0

baijum's answer above is mostly correct however in the latest versions of Ember the "bind-attr" is deprecated. Here is the new way to write it:

{{#link-to "dashboard" tagName="li"}}
    <a href="{{view.href}}">Dashboard</a>
{{/link-to}}

As you can see, it's even easier and kind of works like magic..

Flirt answered 21/7, 2015 at 16:35 Comment(0)
O
0

Like other people said, using {{#link-to}} to link to an existing route, when that route is current URL, {{#link-to}} will automatically add active to its CSS classes.

See Ember issue 4387

Or answered 31/10, 2016 at 19:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.