22

I'm using Twitter Bootstrap for navigation in my Ember.js app. Bootstrap uses an active class on the li tag that wraps navigation links, rather than setting the active class on the link itself.

Ember.js's new linkTo helper will set an active class on the link but (as far as I can see) doesn't offer any to hook on to that property.

Right now, I'm using this ugly approach:

{{#linkTo "inbox" tagName="li"}}
  <a {{bindAttr href="view.href"}}>Inbox</a>
{{/linkTo}}

This will output:

<li class="active" href="/inbox"><a href="/inbox">Inbox</a></li>

Which is what I want, but is not valid HTML.

I also tried binding to the generated LinkView's active property from the parent view, but if you do that, the parent view will be rendered twice before it is inserted which triggers an error.

Apart from manually recreating the logic used internally by the linkTo helper to assign the active class to the link, is there a better way to achieve this effect?

Nick Ragaz
  • 1,352
  • 2
  • 11
  • 18
  • Due to a recent change this doesn't even work anymore (as the linkTo helper doesn't set the view context to its view anymore). – Bastes Feb 26 '13 at 18:55
  • 2
    For anyone who comes across this, I found these answers to be more helpful: http://stackoverflow.com/questions/11628489/emberjs-how-to-mark-active-menu-item-using-router-infrastructure – woz Mar 22 '13 at 20:17
  • 1
    Found a better solution [here](http://stackoverflow.com/questions/14412073/assigning-active-class-to-selected-list-item-in-emberjs). – Volox Apr 11 '13 at 11:08

9 Answers9

27

We definitely need a more public, permanent solution, but something like this should work for now.

The template:

<ul>
{{#view App.NavView}}
  {{#linkTo "about"}}About{{/linkTo}}
{{/view}}

{{#view App.NavView}}
  {{#linkTo "contacts"}}Contacts{{/linkTo}}
{{/view}}
</ul>

The view definition:

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

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

This relies on a couple of constraints:

  • The nav view contains a single, static child view
  • You are able to use a view for your <li>s. There's a lot of detail in the docs about how to customize a view's element from its JavaScript definition or from Handlebars.

I have supplied a live JSBin of this working.

Yehuda Katz
  • 28,535
  • 12
  • 89
  • 91
  • Is there any reason this wouldn't work with a CollectionView's itemViewClass? It looks to me like the instance of itemViewClass doesn't have any childViews. – mcginniwa Jan 19 '13 at 02:28
  • 1
    @YehudaKatz nice solution, I just found this after answering a related question. Figured I'd add comment here since my approach could help anyone stuck by one of the constraints you mentioned. Totally agreed something more public/permanent is needed, will give that some thought. http://stackoverflow.com/questions/14412073/assigning-active-class-to-selected-list-item-in-emberjs/14416595#14416595 – Mike Grassotti Jan 20 '13 at 06:25
  • 5
    This doesn't seem to work (anymore?) - it triggers a "you tried to re-render the view after it was rendered but before it was inserted into the DOM" error. My hack-ish solution still works. – Nick Ragaz Jan 20 '13 at 17:56
  • 1
    In the demo, the `li` doesn't get the `active` class when clicked. It's just the underlying anchor being changed by the default `linkTo` behaviour. Also, the `App.NavView.active` property doesn't have `childViews.firstObject.active` as a dependency so I don't get how it's supposed to update anyway. – Ilia Choly Jan 30 '13 at 16:29
  • 1
    It seems as if the child linkTo view is not yet rendered(created?) when the active-property is evaluated (triggered by the classNameBinding?). Hence the childViews array is empty. I added `.property("childViews.@each")` which triggered it to be re-evaluated when the childViews changed. This however causes the error @Nick Ragaz is describing in the comment above. (full disclosure, i'm just learning ember) – sris Feb 27 '13 at 10:39
  • Let's make that `property("childViews.@each.active")` in my comment above. – sris Feb 27 '13 at 11:19
  • 6
    Adding 'childViews.firstObject.active' to the property() call in Yehuda's jsbin fixed it for me. Here it is working: http://jsbin.com/apisic/1/edit – Zach Wily May 06 '13 at 20:30
  • The use of `viewName` on the `linkTo`/`link-to` wouldn't make the first constraint go away? – Renato Zannon Sep 06 '13 at 21:53
9

Well I took what @alexspeller great idea and converted it to ember-cli:

app/components/link-li.js

export default Em.Component.extend({
    tagName: 'li',
    classNameBindings: ['active'],
    active: function() {
        return this.get('childViews').anyBy('active');
    }.property('childViews.@each.active')
});

In my navbar I have:

{{#link-li}}
    {{#link-to "squares.index"}}Squares{{/link-to}}
{{/link-li}}
{{#link-li}}
    {{#link-to "games.index"}}Games{{/link-to}}
{{/link-li}}
{{#link-li}}
    {{#link-to "about"}}About{{/link-to}}
{{/link-li}}
Giacomo1968
  • 25,759
  • 11
  • 71
  • 103
Adam Klein
  • 91
  • 2
  • 3
8

You can also use nested link-to's:

{{#link-to "ccprPracticeSession.info" controller.controllers.ccprPatient.content content tagName='li' href=false eventName='dummy'}}
  {{#link-to "ccprPracticeSession.info" controller.controllers.ccprPatient.content content}}Info{{/link-to}}
{{/link-to}}
pjlammertyn
  • 989
  • 6
  • 10
6

Building on katz' answer, you can have the active property be recomputed when the nav element's parentView is clicked.

App.NavView = Em.View.extend({

    tagName: 'li',
    classNameBindings: 'active'.w(),

    didInsertElement: function () {
        this._super();
        var _this = this;
        this.get('parentView').on('click', function () {
            _this.notifyPropertyChange('active');
        });
    },

    active: function () {
        return this.get('childViews.firstObject.active');
    }.property()
});
Ilia Choly
  • 18,070
  • 14
  • 92
  • 160
5

I have just written a component to make this a bit nicer:

App.LinkLiComponent = Em.Component.extend({
  tagName: 'li',
  classNameBindings: ['active'],
  active: function() {
    return this.get('childViews').anyBy('active');
  }.property('childViews.@each.active')
});

Em.Handlebars.helper('link-li', App.LinkLiComponent);

Usage:

{{#link-li}}
  {{#link-to "someRoute"}}Click Me{{/link-to}}
{{/link-li}}
alexspeller
  • 788
  • 7
  • 9
2

I recreated the logic used internally. The other methods seemed more hackish. This will also make it easier to reuse the logic elsewhere I might not need routing.

Used like this.

{{#view App.LinkView route="app.route" content="item"}}{{item.name}}{{/view}}

App.LinkView = Ember.View.extend({
    tagName: 'li',
    classNameBindings: ['active'],
    active: Ember.computed(function() {
      var router = this.get('router'),
      route = this.get('route'),
      model = this.get('content');
      params = [route];

      if(model){
        params.push(model);
      }

      return router.isActive.apply(router, params);
    }).property('router.url'),
    router: Ember.computed(function() {
      return this.get('controller').container.lookup('router:main');
    }),
    click: function(){
        var router = this.get('router'),
        route = this.get('route'),
        model = this.get('content');
        params = [route];

        if(model){
            params.push(model);
        }

        router.transitionTo.apply(router,params);
    }
});
Sean Smith
  • 284
  • 2
  • 4
  • 2
    I've accepted this because it is probably the best way to go about it - the other answers all broke with new Ember.js updates. However, the easiest and most reliable thing to do in my case was to patch bootstrap so that an active class on the `a` tag has the same effect as an active class on the `li`. – Nick Ragaz Mar 12 '13 at 02:47
  • 1
    Which can be done by adding the following after line `4634` in `bootstrap.css` (version 2.3.2): `.navbar .nav a.active, .navbar .nav a.active:hover, .navbar .nav a.active:focus` – knownasilya May 28 '13 at 17:16
  • This solution ends up not using an A tag which breaks the usual link functionality in browsers – alexspeller Apr 24 '14 at 02:04
1

You can skip extending a view and use the following.

{{#linkTo "index" tagName="li"}}<a>Homes</a>{{/linkTo}}

Even without a href Ember.JS will still know how to hook on to the LI elements.

Andy Jarrett
  • 863
  • 2
  • 9
  • 26
  • This solution leaves the link without a HREF which breaks usual browser interactions (right click to open in new window, copy url etc) – alexspeller Apr 24 '14 at 02:03
0

For the same problem here I came with jQuery based solution not sure about performance penalties but it is working out of the box. I reopen Ember.LinkView and extended it.

Ember.LinkView.reopen({
    didInsertElement: function(){
        var el = this.$();

        if(el.hasClass('active')){
            el.parent().addClass('active');
        }

        el.click(function(e){
            el.parent().addClass('active').siblings().removeClass('active');
        });
    }
});
Dimuthu Nilanka
  • 420
  • 4
  • 14
0

Current answers at time of writing are dated. In later versions of Ember if you are using {{link-to}} it automatically sets 'active' class on the <a> element when the current route matches the target link.

So just write your css with the expectation that the <a> will have active and it should do this out of the box.

Lucky that feature is added. All of the stuff here which was required to solve this "problem" prior is pretty ridiculous.

Purrell
  • 12,461
  • 16
  • 58
  • 70